DocumentsBetter Auth
OAuth Token Management
OAuth Token Management
Type
Topic
Status
Published
Created
Jul 9, 2026
Updated
Jul 9, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

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 TypeHandlerUse Case
authorization_codehandleAuthorizationCodeGrantUser-delegated access with PKCE or client secret
client_credentialshandleClientCredentialsGrantMachine-to-machine (M2M); no user context
refresh_tokenhandleRefreshTokenGrantObtain 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 resource audience is specified and the JWT plugin is active. Signed via createJwtAccessToken, carrying sub, aud, azp, scope, sid, iss, iat, exp, and optional custom claims.
  • Opaque access token — issued otherwise. A random string stored in the oauthAccessToken table via createOpaqueAccessToken. 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 to oauthRefreshToken), scopes, createdAt, expiresAt .
  • oauthRefreshToken — refresh tokens. Includes revoked (date, nullable) for rotation tracking and authTime for OIDC claims . Session FK uses onDelete: "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:

  1. JWTvalidateJwtAccessToken: verifies signature against JWKs, checks azp claim (rejects plain session JWTs that lack it), optionally validates the linked session .
  2. OpaquevalidateOpaqueAccessToken: looks up oauthAccessToken by token hash, checks expiry, looks up user and session.
  3. RefreshvalidateRefreshToken: checks expiry, clientId binding, revoked flag.

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). Handles kid-miss cache refetch with a 30s cooldown .
  • verifyAccessToken — higher-level wrapper. Accepts both jwksUrl (local) and remoteVerify (introspection-based) options. When remoteVerify.force is set, skips local JWS verification and always calls the introspect endpoint. Verifies audience, issuer, and optional scopes against 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#

FilePurpose
packages/oauth-provider/src/token.tsToken issuance, all grant handlers, refresh rotation
packages/oauth-provider/src/introspect.tsRFC 7662 introspection endpoint
packages/core/src/oauth2/verify.tsResource-server token verification utilities
packages/oauth-provider/src/schema.tsDB schema for tokens, clients, consents
packages/oauth-provider/src/oauth.tsPlugin entry point, endpoint registration, defaults
OAuth Token Management | Dosu