better-auth-oauth-passkey

Better Auth Integration: OAuth Providers and Passkeys

The auth service builds its Better Auth instance through a single factory. The factory reads its configuration from the environment, wires in a Prisma-backed store, mounts the passkey plugin, and registers whichever social providers are actually configured. Around that library instance sits a hand-written OAuth service that owns the account-linking and strong-factor logic the app cares about. The two together are why this node exists: the library gives the primitives, the service decides what a login means.

The factory

createAuth takes a Prisma client and returns the Better Auth instance plus the normalized base path. Before it constructs anything it fails fast: a signing secret must be present in the environment, or it throws. Base URL and base path are normalized by hand — a leading slash is forced onto the path, trailing slashes are stripped from both — so that redirect URIs compose predictably regardless of how the environment set them.

The instance is configured with:

  • Storage — a Prisma adapter over PostgreSQL, with sessions stored in the database rather than kept only in a token.
  • Email + password disabled — this deployment does not do password login through Better Auth; the strong factors are OAuth and passkeys.
  • A passkey plugin — configured with a relying-party id, a relying-party name, and one or more allowed origins. When more than one origin is supplied the plugin receives the list; with one, it receives the single value.
  • Social providers — added conditionally (see below).
  • Rate limiting — on by default, switchable off through configuration.

Conditional social providers

A provider is only registered if both its client id and its client secret are present in the environment. Google and LinkedIn are each gated this way. For each one that is configured, the factory derives the redirect URI from the normalized base URL and base path plus a per-provider callback segment, so the callback URL is never hand-typed. If no provider ends up configured, the factory logs a warning rather than failing — the service can still run on passkeys alone.

This "configure only what is present" rule means a deployment turns a provider on simply by supplying its credentials, and off by omitting them. There is no separate feature flag.

Thin plugin wrappers

The Prisma adapter and the passkey plugin are each pulled in through a one-line wrapper module that re-exports the Better Auth build under a local name. These wrappers exist to give the rest of the codebase a stable import path and a typed surface, not to add behaviour — they are seams, not logic.

The application-layer OAuth service

Alongside the library, an OAuthService carries the flows the product defines. On construction it registers the same providers (Google, LinkedIn) from configuration into a map, so it too is silent about providers that were never set up. Its jobs:

  • Token exchange — trade an authorization code for tokens against the provider's token endpoint, surfacing the provider's own error description on failure.
  • User info — fetch the profile from the provider's userinfo endpoint with the access token.
  • Login — resolve the account by email (lower-cased; an email is required). If the user exists and the provider is not yet linked, link it. If the user does not exist, create one — an OAuth email is treated as already verified because the provider verified it — then link the provider account and open a session.
  • Linking — when a caller already holds a session, the same login entry point routes into a link path instead: it requires the session, refuses to link a provider that is already linked, records the new account, and invalidates any cached profile.

A parallel provider registry lists the supported providers (Google, LinkedIn) with a display name and a connect path, so the surrounding app can render "connect" affordances without duplicating provider knowledge.

Feeding the strong-factor lifecycle

A successful OAuth login records a strong-factor assertion of type oauth against the user, and the returned user object is marked strong-factor-complete with the method and completion time set. The same object also reports whether the user has any passkeys registered. This is the join between these auth paths and the account's security posture: OAuth and passkeys are the two ways a user reaches a completed strong factor, and each login or link updates the flags the rest of the system reads to decide what the account is allowed to do.

Why it is shaped this way

Better Auth handles the mechanics that are tedious and dangerous to write by hand — WebAuthn ceremonies, the OAuth dance, session storage. The application OAuth service handles the decisions that are specific to this product — what counts as a strong factor, when an account gets created versus linked, an optional admin-by-email-domain rule at sign-up. Keeping those two apart means the product rules stay readable and the library can be upgraded underneath them.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →