OAuth Token Management#
OAuth token management in better-auth is implemented in the oauth-provider package. It covers the full lifecycle: token issuance via multiple grant types, storage, expiration, refresh-token rotation, introspection, and remote validation for resource servers.
Token Endpoint & Grant Types#
All token issuance flows through POST /oauth2/token, dispatched by tokenEndpoint in token.ts. Three grant types are supported :
| Grant Type | Handler | Use Case |
|---|---|---|
authorization_code | handleAuthorizationCodeGrant | User-delegated access with PKCE or client secret |
client_credentials | handleClientCredentialsGrant | Machine-to-machine (M2M); no user context |
refresh_token | handleRefreshTokenGrant | Obtain new tokens using a refresh token |
M2M Client Credentials#
The client_credentials grant issues a user-less access token per RFC 6749 §4.4. OIDC scopes (openid, profile, email, offline_access) are explicitly rejected for this grant . Default scopes fall back to the client's registered scopes, then clientCredentialGrantDefaultScopes, then the provider's global scopes . The M2M access-token TTL is configured separately via m2mAccessTokenExpiresIn (default 3600s) .
Token Formats: JWT vs. Opaque#
The format of an access token depends on whether the resource parameter is present in the request and whether the JWT plugin is enabled :
- JWT access token — issued when a
resourceaudience is specified and the JWT plugin is active. Signed viacreateJwtAccessToken, carryingsub,aud,azp,scope,sid,iss,iat,exp, and optional custom claims. - Opaque access token — issued otherwise. A random string stored in the
oauthAccessTokentable viacreateOpaqueAccessToken. Used for introspection-based validation.
Token storage can be hashed or encrypted, controlled by storeTokens (default "hashed") .
Storage Schema#
Three DB models back token state :
oauthAccessToken— opaque access tokens only. Fields:token,clientId,userId(nullable for M2M),sessionId,refreshId(FK tooauthRefreshToken),scopes,createdAt,expiresAt.oauthRefreshToken— refresh tokens. Includesrevoked(date, nullable) for rotation tracking andauthTimefor OIDC claims . Session FK usesonDelete: "set null"so session deletion does not cascade-revoke active refresh tokens.oauthConsent— persists user consent per client/scope set .
Default TTLs (all overrideable in plugin options) :
- Authorization code: 600s (10 min)
- Access token: 3600s (1 hr)
- Refresh token: 2592000s (30 days)
Refresh Token Rotation & Family Invalidation#
Refresh tokens are rotated on every use. createRefreshToken uses an atomic compare-and-swap (incrementOne where revoked IS NULL) to mark the parent row revoked while creating a successor row. Concurrent redemption of the same refresh token causes one caller to win the CAS and the other to receive invalid_grant .
On replay of a revoked refresh token, invalidateRefreshFamily tears down the entire (client, user) refresh-token family per RFC 9700 §4.14, deleting child access tokens first to avoid FK constraint violations .
Introspection Endpoint (RFC 7662)#
POST /oauth2/introspect is implemented in introspectEndpoint. The endpoint requires client credentials (Basic auth or body params) and accepts an optional token_type_hint.
Resolution order when no hint is provided: access token → refresh token. Internal dispatch via validateAccessToken, which tries JWT verification first, then falls back to opaque token DB lookup:
- JWT —
validateJwtAccessToken: verifies signature against JWKs, checksazpclaim (rejects plain session JWTs that lack it), optionally validates the linked session . - Opaque —
validateOpaqueAccessToken: looks upoauthAccessTokenby token hash, checks expiry, looks up user and session. - Refresh —
validateRefreshToken: checks expiry,clientIdbinding,revokedflag.
All three return an RFC 7662 §2.2-shaped payload (active, sub, scope, client_id, exp, iat, iss, sid). Bad-request errors are caught and returned as { active: false } rather than propagated .
Pairwise subject identifiers are resolved at the presentation layer by resolveIntrospectionSub before the response is returned.
Resource Server: Remote Validation Utility#
packages/core/src/oauth2/verify.ts exports two functions for resource servers that need to validate tokens they receive:
verifyJwsAccessToken— local JWT verification against a JWKs endpoint. Caches the key set with a 5-minute TTL , keyed by URL (string sources) or a stable caller-provided object (jwksCacheKey, WeakMap, for function sources). Handleskid-miss cache refetch with a 30s cooldown .verifyAccessToken— higher-level wrapper. Accepts bothjwksUrl(local) andremoteVerify(introspection-based) options. WhenremoteVerify.forceis set, skips local JWS verification and always calls the introspect endpoint. Verifiesaudience,issuer, and optionalscopesagainst the payload .
The remoteVerify config :
introspectUrl – full URL of /oauth2/introspect
clientId – credential for the introspect call
clientSecret – credential for the introspect call
force? – always use remote verification
allowMissingAudience? – skip aud check when absent (opt-in, see RFC 7662 §2.2)
Key Source Files#
| File | Purpose |
|---|---|
packages/oauth-provider/src/token.ts | Token issuance, all grant handlers, refresh rotation |
packages/oauth-provider/src/introspect.ts | RFC 7662 introspection endpoint |
packages/core/src/oauth2/verify.ts | Resource-server token verification utilities |
packages/oauth-provider/src/schema.ts | DB schema for tokens, clients, consents |
packages/oauth-provider/src/oauth.ts | Plugin entry point, endpoint registration, defaults |