oauthprovider

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

README

github.com/ubgo/auth/plugin/oauthprovider

Makes the app an OAuth 2.1 authorization server and OpenID Connect provider. Other applications send their users here to sign in, and they get back an authorization code, then access and refresh tokens and an ID token. It ports better-auth's oauth-provider plugin. It is opt-in, and it is its own module.

Install

go get github.com/ubgo/auth/plugin/oauthprovider

It depends on the core module and sqlb only. ID tokens are signed by the core module's plugin/jwt, whose keys are published at <issuer>/oauth2/jwks.

Wire it

jw := jwt.New(jwt.WithSigningKey(key), jwt.Issuer("https://auth.example.com"))
op := oauthprovider.New(jw,
	oauthprovider.Issuer("https://auth.example.com"),
	oauthprovider.LoginPage("/login"),
	oauthprovider.ConsentPage("/consent"),
)
a, err := auth.New(auth.WithDB(db, sqlb.PostgresDialect{}), auth.WithBaseURL("https://auth.example.com"),
	auth.WithPlugins(email.New(email.Login(), email.Recover(), email.WithMailer(m)), password.New(), jw, op))

mux.Handle("/auth/", authhttp.Handler(a, authhttp.Prefix("/auth"))) // consent, client registry, consents
mux.Handle("/.well-known/", op.Handler())                           // discovery
mux.Handle("/oauth2/", op.Handler())                                // authorize, token, userinfo, ...

List jw before op. The jwt plugin's Issuer must be the provider's Issuer; auth.New signs a probe token and refuses a mismatch (ErrIssuerMatch).

New takes a Signer: Sign(ctx, jwt.SignRequest) (jwt.TokenResult, error) and PublicKeys() jwt.JWKS. *jwt.Plugin satisfies it and is the default; a KMS, an HSM or a key service the app already runs can stand in. A signer that is a plugin is required by name (Requires) and must be listed before the provider; one that is not a plugin needs no registration, so the jwt plugin's token route is not mounted. The issuer probe runs for every signer. A custom signer keeps the jwt plugin's contract: iss is the issuer, the claims are sub, aud, iat, exp plus the request's, typ is the request's Type (JWT when empty), the header's kid names a key PublicKeys lists, and the first listed key's alg is the ID tokens' (it decides the at_hash hash). Register clients from Go with op.CreateClient, or let users register their own (see below).

Endpoints

Handler serves the protocol endpoints at their paths under the issuer, in the wire format the RFCs fix. Each one is a self-served operation (auth.OpInfo.SelfServed), so it runs the full pipeline, with rate limits, hooks and tracing. No other transport mounts it.

Path Method What
/.well-known/openid-configuration GET OpenID Connect Discovery; 404 when openid is not a supported scope
/.well-known/oauth-authorization-server GET RFC 8414 metadata
/oauth2/authorize GET, POST Authorization endpoint: response_type=code only, PKCE S256
/oauth2/token POST (form) authorization_code, refresh_token, client_credentials
/oauth2/userinfo GET, POST OIDC UserInfo; token in Authorization: Bearer or the form, never both
/oauth2/introspect POST (form) RFC 7662; a confidential client sees its own tokens, a client made with IntrospectAny sees every client's
/oauth2/revoke POST (form) RFC 7009; a refresh token revokes its whole family
/oauth2/end-session GET, POST OpenID Connect RP-initiated logout, as better-auth: a valid id_token_hint for the browser's user ends the session at once; otherwise a browser gets a confirmation page (a signed state cookie, 5 minutes) and anything else invalid_request. The client needs EnableEndSession, which needs auth.WithSecret (ErrNoSecret from auth.New for a trusted client, from CreateClient and UpdateClient otherwise; without a secret discovery does not advertise the endpoint). A post_logout_redirect_uri is followed only when registered exactly; otherwise the success page says it was not registered
/oauth2/end-session/confirm POST (form) The confirmation page's form (action=confirm); the signed state is the only context it accepts, and it ends only the session the state was made for
/oauth2/register POST (JSON) RFC 7591 dynamic registration, closed unless AllowDynamicRegistration. Only redirect_uris is required; the answer always carries client_name, grant_types, response_types, token_endpoint_auth_method and scope as registered (the input is ClientMetadata, the answer's Client embeds RegisteredMetadata, the same fields with those always present)
/oauth2/jwks GET The jwt plugin's public keys

These operations are served by every transport under /oauthprovider:

Route Requires What
POST /oauthprovider/consent signed in The consent page's answer: {oauth_query, accept, scope} returns {redirect_to}
GET /oauthprovider/public-client?client_id= signed in What a consent page shows: name, URIs, contacts
POST /oauthprovider/create-client fresh sign-in A client the user owns; its secret is shown once
GET /oauthprovider/clients, GET /oauthprovider/client?client_id= signed in The user's clients; another user's client is client_not_found
POST /oauthprovider/update-client fresh sign-in Replace metadata; the authentication method cannot change
POST /oauthprovider/rotate-secret fresh sign-in New secret; the old one stops at once
POST /oauthprovider/delete-client signed in Deletes the client with its codes, tokens and consents
GET /oauthprovider/get-consent?id=, POST /oauthprovider/update-consent signed in One consent; replace its scopes (only scopes the client may request)
GET /oauthprovider/consents, POST /oauthprovider/revoke-consent signed in The user's consents; withdrawing one revokes that client's tokens for the user

Go callers use Authorize, Consent, Token, UserInfo, Introspect, Revoke, Register, CreateOwnedClient, Clients, OwnedClient, UpdateOwnedClient, RotateSecret, DeleteOwnedClient, PublicClientInfo, Consents, ConsentByID, UpdateConsent, RevokeConsent and EndSession. Trusted code also has CreateClient(ClientSpec) and UpdateClient, which can set SkipConsent, OptionalPKCE, IntrospectAny, EnableEndSession and OwnerID, plus SetClientDisabled, DeleteClient, Client, VerifyAccessToken and Metadata.

The browser flow

  1. A signed-out browser at /oauth2/authorize goes to LoginPage, with the authorize parameters appended. After sign-in, the page sends it to op.ResumeURL(query).
  2. A signed-in user who has not consented to this client and these scopes goes to ConsentPage, also with the parameters. The page shows PublicClientInfo and posts {oauth_query: <its query string>, accept: true|false} to the consent operation. The query is validated again in full, so nothing the page sends is trusted. Without a ConsentPage the answer is consent_required. A client made with SkipConsent never asks.
  3. The browser then goes to the client's redirect URI with code, state and iss (RFC 9207), or with error and state.

prompt=none never shows a page: it answers login_required or consent_required. prompt=login and prompt=select_account send even a signed-in user to the login page. The prompt is dropped from those parameters, so the page cannot loop. prompt=consent forces the consent page. max_age sends a user whose session is older to the login page, and login_hint is passed to it. prompt=create goes to SignupPage. A fetch caller (Sec-Fetch-Mode: cors or Accept: application/json) gets redirects as {"redirect": true, "url": ...}. request and request_uri are refused (request_not_supported, request_uri_not_supported). ID tokens carry at_hash: SHA-512 for EdDSA keys, SHA-256 otherwise.

Security rules

Each rule has a test in integration/r33_test.go that was broken once to prove it fails:

  • A redirect URI matches a registered one as an exact string. An unknown client or unregistered URI is answered to the user, never redirected (invalid_redirect_target), so the endpoint is not an open redirector. Registration takes only https, loopback http and reverse-domain private-use schemes, with no fragment and no credentials.
  • PKCE with S256 is mandatory for public clients and the default for confidential ones; plain is refused. A code issued without a challenge refuses a verifier.
  • A code is single use, expires after 10 minutes, and is bound to its client, its redirect URI and its verifier. Reusing it revokes every token issued from it.
  • A refresh token rotates on every use. Presenting a spent one revokes its whole family.
  • A client secret is compared in constant time against its SHA-256, and a client must authenticate with its registered method. client_credentials is refused to public clients and never grants openid, profile, email or offline_access.
  • Client secrets, codes, and access and refresh tokens are stored only as SHA-256 hashes. Their prefixes (ubos_, uboc_, uboa_, ubor_) let secret scanners recognize a leak.
  • The token, revocation, introspection and userinfo endpoints ignore the browser's cookie, so they are open to CORS without carrying a session.

Options

Option Default What
Issuer(url) required The iss and the base of every URL; https, or http on loopback
LoginPage(url) required Same-site path or a URL on a trusted origin
ConsentPage(url) none Without it, clients that need consent get consent_required
BasePath(path) /oauth2 Where the protocol endpoints live under the issuer
Scopes(...) openid profile email offline_access Supported scopes; without openid it is a plain OAuth 2.1 server
GrantTypes(...) all three refresh_token needs authorization_code
CodeTTL, AccessTokenTTL, RefreshTokenTTL, IDTokenTTL 10m, 1h, 30d, 10h Lifetimes (better-auth's defaults)
M2MAccessTokenTTL(d) the access TTL Lifetime of client_credentials access tokens
ScopeExpirations(map) none Shorter access-token lifetimes per scope; a token gets the earliest
CacheResources(ttl), CacheResourcesIn(kv, ttl) off Keeps resource policies for ttl (1m for 0, at most 15m), in this process or in any auth.SecondaryStorage; see Resources below
SignupPage(url) none Where prompt=create goes; the login page without it
ValidateRedirectURI(fn) exact match Replaces the redirect decision (gets the URI, the registered ones and the exact-match result); relaxes RFC 9700, so accept only hosts you control
AccessTokenClaims(fn) none Extra claims in JWT access tokens; provider claims cannot be replaced
AdvertisedScopes(...), AdvertisedClaims(...) every scope, the built-in claims What discovery lists
GenerateClientID(fn), GenerateClientSecret(fn) random At least 22 characters; a client id may never be an https URL
PairwiseSecret(s) none Pairwise subjects for clients registered with subject_type: pairwise (at least 32 characters)
Claims(fn) none Extra ID token and userinfo claims; it cannot override iss, sub, aud, exp, iat, nonce, auth_time or azp
AllowDynamicRegistration() off RFC 7591 registration for signed-in users
AllowUnauthenticatedRegistration() off Registration with no session, as MCP clients expect; such a client has no owner
RefreshReuseInterval(d) 0 A just-rotated refresh token works again within d (a new pair each time), for clients that refresh from two tabs; after d, reuse revokes the family
AssertionMaxLifetime(d) 5m The longest a private_key_jwt assertion may live
RateLimit(endpoint, n, window), NoDefaultRateLimits() better-auth's Per-IP limits per endpoint; n 0 removes one
ClientIDMetadataDocuments(...) off CIMD: https client ids are fetched metadata documents
ProtectedResource(url, scopes...) none A resource server (an MCP server) whose RFC 9728 metadata Handler serves at /.well-known/oauth-protected-resource{path}
MCP() off better-auth's mcp(): AllowUnauthenticatedRegistration and a 30-second RefreshReuseInterval (MCPRefreshReuseInterval); later options override either
RegistrationDefaultScopes(...), RegistrationAllowedScopes(...) every scope A registration naming no scope gets the defaults; one naming a scope outside defaults and allowed is invalid_scope
RegistrationSecretExpiresAt(t) never A fixed end for dynamically registered secrets; with RegistrationSecretExpiration the earlier wins. An instant not after now on the app's clock (auth.WithClock) fails auth.New with ErrBadTTL
ClientJWKSFetch(fn) public addresses only Replaces the jwks_uri fetcher (tests, an egress proxy)
DPoPSigningAlgorithms(...) EdDSA ES256 ES512 PS256 RS256 The DPoP proof algorithms accepted and advertised
HashStoredSecrets(fn) SHA-256 The stored form of client secrets, codes, access and refresh tokens (SecretKind says which): deterministic, 32 bytes, refused at auth.New otherwise
GenerateAccessToken(fn), GenerateRefreshToken(fn) 256 random bits The random part of opaque access and refresh tokens; at least 22 characters
UserInfoInsufficientScope() off Userinfo for a token without openid answers RFC 6750's 403 insufficient_scope instead of better-auth's 400 invalid_scope
SelectAccountFallsBackToLogin() off prompt=select_account with no SelectAccount page goes to the login page instead of better-auth's unsupported_prompt_select_account
RevokeAccessTokenOnly() off, warns at start better-auth's revocation: an access token's refresh token keeps working. Risk: a leaked grant survives revoking its access token
RefuseJWTRevocation() off, warns at start better-auth's answer for a live JWT access token, unsupported_token_type. Risk: the token stays usable everywhere until it expires
EncryptClientSecrets(enc) off, warns at start better-auth's encrypted client secrets: a recoverable copy beside the hash, read with RevealClientSecret. Risk: the key plus the database yields every secret
RefreshReplay(enc) off, warns at start better-auth's replay of the first response to a refresh token reused inside RefreshReuseInterval (required). Risk: issued tokens are stored recoverable for the window

Resource servers and MCP

RequireToken(RequireOptions{Resource, Scopes, JSONRPC, ChallengeScopes}) guards a resource server's routes, which is better-auth's requireMcpAuth. It verifies the bearer access token and every required scope, and puts the token in the context (TokenFrom). A missing or bad token gets 401, and a missing scope gets 403 insufficient_scope naming the missing scopes. Each refusal carries the WWW-Authenticate: Bearer challenge with resource_metadata, which is how an MCP client finds the authorization server. JSONRPC answers in the JSON-RPC 2.0 error envelope (code -32000, id null). ChallengeScopes names other scopes in a 401 challenge than the ones checked (better-auth's challengeScopes). An MCP setup is ProtectedResource plus MCP() plus RequireToken on the MCP endpoint.

Resource indicators and JWT access tokens

A client asks for a token for one API by sending resource (RFC 8707), at authorize or in a client_credentials request. The resource must be declared with ProtectedResource, or the answer is invalid_target. Its scopes must be user scopes or the resource's own, or the answer is invalid_scope. The access token for a resource is a JWT signed with the jwt plugin's key: sub (the user, or the client for client_credentials), aud (the resource), client_id, scope, iat, exp, jti and token_id. A service in another process verifies it offline with jwt.Verifier{Keys: jwt.KeysFromJWKS(...), Issuer: issuer, Audience: resource, Types: []string{oauthprovider.AccessTokenType}} against /oauth2/jwks. It is stored hashed like an opaque token, so revocation and introspection (which answers aud) still apply in-process, and RequireToken with Resource accepts only tokens for that resource. A refresh keeps the resource; a different one at the exchange or refresh is invalid_target. Without resource the access token stays opaque. The header typ is at+jwt (RFC 9068, as better-auth), so it is never taken for an ID token (JWT). ID tokens carry sid, the session they were issued in, which RP-initiated logout matches against the browser's session.

Client authentication with private_key_jwt

A client registered with AuthMethod: AuthMethodPrivateKeyJWT and an inline JWKS or a JWKSURI (one or the other; public Ed25519, RSA of at least 2048 bits, or P-256, P-384 or P-521 keys with a kid; a private or symmetric member is refused) has no secret. It authenticates at the token, introspection and revocation endpoints with client_assertion and client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer (RFC 7523). The assertion is signed by one of its keys, has iss and sub equal to the client_id, an aud of the endpoint URL or the issuer, an exp at most AssertionMaxLifetime ahead, and a jti that works once: spent ids are kept in auth_oauth_assertion until they expire. A secret together with an assertion is invalid_request. When the form has no client_id, the assertion's sub names the client, and the signature decides. Every jwt.AlgValues algorithm is accepted (listed as token_endpoint_auth_signing_alg_values_supported); a key with no alg verifies under the header's algorithm within its family (any RSA algorithm for an RSA key, the curve's for an EC key), and the header needs no typ.

A jwks_uri must be https on an origin the app trusts (the client_id's own origin for a CIMD client), with no credentials or fragment. The set is fetched on first use by a fetcher that reaches public addresses only, follows no redirect, times out after 5 seconds and reads at most 64 KiB of an exactly-200 JSON or application/jwk-set+json answer. It is cached 5 minutes (500 sets at most), served stale up to 10 minutes while the host fails, and fetched again when an assertion fails to verify, at most every 30 seconds. ConsumeClientAssertion(ctx, clientID, assertion, audience) runs the same check, jti spent, for an endpoint of the app's own.

Client ID metadata documents (CIMD)

With ClientIDMetadataDocuments(), a client_id that is an https URL names a JSON document the client publishes. The first authorize or token request fetches it and stores it as a client row. It is fetched again once older than CIMDRevalidate (1 hour), and a failed refetch refuses the request. The default fetcher only reaches public addresses (RFC 6890 ranges refused), dials the address it checked, follows no redirect, times out after 5 seconds, and reads at most 5 KB of an exactly-200 application/json or application/*+json answer. At most 16 fetches run at once.

A document is refused (invalid_client, or the authorize error page) when:

  • its client_id differs from the URL;
  • it sets a member only the server decides, such as client_secret, skip_consent or require_pkce, in snake or camel case (skipConsent);
  • it uses a shared-secret method (only none or private_key_jwt is allowed);
  • a URL of an origin-bound field is on another origin: client_uri and the post-logout URIs by default (DefaultCIMDOriginBoundFields), CIMDOriginBoundFields(...) to choose (loopback and private-use schemes allowed in the redirect fields), and the jwks_uri always;
  • none of its grants is one the provider serves (unknown ones are dropped; one is enough). Without redirect_uris it keeps only grants that need none, such as client_credentials.

IsCIMDClientID, ValidCIMDClientID and ValidateCIMDDocument expose the same checks to Go.

The client_id URL needs an https host and a path that is not /, with no dot segments, fragment or credentials. CIMDAllow gates URLs before any fetch, CIMDProfileMCP requires client_name and redirect_uris, and CIMDFetch replaces the fetcher (tests, an egress proxy). Registered clients have generated ids, so a document can never take one over. Discovery advertises client_id_metadata_document_supported.

A fetched document stays fresh for its HTTP lifetime, capped by CIMDRevalidate. better-auth's rules apply:

  • s-maxage takes precedence over max-age, which takes precedence over Expires, less Age;
  • no-store, private, no-cache and Vary: * give a lifetime of zero, and so does a repeated or invalid directive.

A stale document is revalidated with If-None-Match and If-Modified-Since. A 304 keeps it, but only when validators were stored. Concurrent requests for one URL share one fetch. The governor (CIMDPolicy) caps fetches: 16 in flight, 4 per origin, 120 a minute and 30 per origin a minute, with retries of a failed URL paced to one a second. Fresh hits and joined fetches spend nothing, and a fetch over budget is refused (429 temporarily_unavailable), never queued. CIMDOnClientCreated and CIMDOnClientRefreshed are best-effort hooks, and CIMDTransport replaces the fetcher with one that sees the conditional headers.

The resource registry

Resources are better-auth's oauthResource rows: CreateResource, Resource, Resources, UpdateResource and DeleteResource from Go, with LinkClient and UnlinkClient for the client links (a second link answers alreadyLinked). A resource sets AllowedScopes, which narrows a token's scopes by intersection (invalid_scope when none are left), shorter AccessTTL and RefreshTTL, CustomClaims for its JWT access tokens and introspection (protected claims ignored), DPoPRequired, Disabled and Metadata. A resource declared with ProtectedResource and no row has the provider's defaults; a row overrides it. Several resources in one request (the parameter repeated over HTTP, space-separated in Go) combine their policies: the shortest lifetimes, DPoP when any requires it, every custom claim, and one signing algorithm and key at most (conflicting pins are invalid_request); the JWT's aud names each, introspection answers aud as an array, a token or refresh request may narrow to some of them, and AccessToken.HasResource answers for one. Resource.SigningKeyID pins the signing key by key id (jwt.AlgorithmKeys may hold several keys of one algorithm), checked by a probe signature at write; PolicyVersion counts writes. A consent records its resources, so a new resource asks again. LinkClientWithMetadata and ClientResourceLinks read and write a link's metadata. PatchResource(ctx, id, ResourcePatch{...}) changes only the fields it names, and SeedResources(ctx, mode, seeds...) writes configured resources after migrations: SeedInsertOnly (the default) leaves existing rows, SeedMerge sets the named fields, SeedOverwrite replaces the policy.

CacheResources(ttl) is better-auth's cachedResources, opt-in: the resource policies an issuance, a refresh and an introspection read are kept in this process for ttl, and CacheResourcesIn(kv, ttl) keeps them in any auth.SecondaryStorage instead (contrib/redis's NewStorage, or the store already given to auth.WithSecondaryStorage; only Get, Set and Delete are used, entries carry their own expiry judged by the core's clock, and a failing store is logged and the database read instead), so every instance shares one cache (0 is DefaultResourceCacheTTL, one minute; above MaxResourceCacheTTL, 15 minutes, or negative is ErrBadResourceCacheTTL at auth.New). CreateResource, UpdateResource, PatchResource, SeedResources and DeleteResource drop the entry once they have written, so this instance's writes are seen at once, and with a shared store every instance's; with the in-process cache, a change on another instance, or by SQL, is seen when the entry expires, as in better-auth. Resource and Resources always read the database, and an identifier with no row is remembered only when it is a ProtectedResource declared in code, so requests for random identifiers cannot grow the cache.

Every issuance resolves the resource again, as better-auth does:

  • an unknown or disabled resource is invalid_target;
  • a client not linked to the resource is invalid_target (EnforcePerClientResources(false) turns this off);
  • a token whose resource was deleted becomes inactive, while a disabled resource's issued tokens keep working until they expire.

A token for a resource carries the userinfo URL as a second audience when openid is granted. A client linked to a token's resource may introspect it. RegistrationDefaultResources are linked to every new client, RegistrationAllowedResources are the extra ones a registration's resources member may name, and IdentifierValidator replaces the RFC 8707 identifier check.

Client policy

These match better-auth:

  • client_credentials needs a server-owned ceiling (ClientSpec.ClientCredentialsScopes): without one the grant is unauthorized_client, a scope outside it or a user scope is invalid_scope, and an omitted scope takes the whole ceiling.
  • application_type is web (the default: https on non-loopback hosts) or native (https on non-loopback hosts, http only on exactly localhost, 127.0.0.1 or [::1], or a reverse-domain private-use scheme). A CIMD document without one is held to the native rules.
  • ClientSpec.SecretExpiresAt ends a secret, and an expired one authenticates nothing. RegistrationSecretExpiration gives dynamically registered confidential clients one, and RegistrationRequirePKCE(false) lets them omit PKCE.
  • ReferenceID and Metadata are stored as given.
  • An owner may set the client_credentials ceiling through the operations (client_credentials_scopes at creation, ClientCredentialsScopes in an update) only when ClientPrivileges exists and grants ActionConfigureCCScopes; an anonymous registration and a metadata document may never set it.

Resource servers in other processes

ResourceGuard{Issuer, Audience, Keys, IntrospectURL, ClientID, ClientSecret, AllowMissingAudience, Scopes, ChallengeScopes, ScopeSatisfied, ResourceMetadataURL, JSONRPC}.Middleware is better-auth's createMcpProtectedRequestHandler. It verifies at+jwt access tokens offline with the provider's keys (jwt.NewRemoteKeys(...).Lookup), and opaque ones by introspection (remoteVerify): the introspected audience must name this resource, and one that names none is refused unless AllowMissingAudience is set. It then checks scopes and answers the same challenges as RequireToken; the verified token is in GuardedTokenFrom(ctx). DPoP-bound tokens are refused there, because the guard checks no proofs. RequireOptions.ScopeSatisfied replaces the exact scope match, WriteInsufficientScope answers a 403 from inside a handler, and ProtectedResourceMetadata builds an RFC 9728 document for a resource served elsewhere.

Signed query, pre-login, and registration options

With auth.WithSecret, the parameters the provider hands a login or consent page are signed as better-auth signs them: exp (the code lifetime), ba_iat, the sorted ba_param names, and sig, an HMAC over the canonical query. The consent operation verifies the oauth_query it receives (invalid_signature for a changed, unsigned or expired one), and VerifyOAuthQuery does the same from Go. AllowPublicClientPrelogin serves POST /oauthprovider/public-client-prelogin (PublicClientPrelogin), which shows a client's public fields to a page holding a valid signed query for that client, before sign-in; it and PostLogin need auth.WithSecret, and auth.New refuses them without one (ErrNoSecret).

TokenResponseFields(fn) adds members to the token response (never replacing a standard one), before any token is stored. ValidateInitialAccessToken(fn) accepts an RFC 7591 initial access token as Authorization: Bearer on /oauth2/register: a valid one admits the registration even when open registration is off and sets the client's ReferenceID, and a presented token is refused (invalid_token) when no validator is configured. software_id, software_version and software_statement are stored as given. A resource's SigningAlgorithm pins its JWT access tokens to one of the jwt plugin's algorithms. ClientReference(fn) makes clients belong to a reference (an organization) instead of the user, and ClientPrivileges(fn) gates each owner action (create, read, update, delete, list, rotate; forbidden when refused).

Pairwise subjects

With PairwiseSecret, a client registered with subject_type: "pairwise" sees each user as base64(HMAC-SHA256(secret, "<host of its first redirect URI>.<user id>")), better-auth's computation byte for byte, so two clients cannot join their users by sub. It applies in ID tokens, userinfo, introspection and JWT access tokens. Such a client's redirect URIs must all be on one host (the sector), and without the secret the registration is refused. Discovery then lists subject_types_supported: ["public", "pairwise"].

The claims parameter

An authorize request with openid may send the OpenID Connect claims parameter (Core section 5.5), as better-auth reads it. It must be a JSON object whose userinfo and id_token members map claim names to null or a request object; anything else is invalid_request, as is the parameter without openid. An essential acr that does not allow "0" (the only level this provider asserts) is access_denied. The userinfo names the provider supports are recorded on the consent, so a request for a name not yet consented asks again. Userinfo then releases those claims even without their scope. The standard claims are name, picture, given_name and family_name (split from the display name), email and email_verified. Discovery lists claims_parameter_supported and acr_values_supported: ["0"].

Pushed authorization requests

With RequestURIResolver(fn), an authorize request with request_uri takes every parameter from what fn returns, except client_id, which comes from the URL (RFC 9126 section 4); a nil answer is invalid_request_uri. This matches better-auth's requestUriResolver. Without a resolver request_uri is request_uri_not_supported, and request together with request_uri is invalid_request. The pushed-request endpoint itself is the app's to serve.

Back-channel logout

A client registered with backchannel_logout_uri (https, or http on loopback) is told when a session it holds tokens for ends: sign-out, a revoked session, a password reset or a ban. After the commit, the provider POSTs logout_token, a logout+jwt signed with the ID-token key, carrying iss, aud, sub (pairwise when the client is), sid, iat, exp (two minutes), jti and the back-channel event, and never a nonce. Each client gets one attempt with a 5-second timeout and no redirects, as OpenID Connect Back-Channel Logout 1.0 section 2.5 asks. The session's access tokens stop at once; its refresh tokens stop too unless offline_access was granted (section 2.7). BackchannelHTTPClient replaces the HTTP client. Discovery lists backchannel_logout_supported and backchannel_logout_session_supported.

DPoP

A client registered with dpop_bound_access_tokens: true, an authorize request with dpop_jkt, or a bound refresh token makes the token endpoint require a DPoP proof (RFC 9449). Any token request may send one. The proof must:

  • be a dpop+jwt, signed with one of DPoPSigningAlgorithms (default EdDSA, ES256, ES512, PS256, RS256), by the public JWK in its header (a private or symmetric member is refused; a key with no alg is read for the header's algorithm within its family);
  • name htm and htu (origin and path) of the request;
  • have an iat at most 5 seconds ahead and DPoPProofMaxAge (300 seconds) old;
  • carry a jti used once for that key and request.

A bound token answers token_type: "DPoP", carries cnf.jkt (in the JWT access token and in introspection), and is accepted at userinfo and RequireToken only with Authorization: DPoP <token> and a fresh proof of the same key whose ath is the token's hash. The DPoP scheme with an unbound token is invalid_token. Failures are invalid_dpop_proof with a WWW-Authenticate: DPoP challenge. VerifyPresentedToken applies the same rules from Go.

Extension grants

WithGrant(ExtensionGrant{Type, Handle, ErrorNamespace}) serves an extra grant_type, an absolute URI, at the token endpoint. The device authorization grant module uses it. The provider authenticates the client and checks that it registered the grant type before calling Handle. Handle returns a GrantDecision (user, scopes, resource, auth time), and the provider checks the scopes and issues tokens through the same path as every other grant. Errors whose code starts with ErrorNamespace. keep the rest as their wire code (authorization_pending). Extension grants are listed in grant_types_supported; DeviceAuthorizationEndpoint(url) adds device_authorization_endpoint. AuthenticateClient and CheckScopes are exported for a module that serves an endpoint of its own, with IssueTokens(ctx, IssueRequest{GrantType, ClientID, UserID, Scopes, Resource, Confirmation, ...}) (better-auth's issueTokens: a raw minting primitive, the caller has decided the grant), HashToken(token, kind) and ConsumeClientAssertion.

Revocation, userinfo and error names

better-auth's behaviour is the default where it is no weaker, and an opt-in that warns at every start where it is. By default revoking an access token ends its refresh token too, and a JWT access token is revoked like an opaque one; RevokeAccessTokenOnly and RefuseJWTRevocation give better-auth's answers. Userinfo without openid is 400 invalid_scope, and a token whose session has ended is 401 invalid_token there. Wire codes follow better-auth: invalid_redirect (an unregistered redirect URI), invalid_user (a grant whose user was deleted), unsupported_prompt_select_account, not_found (a missing client or consent), unsupported_token_type. invalid_software_statement, unapproved_software_statement and invalid_request_object are never answered, as in better-auth: software statements are stored as given and request objects are request_not_supported.

Authorization requests

An unknown client_id is invalid_client and a disabled client client_disabled; neither is sent to the redirect URI, and a browser (an Accept: text/html navigation) gets an HTML error page with no-store, a CSP and nosniff (errors.Is also sees ErrBadRedirectTarget). A refresh token needs PKCE: offline_access without a challenge is invalid_request unless a confidential client sends openid and a nonce. display and ui_locales are passed to the login, consent and other pages. An id_token_hint must be an ID token this provider signed for the client (expired is fine, invalid_request otherwise); when it names another user than the signed-in one, the browser goes to the login page, or prompt=none answers login_required. Logout errors are the same HTML page for a browser. The consent operation's claims (a claims-request object) grants fewer of the requested userinfo claims. Userinfo without any credential answers 401 with a bare WWW-Authenticate: Bearer. Introspection reports an access token inactive once its session has ended, by sign-out or by expiry; a refresh token outlives its session.

Tables

auth_oauth_client, auth_oauth_code, auth_oauth_token, auth_oauth_consent and auth_oauth_assertion, with no foreign keys; migration 0013 adds the client's jwks_uri. DeleteUser removes a user's rows, and Sweep deletes expired codes and fully expired tokens.

Not yet

These are not built yet:

  • request objects (request, refused) and a pushed-request endpoint (the resolver is the app's)
  • DPoP nonces
  • extension client-authentication strategies
  • the device grant via the provider, which plugin/deviceauth wires through WithGrant
  • the Agent Auth protocol, which is plugin/agentauth

Documentation

Overview

Package oauthprovider makes the app an OAuth 2.1 authorization server and OpenID Connect provider: other applications ("clients") send their users here to sign in, and receive an authorization code, access and refresh tokens, and an ID token. It is better-auth's oauth-provider plugin.

It is opt-in and its own module. The protocol endpoints (authorize, token, userinfo, introspection, revocation, dynamic registration, discovery and the key set) are served by Plugin.Handler in the wire format the RFCs fix; each one is a self-served auth operation underneath (auth.OpInfo .SelfServed), so it runs the same pipeline as every other operation: rate limits, hooks, tracing. The management operations (consent, the owner's client registry, the user's consents) are ordinary operations every transport mounts under /oauthprovider.

Security rules, each pinned by a test: a redirect URI matches a registered one exactly; PKCE (S256) is mandatory for public clients and the default for every client; a code is single use, bound to its client, redirect URI and PKCE verifier, and its reuse revokes every token issued from it; a refresh token rotates on every use and the reuse of a rotated one revokes its whole family; client secrets are compared in constant time; every secret (client secret, code, access and refresh token) is stored as its SHA-256 and shown once.

Index

Constants

View Source
const (
	CIMDFieldClientURI      = "client_uri"
	CIMDFieldLogoURI        = "logo_uri"
	CIMDFieldTosURI         = "tos_uri"
	CIMDFieldPolicyURI      = "policy_uri"
	CIMDFieldRedirectURIs   = "redirect_uris"
	CIMDFieldPostLogoutURIs = "post_logout_redirect_uris"
)

CIMD origin-bound fields: the document members CIMDOriginBoundFields may name.

View Source
const (
	DefaultCIMDMinimumFetchInterval   = time.Second
	DefaultCIMDMaxConcurrent          = 16
	DefaultCIMDMaxConcurrentPerOrigin = 4
	DefaultCIMDMaxPerMinute           = 120
	DefaultCIMDMaxPerOriginPerMinute  = 30
	// DefaultCIMDMaxCacheEntries bounds the governor's per-URL and
	// per-origin records, better-auth's maxCacheEntries.
	DefaultCIMDMaxCacheEntries = 1000
)

better-auth's metadataFetchPolicy defaults.

View Source
const (
	// DPoPProofType is a proof's typ header.
	DPoPProofType = "dpop+jwt"
	// SchemeDPoP is the Authorization scheme and token_type of a bound
	// token.
	SchemeDPoP = "DPoP"
	// HeaderDPoP carries the proof.
	HeaderDPoP = "DPoP"
	// DefaultDPoPProofMaxAge is how old a proof's iat may be,
	// better-auth's dpop.proofMaxAgeSeconds default.
	//ds:def id=oauthprovider-defaultdpopmaxage-r4n8wq2k owner=@khanakia stability=stable
	DefaultDPoPProofMaxAge = 300 * time.Second
)

DPoP (RFC 9449), as better-auth's core verifies it.

View Source
const (
	// WellKnownOpenID is the OpenID Connect Discovery document, served under
	// the issuer's path.
	WellKnownOpenID = "/.well-known/openid-configuration"
	// WellKnownOAuth is the RFC 8414 metadata document, served before the
	// issuer's path (and, for clients that append it, after it).
	WellKnownOAuth = "/.well-known/oauth-authorization-server"
)

Discovery paths and HTTP constants.

View Source
const (

	// PageConfirm and PageSuccess are the pages EndSessionResult asks the
	// transport to render.
	PageConfirm = "confirm"
	PageSuccess = "success"
)

RP-initiated logout constants, better-auth's.

View Source
const (
	EndpointAuthorize            auth.EndpointID = "oauthprovider.authorize"
	EndpointToken                auth.EndpointID = "oauthprovider.token"
	EndpointUserInfo             auth.EndpointID = "oauthprovider.userinfo"
	EndpointIntrospect           auth.EndpointID = "oauthprovider.introspect"
	EndpointRevoke               auth.EndpointID = "oauthprovider.revoke"
	EndpointRegister             auth.EndpointID = "oauthprovider.register"
	EndpointConsent              auth.EndpointID = "oauthprovider.consent"
	EndpointCreateClient         auth.EndpointID = "oauthprovider.create_client"
	EndpointListClients          auth.EndpointID = "oauthprovider.list_clients"
	EndpointGetClient            auth.EndpointID = "oauthprovider.get_client"
	EndpointUpdateClient         auth.EndpointID = "oauthprovider.update_client"
	EndpointRotateSecret         auth.EndpointID = "oauthprovider.rotate_secret"
	EndpointDeleteClient         auth.EndpointID = "oauthprovider.delete_client"
	EndpointPublicClient         auth.EndpointID = "oauthprovider.public_client"
	EndpointListConsents         auth.EndpointID = "oauthprovider.list_consents"
	EndpointRevokeConsent        auth.EndpointID = "oauthprovider.revoke_consent"
	EndpointGetConsent           auth.EndpointID = "oauthprovider.get_consent"
	EndpointUpdateConsent        auth.EndpointID = "oauthprovider.update_consent"
	EndpointEndSession           auth.EndpointID = "oauthprovider.end_session"
	EndpointEndSessionConfirm    auth.EndpointID = "oauthprovider.end_session_confirm"
	EndpointPublicClientPrelogin auth.EndpointID = "oauthprovider.public_client_prelogin"
	EndpointContinue             auth.EndpointID = "oauthprovider.continue"
)

Operation ids. The self-served ones are served by Handler; the others by every transport, under /oauthprovider.

View Source
const (
	ScopeOpenID        = "openid"
	ScopeProfile       = "profile"
	ScopeEmail         = "email"
	ScopeOfflineAccess = "offline_access"
)

Standard scopes. ScopeOpenID asks for an ID token, ScopeOfflineAccess for a refresh token; ScopeProfile and ScopeEmail release the claims OpenID Connect Core section 5.4 assigns them.

View Source
const (
	GrantAuthorizationCode = "authorization_code"
	GrantRefreshToken      = "refresh_token"
	GrantClientCredentials = "client_credentials"
)

Grant types.

View Source
const (
	AuthMethodBasic = "client_secret_basic"
	AuthMethodPost  = "client_secret_post"
	AuthMethodNone  = "none"
	// AuthMethodPrivateKeyJWT authenticates with a JWT the client signs
	// with a key from its registered JWKS (RFC 7523 section 2.2).
	AuthMethodPrivateKeyJWT = "private_key_jwt"
)

Client authentication methods at the token endpoint (RFC 7591 section 2).

View Source
const (
	// DefaultCodeTTL is how long an authorization code may wait to be
	// exchanged. RFC 6749 section 4.1.2 recommends at most ten minutes.
	//ds:def id=oauthprovider-defaultcodettl-r5m2qw8x owner=@khanakia stability=stable
	DefaultCodeTTL = 10 * time.Minute
	// DefaultAccessTokenTTL is an access token's lifetime.
	//ds:def id=oauthprovider-defaultaccessttl-h4k9zt3p owner=@khanakia stability=stable
	DefaultAccessTokenTTL = time.Hour
	// DefaultRefreshTokenTTL is a refresh token's lifetime; each rotation
	// starts a new one.
	//ds:def id=oauthprovider-defaultrefreshttl-b8x3nq6v owner=@khanakia stability=stable
	DefaultRefreshTokenTTL = 30 * 24 * time.Hour
	// DefaultAssertionMaxLifetime is the longest a private_key_jwt client
	// assertion may live (exp minus now), better-auth's assertionMaxLifetime.
	DefaultAssertionMaxLifetime = 5 * time.Minute
	// DefaultIDTokenTTL is an ID token's lifetime: 10 hours, better-auth's
	// default, as the OpenID Connect specification recommends.
	DefaultIDTokenTTL = 10 * time.Hour
)

Lifetimes. Each has an option; the defaults are better-auth's.

View Source
const (
	PrefixAccessToken  = "uboa_"
	PrefixRefreshToken = "ubor_"
	PrefixCode         = "uboc_"
	PrefixClientSecret = "ubos_"
)

Token prefixes, so a leaked token is recognisable by secret scanners and by kind: uboa_ an access token, ubor_ a refresh token, uboc_ a code, ubos_ a client secret.

View Source
const (
	SubjectPublic   = "public"
	SubjectPairwise = "pairwise"
)

Subject types (OpenID Connect Core section 8).

View Source
const (
	ActionCreate = "create"
	ActionRead   = "read"
	ActionUpdate = "update"
	ActionDelete = "delete"
	ActionList   = "list"
	ActionRotate = "rotate"
	// ActionConfigureCCScopes sets a client's client_credentials ceiling
	// through the owner operations. Without a ClientPrivileges function it
	// is always refused: the ceiling grants machine access, so the app must
	// say who may give it.
	ActionConfigureCCScopes = "configure-client-credentials-scopes"
)

Client actions ClientPrivileges is asked about, better-auth's clientPrivileges actions.

View Source
const (
	//ds:def id=oauthprovider-tableresource-p6w3kq9m owner=@khanakia stability=stable
	TableNameResource  = "auth_oauth_resource"
	TableAliasResource = "oars"
	//ds:def id=oauthprovider-tableclientresource-z2n7xv4q owner=@khanakia stability=stable
	TableNameClientResource  = "auth_oauth_client_resource"
	TableAliasClientResource = "oacr"

	ColIdentifier    = "identifier"
	ColAccessTTL     = "access_token_ttl"
	ColRefreshTTL    = "refresh_token_ttl"
	ColAllowedScopes = "allowed_scopes"
	ColCustomClaims  = "custom_claims"
	ColDPoPRequired  = "dpop_required"
	ColDisabled      = "disabled"
	ColMetadata      = "metadata"
	ColResourceID    = "resource_id"
)

The resource registry, better-auth's oauthResource and oauthClientResource: per-resource token policy, and which clients may ask for which resource.

View Source
const (
	// DefaultResourceCacheTTL is how long CacheResources keeps a resource
	// when it is given 0.
	DefaultResourceCacheTTL = time.Minute
	// MaxResourceCacheTTL caps CacheResources: a resource disabled or
	// deleted on another instance is served at most this long after.
	MaxResourceCacheTTL = 15 * time.Minute
)

Bounds of CacheResources.

View Source
const (
	//ds:def id=oauthprovider-tableclient-n4x8kq2v owner=@khanakia stability=stable
	TableNameClient  = "auth_oauth_client"
	TableAliasClient = "oac"
	//ds:def id=oauthprovider-tablecode-t6m3wp9z owner=@khanakia stability=stable
	TableNameCode  = "auth_oauth_code"
	TableAliasCode = "oaco"
	//ds:def id=oauthprovider-tabletoken-c2h7vb5r owner=@khanakia stability=stable
	TableNameToken  = "auth_oauth_token"
	TableAliasToken = "oat"
	//ds:def id=oauthprovider-tableconsent-y9q4kd3s owner=@khanakia stability=stable
	TableNameConsent  = "auth_oauth_consent"
	TableAliasConsent = "oacn"
)

The plugin's tables. No foreign keys (ADR-0002): UserTables names the user columns, so auth.DeleteUser removes a user's rows.

View Source
const (
	ColID        = "id"
	ColClientID  = "client_id"
	ColUserID    = "user_id"
	ColScopes    = "scopes"
	ColCreatedAt = "created_at"
	ColUpdatedAt = "updated_at"
	ColSessionID = "session_id"
	ColAuthTime  = "auth_time"
)

Column names shared by several tables.

View Source
const (
	// ColSecretHash is the SHA-256 of the client secret; NULL for a public
	// client (token_endpoint_auth_method none).
	ColSecretHash   = "secret_hash"
	ColName         = "name"
	ColRedirectURIs = "redirect_uris"
	ColGrantTypes   = "grant_types"
	ColAuthMethod   = "auth_method"
	ColSkipConsent  = "skip_consent"
	ColRequirePKCE  = "require_pkce"
	// ColIntrospectAny lets the client introspect tokens issued to other
	// clients: a resource server. Set only from Go (CreateClient).
	ColIntrospectAny = "introspect_any"
	ColDisabledAt    = "disabled_at"
	ColClientURI     = "client_uri"
	ColLogoURI       = "logo_uri"
	ColContacts      = "contacts"
	ColTosURI        = "tos_uri"
	ColPolicyURI     = "policy_uri"
)

auth_oauth_client columns. The id is the client_id.

View Source
const (
	ColCodeHash        = "code_hash"
	ColRedirectURI     = "redirect_uri"
	ColChallenge       = "code_challenge"
	ColChallengeMethod = "code_challenge_method"
	ColNonce           = "nonce"
	ColExpiresAt       = "expires_at"
	ColConsumedAt      = "consumed_at"
)

auth_oauth_code columns.

View Source
const (
	ColAccessHash       = "access_hash"
	ColRefreshHash      = "refresh_hash"
	ColFamilyID         = "family_id"
	ColAccessExpiresAt  = "access_expires_at"
	ColRefreshExpiresAt = "refresh_expires_at"
	ColRotatedAt        = "rotated_at"
	ColRevokedAt        = "revoked_at"
)

auth_oauth_token columns. One row is one issuance: an access token and, with offline_access, its refresh token. FamilyID ties every row descended from one code (the code's id) or one client_credentials grant, so a replayed code or a reused refresh token revokes them all.

View Source
const (
	// ColPostLogoutURIs are the client's registered RP-initiated logout
	// targets, newline-joined.
	ColPostLogoutURIs = "post_logout_redirect_uris"
	// ColEndSession lets the client end the user's session through the
	// end-session endpoint.
	ColEndSession = "end_session"
	// ColJWKS is the client's public key set (RFC 7517 JSON) for
	// private_key_jwt.
	ColJWKS = "jwks"
	// ColDiscovery names how a client row came to be: "" registered, "cimd"
	// fetched from its client_id URL.
	ColDiscovery = "discovery"
	// ColFetchedAt is when a CIMD client's document was last fetched.
	ColFetchedAt = "fetched_at"
	// ColResource is the RFC 8707 resource a code or token is bound to; ""
	// for none.
	ColResource = "resource"
	// ColSubjectType is the client's subject_type: NULL or "public", or
	// "pairwise" (migration 0003).
	ColSubjectType = "subject_type"
	// ColUserInfoClaims is the claim names an authorization request asked
	// for in its claims parameter, space-joined (migration 0004).
	ColUserInfoClaims = "userinfo_claims"
	// ColBackchannelURI and ColBackchannelSession are the client's
	// back-channel logout registration (migration 0005).
	ColBackchannelURI     = "backchannel_logout_uri"
	ColBackchannelSession = "backchannel_logout_session_required"
	// ColDPoPBound, ColDPoPJKT and ColJKT are DPoP (migration 0006): the
	// client's dpop_bound_access_tokens, a code's dpop_jkt, and the
	// thumbprint of the key a token is bound to.
	ColDPoPBound = "dpop_bound"
	ColDPoPJKT   = "dpop_jkt"
	ColJKT       = "jkt"
	// Migration 0008: the application type and the restricted fields.
	ColAppType       = "application_type"
	ColCCScopes      = "client_credentials_scopes"
	ColReferenceID   = "reference_id"
	ColSecretExpires = "client_secret_expires_at"
	// Migration 0009: a document client's freshness and validators.
	ColCIMDExpires = "cimd_expires_at"
	ColCIMDETag    = "cimd_etag"
	ColCIMDLastMod = "cimd_last_modified"
	// Migration 0010: RFC 7591 software identifiers, and a resource's
	// signing algorithm.
	ColSoftwareID        = "software_id"
	ColSoftwareVersion   = "software_version"
	ColSoftwareStatement = "software_statement"
	ColSigningAlg        = "signing_algorithm"
	// Migration 0012: the client certificate thumbprint a token is bound
	// to (RFC 8705), and the post-login reference of codes, tokens and
	// consents (reference_id, as on the client).
	ColX5T = "x5t_s256"
	// Migration 0013: the URL a private_key_jwt client publishes its key
	// set at, instead of an inline jwks.
	ColJWKSURI = "jwks_uri"
	// Migration 0014: an access token revoked on its own (its refresh token
	// kept, RevokeAccessTokenOnly), the encrypted replay of a rotation
	// (RefreshReplay), the resources a consent covers, a client-resource
	// link's metadata, a client secret kept encrypted
	// (EncryptClientSecrets), and a resource's signing key pin.
	ColAccessRevokedAt = "access_revoked_at"
	ColReplay          = "rotation_replay"
	ColReplayExpires   = "rotation_replay_expires_at"
	ColResources       = "resources"
	ColSecretEncrypted = "secret_encrypted"
	ColSigningKeyID    = "signing_key_id"
	ColPolicyVersion   = "policy_version"
	// TableNameAssertion records spent private_key_jwt assertion ids until
	// they expire, so one assertion authenticates once.
	//ds:def id=oauthprovider-tableassertion-v5k2np8w owner=@khanakia stability=stable
	TableNameAssertion  = "auth_oauth_assertion"
	TableAliasAssertion = "oaas"
)

Columns added by migration 0002.

View Source
const AccessTokenType = "at+jwt"

AccessTokenType is the JWS typ header of a JWT access token, RFC 9068's and better-auth's. A resource server verifying one offline passes it in jwt.Verifier.Types.

View Source
const ClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

ClientAssertionType is the only client_assertion_type accepted (RFC 7523 section 2.2).

View Source
const DefaultBasePath = "/oauth2"

DefaultBasePath is where Handler serves the protocol endpoints, under the issuer's path: <issuer>/oauth2/token and so on. BasePath moves it.

View Source
const (

	// DefaultCIMDRevalidate is how long a fetched document is trusted
	// before it is fetched again (better-auth's metadataRevalidationInterval).
	//ds:def id=oauthprovider-defaultcimdrevalidate-h3w8kq5n owner=@khanakia stability=stable
	DefaultCIMDRevalidate = time.Hour
)

CIMD (OAuth Client ID Metadata Document): a client_id that is an https URL names a JSON document the client publishes about itself. The provider fetches it on first use, keeps it as a client row, and fetches it again when it is older than the revalidation interval. better-auth's cimd plugin.

View Source
const (

	// LogoutTokenType is the logout token's typ header.
	LogoutTokenType = "logout+jwt"
)

OpenID Connect Back-Channel Logout 1.0, as better-auth sends it.

View Source
const MCPRefreshReuseInterval = 30 * time.Second

MCPRefreshReuseInterval is the refresh-token reuse window MCP sets, better-auth's mcp() default of 30 seconds: MCP clients often refresh from several processes at once.

View Source
const PluginName = "oauthprovider"

PluginName is the plugin's permanent identity, and the segment its management operations are served under.

View Source
const WellKnownProtectedResource = "/.well-known/oauth-protected-resource"

WellKnownProtectedResource is the RFC 9728 protected resource metadata document, served before the resource's path.

Variables

View Source
var (
	ErrAccountSelectionRequired = auth.NewError("oauthprovider.account_selection_required", http.StatusForbidden, "The user must choose an account.")
	ErrInteractionRequired      = auth.NewError("oauthprovider.interaction_required", http.StatusForbidden, "The user must complete a step before this authorization.")
)

The OpenID Connect prompt=none errors of a gate: the picker is account_selection_required, the sign-up and post-login pages interaction_required.

View Source
var (
	ErrInvalidRequest          = auth.NewError("oauthprovider.invalid_request", http.StatusBadRequest, "The request is missing a parameter, repeats one, or is otherwise malformed.")
	ErrInvalidClient           = auth.NewError("oauthprovider.invalid_client", http.StatusUnauthorized, "Client authentication failed.")
	ErrInvalidGrant            = auth.NewError("oauthprovider.invalid_grant", http.StatusBadRequest, "The grant is invalid, expired, revoked, or was issued to another client.")
	ErrUnauthorizedClient      = auth.NewError("oauthprovider.unauthorized_client", http.StatusBadRequest, "This client may not use this grant type.")
	ErrUnsupportedGrantType    = auth.NewError("oauthprovider.unsupported_grant_type", http.StatusBadRequest, "The grant type is not supported.")
	ErrUnsupportedResponseType = auth.NewError("oauthprovider.unsupported_response_type", http.StatusBadRequest, "Only response_type=code is supported.")
	ErrInvalidScope            = auth.NewError("oauthprovider.invalid_scope", http.StatusBadRequest, "A requested scope is unknown or not allowed for this client.")
	ErrAccessDenied            = auth.NewError("oauthprovider.access_denied", http.StatusForbidden, "The user or the server denied the request.")
	ErrLoginRequired           = auth.NewError("oauthprovider.login_required", http.StatusUnauthorized, "The user must sign in.")
	ErrConsentRequired         = auth.NewError("oauthprovider.consent_required", http.StatusForbidden, "The user must consent.")
	ErrInvalidToken            = auth.NewError("oauthprovider.invalid_token", http.StatusUnauthorized, "The access token is invalid, expired or revoked.")
	ErrInsufficientScope       = auth.NewError("oauthprovider.insufficient_scope", http.StatusForbidden, "The access token lacks a required scope.")
	ErrInvalidRedirectURI      = auth.NewError("oauthprovider.invalid_redirect_uri", http.StatusBadRequest, "A redirect URI is not an absolute https URL, a loopback http URL or a private-use scheme, or carries a fragment.")
	ErrInvalidClientMetadata   = auth.NewError("oauthprovider.invalid_client_metadata", http.StatusBadRequest, "The client metadata is not valid.")
	ErrRegistrationClosed      = auth.NewError("oauthprovider.registration_closed", http.StatusForbidden, "Dynamic client registration is not open.")
	ErrInvalidTarget           = auth.NewError("oauthprovider.invalid_target", http.StatusBadRequest, "The requested resource is unknown or not allowed.")
	ErrRequestNotSupported     = auth.NewError("oauthprovider.request_not_supported", http.StatusBadRequest, "Request objects are not supported.")
	ErrInvalidRequestURI       = auth.NewError("oauthprovider.invalid_request_uri", http.StatusBadRequest, "request_uri is invalid or expired.")
	ErrRequestURINotSupported  = auth.NewError("oauthprovider.request_uri_not_supported", http.StatusBadRequest, "request_uri is not supported.")
	ErrClientNotFound          = auth.NewError("oauthprovider.not_found", http.StatusNotFound, "Not found.")
	// ErrClientDisabled is an authorization request for a client an admin
	// disabled, better-auth's client_disabled. Like an unknown client
	// (invalid_client) it is shown to the user and never sent to the
	// redirect URI; errors.Is also sees ErrBadRedirectTarget.
	ErrClientDisabled = auth.NewError("oauthprovider.client_disabled", http.StatusBadRequest, "The client is disabled.")
	// ErrBadRedirectTarget is an authorize request whose client_id is
	// unknown or whose redirect_uri is not registered: the one case where the
	// error is never sent to the redirect URI, because sending it there
	// would make the provider an open redirector.
	ErrBadRedirectTarget = auth.NewError("oauthprovider.invalid_redirect_target", http.StatusBadRequest, "The client is unknown or the redirect URI is not registered for it.")
)

Errors. The part after "oauthprovider." is the RFC 6749 / 7591 / 6750 error code Handler answers, so a Go caller and an HTTP client see the same reason.

View Source
var (
	ErrNoIssuer      = errors.New("oauthprovider: Issuer is required: an absolute https URL (http only for a loopback host) with no query or fragment")
	ErrNoLoginPage   = errors.New("oauthprovider: LoginPage is required: where the authorize endpoint sends a browser that is not signed in")
	ErrBadGrantTypes = errors.New("oauthprovider: GrantTypes needs authorization_code for refresh_token, and only known grants")
	ErrBadScopes     = errors.New("oauthprovider: a scope is non-empty and contains no whitespace")
	ErrBadTTL        = errors.New("oauthprovider: every lifetime is positive")
	ErrNoJWT         = errors.New("oauthprovider: a signer is required to sign ID tokens; pass the jwt plugin (or another Signer) to New, and list a signer that is a plugin in auth.WithPlugins before this one")
	ErrBadResource   = errors.New("oauthprovider: a ProtectedResource is an absolute https URL (http only for a loopback host) with no query or fragment")
	ErrIssuerMatch   = errors.New("oauthprovider: the jwt plugin's Issuer must equal the provider's Issuer, or ID tokens would name another issuer")
	// ErrNoSecret is a feature that signs with a key from auth.WithSecret,
	// configured without one: a client with EnableEndSession (the logout
	// confirmation cookie), AllowPublicClientPrelogin (it verifies the
	// signed authorization query) and PostLogin (its marker, joined with
	// ErrBadGate). auth.New refuses it, and CreateClient and UpdateClient
	// refuse EnableEndSession, rather than the first request answering 500.
	ErrNoSecret = errors.New("oauthprovider: auth.WithSecret is required by this feature")
)

Configuration errors, returned by auth.New.

View Source
var (
	// ErrUnsupportedTokenType is a revocation of a still-valid JWT access
	// token under RefuseJWTRevocation (RFC 7009 section 2.2.1).
	ErrUnsupportedTokenType = auth.NewError("oauthprovider.unsupported_token_type", http.StatusBadRequest, "This token type cannot be revoked; it expires on its own.")
	// ErrInvalidUser is a grant whose user has been deleted since it was
	// issued, better-auth's invalid_user.
	ErrInvalidUser = auth.NewError("oauthprovider.invalid_user", http.StatusBadRequest, "The user of this grant no longer exists.")
	// ErrUnsupportedPromptSelectAccount is prompt=select_account with no
	// SelectAccount page, better-auth's unsupported_prompt_select_account;
	// SelectAccountFallsBackToLogin sends such a request to the login page
	// instead.
	ErrUnsupportedPromptSelectAccount = auth.NewError("oauthprovider.unsupported_prompt_select_account", http.StatusBadRequest, "This server cannot show an account picker.")
)

Errors better-auth answers under these names.

Typed registry columns. A resource's id is the hex SHA-256 of its identifier, because an identifier is a URL longer than an indexable key on every engine.

View Source
var (
	TableClient   = sqlb.Table{Name: TableNameClient, Alias: TableAliasClient}
	ClientIDCol   = sqlb.Col[string](TableClient, ColID)
	ClientSecret  = sqlb.Col[[]byte](TableClient, ColSecretHash)
	ClientName    = sqlb.Col[string](TableClient, ColName)
	ClientRedirs  = sqlb.Col[string](TableClient, ColRedirectURIs)
	ClientScopes  = sqlb.Col[string](TableClient, ColScopes)
	ClientGrants  = sqlb.Col[string](TableClient, ColGrantTypes)
	ClientMethod  = sqlb.Col[string](TableClient, ColAuthMethod)
	ClientSkip    = sqlb.Col[bool](TableClient, ColSkipConsent)
	ClientPKCE    = sqlb.Col[bool](TableClient, ColRequirePKCE)
	ClientIntro   = sqlb.Col[bool](TableClient, ColIntrospectAny)
	ClientOwner   = sqlb.Col[*string](TableClient, ColUserID)
	ClientOff     = sqlb.Col[*time.Time](TableClient, ColDisabledAt)
	ClientURI     = sqlb.Col[*string](TableClient, ColClientURI)
	ClientContact = sqlb.Col[*string](TableClient, ColContacts)
	ClientTos     = sqlb.Col[*string](TableClient, ColTosURI)
	ClientPolicy  = sqlb.Col[*string](TableClient, ColPolicyURI)
	ClientCreated = sqlb.Col[time.Time](TableClient, ColCreatedAt)
	ClientUpdated = sqlb.Col[time.Time](TableClient, ColUpdatedAt)

	TableCode       = sqlb.Table{Name: TableNameCode, Alias: TableAliasCode}
	CodeID          = sqlb.Col[string](TableCode, ColID)
	CodeHash        = sqlb.Col[[]byte](TableCode, ColCodeHash)
	CodeClient      = sqlb.Col[string](TableCode, ColClientID)
	CodeUser        = sqlb.Col[string](TableCode, ColUserID)
	CodeSession     = sqlb.Col[string](TableCode, ColSessionID)
	CodeRedirect    = sqlb.Col[string](TableCode, ColRedirectURI)
	CodeScopes      = sqlb.Col[string](TableCode, ColScopes)
	CodeChallenge   = sqlb.Col[*string](TableCode, ColChallenge)
	CodeMethod      = sqlb.Col[*string](TableCode, ColChallengeMethod)
	CodeNonce       = sqlb.Col[*string](TableCode, ColNonce)
	CodeAuthTime    = sqlb.Col[time.Time](TableCode, ColAuthTime)
	CodeExpires     = sqlb.Col[time.Time](TableCode, ColExpiresAt)
	CodeConsumed    = sqlb.Col[*time.Time](TableCode, ColConsumedAt)
	CodeCreated     = sqlb.Col[time.Time](TableCode, ColCreatedAt)
	TableToken      = sqlb.Table{Name: TableNameToken, Alias: TableAliasToken}
	TokenID         = sqlb.Col[string](TableToken, ColID)
	TokenAccessHash = sqlb.Col[[]byte](TableToken, ColAccessHash)
	TokenRefresh    = sqlb.Col[[]byte](TableToken, ColRefreshHash)
	TokenFamily     = sqlb.Col[string](TableToken, ColFamilyID)
	TokenClient     = sqlb.Col[string](TableToken, ColClientID)
	TokenUser       = sqlb.Col[*string](TableToken, ColUserID)
	TokenSession    = sqlb.Col[*string](TableToken, ColSessionID)
	TokenScopes     = sqlb.Col[string](TableToken, ColScopes)
	// TokenRefreshNull is the refresh hash as a nullable column, for the
	// insert of a row without a refresh token.
	TokenRefreshNull    = sqlb.Col[*[]byte](TableToken, ColRefreshHash)
	TokenAccessExpires  = sqlb.Col[time.Time](TableToken, ColAccessExpiresAt)
	TokenRefreshExpires = sqlb.Col[*time.Time](TableToken, ColRefreshExpiresAt)
	TokenAuthTime       = sqlb.Col[*time.Time](TableToken, ColAuthTime)
	TokenCreated        = sqlb.Col[time.Time](TableToken, ColCreatedAt)
	TokenRotated        = sqlb.Col[*time.Time](TableToken, ColRotatedAt)
	TokenRevoked        = sqlb.Col[*time.Time](TableToken, ColRevokedAt)

	TableConsent   = sqlb.Table{Name: TableNameConsent, Alias: TableAliasConsent}
	ConsentID      = sqlb.Col[string](TableConsent, ColID)
	ConsentUser    = sqlb.Col[string](TableConsent, ColUserID)
	ConsentClient  = sqlb.Col[string](TableConsent, ColClientID)
	ConsentScopes  = sqlb.Col[string](TableConsent, ColScopes)
	ConsentCreated = sqlb.Col[time.Time](TableConsent, ColCreatedAt)
	ConsentUpdated = sqlb.Col[time.Time](TableConsent, ColUpdatedAt)
)

Typed tables and columns.

View Source
var (
	ClientPostLogout        = sqlb.Col[*string](TableClient, ColPostLogoutURIs)
	ClientEndSession        = sqlb.Col[bool](TableClient, ColEndSession)
	ClientJWKS              = sqlb.Col[*string](TableClient, ColJWKS)
	ClientDiscovery         = sqlb.Col[*string](TableClient, ColDiscovery)
	ClientFetchedAt         = sqlb.Col[*time.Time](TableClient, ColFetchedAt)
	CodeResource            = sqlb.Col[*string](TableCode, ColResource)
	ClientSubject           = sqlb.Col[*string](TableClient, ColSubjectType)
	CodeClaims              = sqlb.Col[*string](TableCode, ColUserInfoClaims)
	TokenClaims             = sqlb.Col[*string](TableToken, ColUserInfoClaims)
	ConsentClaims           = sqlb.Col[*string](TableConsent, ColUserInfoClaims)
	ClientBackURI           = sqlb.Col[*string](TableClient, ColBackchannelURI)
	ClientBackSess          = sqlb.Col[bool](TableClient, ColBackchannelSession)
	ClientDPoP              = sqlb.Col[bool](TableClient, ColDPoPBound)
	CodeJKT                 = sqlb.Col[*string](TableCode, ColDPoPJKT)
	TokenJKT                = sqlb.Col[*string](TableToken, ColJKT)
	ClientAppType           = sqlb.Col[*string](TableClient, ColAppType)
	ClientCCScopes          = sqlb.Col[*string](TableClient, ColCCScopes)
	ClientRefID             = sqlb.Col[*string](TableClient, ColReferenceID)
	ClientMeta              = sqlb.Col[*string](TableClient, ColMetadata)
	ClientSecretExp         = sqlb.Col[*time.Time](TableClient, ColSecretExpires)
	ClientCIMDExpires       = sqlb.Col[*time.Time](TableClient, ColCIMDExpires)
	ClientCIMDETag          = sqlb.Col[*string](TableClient, ColCIMDETag)
	ClientCIMDLastMod       = sqlb.Col[*string](TableClient, ColCIMDLastMod)
	ClientSoftwareID        = sqlb.Col[*string](TableClient, ColSoftwareID)
	ClientSoftwareVersion   = sqlb.Col[*string](TableClient, ColSoftwareVersion)
	ClientSoftwareStatement = sqlb.Col[*string](TableClient, ColSoftwareStatement)
	TokenResource           = sqlb.Col[*string](TableToken, ColResource)
	// The binding and reference columns of migration 0012.
	TokenX5T = sqlb.Col[*string](TableToken, ColX5T)
	// ClientJWKSURI is migration 0013's jwks_uri.
	ClientJWKSURI = sqlb.Col[*string](TableClient, ColJWKSURI)
	// The columns of migration 0014.
	TokenAccessRevoked = sqlb.Col[*time.Time](TableToken, ColAccessRevokedAt)
	TokenReplay        = sqlb.Col[*string](TableToken, ColReplay)
	TokenReplayExpires = sqlb.Col[*time.Time](TableToken, ColReplayExpires)
	ConsentResources   = sqlb.Col[*string](TableConsent, ColResources)
	ClientSecretEnc    = sqlb.Col[*string](TableClient, ColSecretEncrypted)
	ResourceKeyID      = sqlb.Col[*string](TableResource, ColSigningKeyID)
	ResourceVersion    = sqlb.Col[int64](TableResource, ColPolicyVersion)
	LinkMeta           = sqlb.Col[*string](TableClientResource, ColMetadata)
	TokenRef           = sqlb.Col[*string](TableToken, ColReferenceID)
	CodeRef            = sqlb.Col[*string](TableCode, ColReferenceID)
	ConsentRef         = sqlb.Col[*string](TableConsent, ColReferenceID)

	TableAssertion   = sqlb.Table{Name: TableNameAssertion, Alias: TableAliasAssertion}
	AssertionID      = sqlb.Col[string](TableAssertion, ColID)
	AssertionExpires = sqlb.Col[time.Time](TableAssertion, ColExpiresAt)
)

AuthMethodValues is every client authentication method a client may register with.

DPoPAlgorithms are the proof algorithms accepted unless DPoPSigningAlgorithms replaces them: better-auth's default list. A proof's key usually carries no alg, so its type decides the family and the header's alg is checked against it (jwt.PublicKeyForAlg).

View Source
var DefaultCIMDOriginBoundFields = []string{CIMDFieldPostLogoutURIs, CIMDFieldClientURI}

DefaultCIMDOriginBoundFields are better-auth's originBoundFields default: redirect URIs are left out because exact matching at authorize already binds them, and native clients redirect to another origin than their document's.

DefaultGrantTypes is every grant the provider serves unless GrantTypes narrows it.

View Source
var DefaultRateLimits = map[auth.EndpointID]int{
	EndpointToken:      20,
	EndpointAuthorize:  30,
	EndpointIntrospect: 100,
	EndpointRevoke:     30,
	EndpointRegister:   5,
	EndpointUserInfo:   60,
}

DefaultRateLimits are better-auth's per-endpoint limits, per client IP per minute: token 20, authorize 30, introspect 100, revoke 30, register 5, userinfo 60. RateLimit replaces one; NoDefaultRateLimits drops them all so only the app's own rules apply.

DefaultScopes is what the provider supports unless Scopes replaces it.

View Source
var ErrBadCIMDField = errors.New("oauthprovider: CIMDOriginBoundFields names a field that is not a URL member of a client metadata document")

ErrBadCIMDField is a CIMDOriginBoundFields name that is not one of the CIMDField constants; auth.New fails with it.

View Source
var ErrBadDPoPAlgorithms = errors.New("oauthprovider: DPoPSigningAlgorithms needs at least one algorithm from jwt.AlgValues")

ErrBadDPoPAlgorithms is a DPoPSigningAlgorithms list that is empty or names an algorithm the jwt plugin cannot verify: a configuration error, so auth.New fails.

View Source
var ErrBadEncryption = errors.New("oauthprovider: a SecretEncryption needs Encrypt and Decrypt, and Decrypt(Encrypt(x)) == x")

ErrBadEncryption is a SecretEncryption without both functions, or whose Decrypt does not give back what Encrypt sealed; auth.New fails with it.

View Source
var ErrBadExtension = errors.New("oauthprovider: invalid extension")

ErrBadExtension is an Extension Validate refuses.

View Source
var ErrBadExtensionGrant = errors.New("oauthprovider: an extension grant needs an absolute-URI type that is not built in, registered once, with a handler")

ErrBadExtensionGrant is a WithGrant whose type is not an absolute URI, is a built-in grant, is registered twice, or has no handler.

View Source
var ErrBadGate = errors.New("oauthprovider: a gate needs its page (trusted) and its decision, and PostLogin needs auth.WithSecret")

ErrBadGate is a gate without its page or decision, or a post-login gate without auth.WithSecret.

View Source
var ErrBadPairwiseSecret = errors.New("oauthprovider: PairwiseSecret needs at least 32 characters")

ErrBadPairwiseSecret is a PairwiseSecret shorter than 32 characters.

View Source
var ErrBadResourceCacheTTL = fmt.Errorf("oauthprovider: the CacheResources TTL is between 0 (the default, %s) and %s", DefaultResourceCacheTTL, MaxResourceCacheTTL)

ErrBadResourceCacheTTL is a configuration error: a CacheResources TTL that is negative or above MaxResourceCacheTTL.

View Source
var ErrBadSecretHasher = errors.New("oauthprovider: HashStoredSecrets must return the same 32 bytes for the same input")

ErrBadSecretHasher is a HashStoredSecrets function that is not deterministic or does not return 32 bytes; auth.New fails with it.

View Source
var ErrBadSeedMode = errors.New("oauthprovider: SeedResources mode is insertOnly, merge or overwrite")

ErrBadSeedMode is a SeedResources mode that is not a SeedMode constant.

View Source
var ErrBadTrustedClient = errors.New("oauthprovider: invalid trusted client")

ErrBadTrustedClient is a TrustedClient Init refuses.

View Source
var ErrCIMDBusy = auth.NewError("oauthprovider.temporarily_unavailable", http.StatusTooManyRequests, "Client metadata cannot be fetched now. Try again shortly.")

ErrCIMDBusy is a metadata fetch the governor refused: too many in flight, over a per-minute budget, or a retry too soon after a failure. Excess fetches are refused, never queued.

View Source
var ErrCIMDFetch = errors.New("oauthprovider: client metadata document refused")

ErrCIMDFetch is a metadata document that could not be fetched or is not acceptable; every such failure is answered invalid_client.

View Source
var ErrForbidden = auth.NewError("oauthprovider.forbidden", http.StatusForbidden, "You may not do this with this client.")

ErrForbidden is an owner operation ClientPrivileges refused.

View Source
var ErrInvalidDPoPProof = auth.NewError("oauthprovider.invalid_dpop_proof", http.StatusBadRequest, "The DPoP proof is missing or invalid.")

ErrInvalidDPoPProof is a missing, malformed, mismatched or replayed DPoP proof.

View Source
var ErrInvalidSignature = auth.NewError("oauthprovider.invalid_signature", http.StatusUnauthorized, "The authorization request was altered or has expired.")

ErrInvalidSignature is an oauth_query whose signature, set of signed names or expiry does not verify.

View Source
var ErrNoGrantType = errors.New("oauthprovider: IssueTokens needs a GrantType")

ErrNoGrantType is an IssueTokens call without a grant type: better-auth's issueTokens refuses one, because the grant is what TokenResponseFields and the audit trail see.

View Source
var ErrResourceExists = auth.NewError("oauthprovider.resource_exists", 409, "This resource is already registered.")

ErrResourceExists is a CreateResource for an identifier already registered.

View Source
var ErrSecretNotRecoverable = auth.NewError("oauthprovider.secret_not_recoverable", http.StatusNotFound, "This client's secret cannot be read back.")

ErrSecretNotRecoverable is RevealClientSecret for a client whose secret was stored only as a hash: without EncryptClientSecrets, or created before it was turned on, or a client with no secret.

View Source
var ErrShortGenerated = errors.New("oauthprovider: a generated client id or secret is at least 22 characters, and a client id is never an https URL")

ErrShortGenerated is a GenerateClientID or GenerateClientSecret function that returned fewer than 22 characters, or a client id shaped like a metadata document URL: a programming error, so registration fails.

Functions

func CertificateThumbprint

func CertificateThumbprint(cert *x509.Certificate) string

CertificateThumbprint is RFC 8705's x5t#S256 of a certificate: base64url(SHA-256(DER)), unpadded. A mutual-TLS ClientAuthMethod returns it in its Confirmation.

func IsCIMDClientID

func IsCIMDClientID(id string) bool

IsCIMDClientID reports whether a client_id is shaped like a client metadata document URL, better-auth's isCimdClientIdUrlCandidate: an https URL. Generated client ids never are.

func ValidCIMDClientID

func ValidCIMDClientID(raw string) bool

ValidCIMDClientID applies CIMD section 3's client_id URL rules (https, a host, a path other than "/", no dot segments, fragment or user information), better-auth's validateClientIdUrl.

Types

type AccessToken

type AccessToken struct {
	ID        string    `json:"id"`
	ClientID  string    `json:"client_id"`
	UserID    string    `json:"user_id,omitempty"`
	SessionID string    `json:"session_id,omitempty"`
	Scopes    []string  `json:"scopes"`
	IssuedAt  time.Time `json:"issued_at"`
	ExpiresAt time.Time `json:"expires_at"`
	// Resource is the RFC 8707 resource the token is bound to (its aud), or
	// "" for a token for no particular resource.
	Resource string `json:"resource,omitempty"`

	// JKT is the thumbprint of the DPoP key the token is bound to, or "".
	JKT string `json:"jkt,omitempty"`
	// X5TS256 is the thumbprint of the client certificate the token is
	// bound to (RFC 8705), or "".
	X5TS256 string `json:"x5t#S256,omitempty"`
	// ReferenceID is the post-login reference (an organization) the
	// authorization was given for, or "".
	ReferenceID string `json:"reference_id,omitempty"`
	// contains filtered or unexported fields
}

AccessToken is a verified access token, for a resource server.

func TokenFrom

func TokenFrom(ctx context.Context) (AccessToken, bool)

TokenFrom returns the access token RequireToken verified for this request.

func (AccessToken) HasResource

func (t AccessToken) HasResource(resource string) bool

HasResource reports whether the token was issued for resource, one of the resources it names.

func (AccessToken) HasScope

func (t AccessToken) HasScope(scope string) bool

HasScope reports whether the token carries scope.

type AccessTokenClaimsFunc

type AccessTokenClaimsFunc func(ctx context.Context, info AccessTokenClaimsInfo) map[string]any

AccessTokenClaimsFunc adds claims to a JWT access token, better-auth's customAccessTokenClaims. A registered or provider claim it returns is ignored.

type AccessTokenClaimsInfo

type AccessTokenClaimsInfo struct {
	// User is the token's user; nil for a client_credentials token.
	User     *auth.User
	ClientID string
	Scopes   []string
	Resource string
	// ReferenceID is the post-login reference (PostLogin's
	// ConsentReferenceID), or "".
	ReferenceID string
}

AccessTokenClaimsInfo is what AccessTokenClaims receives.

type AuthorizeRequest

type AuthorizeRequest struct {
	ResponseType        string `json:"response_type"`
	ClientID            string `json:"client_id"`
	RedirectURI         string `json:"redirect_uri"`
	Scope               string `json:"scope"`
	State               string `json:"state"`
	CodeChallenge       string `json:"code_challenge"`
	CodeChallengeMethod string `json:"code_challenge_method"`
	Nonce               string `json:"nonce"`
	Prompt              string `json:"prompt"`
	ResponseMode        string `json:"response_mode"`
	// Resource is the RFC 8707 resource indicator: a ProtectedResource the
	// tokens are for. Tokens for a resource are JWTs whose aud is it.
	// Several resources (the parameter repeated over HTTP) are written
	// space-separated; their policies combine (resourcePolicy).
	Resource string `json:"resource"`
	// MaxAge is the OpenID Connect max_age in seconds: a session older than
	// this signs in again.
	MaxAge string `json:"max_age"`
	// LoginHint is passed to the login page untouched.
	LoginHint string `json:"login_hint"`
	// Display and UILocales (OpenID Connect Core section 3.1.2.1) are
	// passed to the login, consent and other pages untouched, for them to
	// choose a layout and a language.
	Display   string `json:"display"`
	UILocales string `json:"ui_locales"`
	// IDTokenHint is an ID token this provider issued to the client earlier
	// (expired is fine). When the signed-in user is not its subject, the
	// browser goes to the login page, or prompt=none fails with
	// login_required; one this provider did not sign for this client is
	// invalid_request.
	IDTokenHint string `json:"id_token_hint"`
	// Request and RequestURI are refused (request_not_supported,
	// request_uri_not_supported): this provider takes plain parameters.
	Request    string `json:"request"`
	RequestURI string `json:"request_uri"`
	// Claims is the OpenID Connect claims request (a JSON object); it needs
	// openid. Its userinfo names are released at userinfo whatever the
	// scopes, once consented.
	Claims string `json:"claims"`
	// DPoPJKT binds the code to a DPoP key thumbprint (RFC 9449 section
	// 10): the token request must carry a proof by that key.
	DPoPJKT string `json:"dpop_jkt"`
}

AuthorizeRequest is an authorization request, RFC 6749 section 4.1.1 with PKCE (RFC 7636) and the OpenID Connect parameters. Handler fills it from the query string; the json tags are the parameter names.

type AuthorizeResult

type AuthorizeResult struct {
	RedirectTo string `json:"redirect_to"`
}

AuthorizeResult is where to send the browser: the client's redirect URI with a code, the login page, or the consent page.

type CIMDFetchPolicy

type CIMDFetchPolicy struct {
	// MinimumFetchInterval paces retries of a URL whose last fetch failed.
	MinimumFetchInterval time.Duration
	// MaxConcurrent and MaxConcurrentPerOrigin bound fetches in flight.
	MaxConcurrent          int
	MaxConcurrentPerOrigin int
	// MaxPerMinute and MaxPerOriginPerMinute bound fetches in a rolling
	// minute. A fresh document, or a request joining a fetch already in
	// flight for its URL, spends none.
	MaxPerMinute          int
	MaxPerOriginPerMinute int
}

CIMDFetchPolicy bounds metadata fetches on one instance, better-auth's metadataFetchPolicy. Zero fields take the defaults above.

type CIMDFetcher

type CIMDFetcher func(ctx context.Context, rawURL string) ([]byte, error)

CIMDFetcher fetches a metadata document's bytes. The default refuses every address that is not public (RFC 6890 special-purpose ranges), dials the address it checked rather than resolving twice, follows no redirect, gives up after 5 seconds, and reads at most 5 KB of an exactly 200 application/json answer. Replace it only for tests, or for an egress proxy that enforces the same rules.

type CIMDOption

type CIMDOption func(*cimdConfig)

CIMDOption configures ClientIDMetadataDocuments.

func CIMDAllow

func CIMDAllow(fn func(clientID string) bool) CIMDOption

CIMDAllow is a policy gate run before any fetch: false refuses the client_id (invalid_client), better-auth's isMetadataDocumentUrlAllowed.

func CIMDFetch

func CIMDFetch(f CIMDFetcher) CIMDOption

CIMDFetch replaces the fetcher; see CIMDFetcher.

func CIMDMaxCacheEntries

func CIMDMaxCacheEntries(n int) CIMDOption

CIMDMaxCacheEntries overrides DefaultCIMDMaxCacheEntries.

func CIMDOnClientCreated

func CIMDOnClientCreated(fn func(ctx context.Context, c Client, document json.RawMessage)) CIMDOption

CIMDOnClientCreated runs after a document client is first stored, better-auth's onClientCreated; best effort: a panic is recovered and logged.

func CIMDOnClientRefreshed

func CIMDOnClientRefreshed(fn func(ctx context.Context, c, previous Client, document json.RawMessage)) CIMDOption

CIMDOnClientRefreshed runs after a document client is refreshed from a new document, with the client before, better-auth's onClientRefreshed; best effort.

func CIMDOriginBoundFields

func CIMDOriginBoundFields(fields ...string) CIMDOption

CIMDOriginBoundFields replaces DefaultCIMDOriginBoundFields, better-auth's originBoundFields: every URL in these members must be on the client_id's origin (a loopback host and a private-use scheme are allowed in the two redirect fields). No fields turns origin binding off, which lets a document claim another site's pages; the jwks_uri is bound whatever this says.

func CIMDPolicy

func CIMDPolicy(pol CIMDFetchPolicy) CIMDOption

CIMDPolicy sets the fetch governor.

func CIMDProfileMCP

func CIMDProfileMCP() CIMDOption

CIMDProfileMCP requires client_name and redirect_uris, as the MCP specification's pinned CIMD draft does (better-auth's metadataProfile "mcp-2026-07-28").

func CIMDRevalidate

func CIMDRevalidate(d time.Duration) CIMDOption

CIMDRevalidate overrides DefaultCIMDRevalidate.

func CIMDTransport

func CIMDTransport(fn func(ctx context.Context, rawURL string, conditional http.Header) (CIMDResponse, error)) CIMDOption

CIMDTransport replaces the fetcher with one that sees the conditional headers (If-None-Match, If-Modified-Since) and answers the response, for revalidation and HTTP freshness; CIMDFetch's byte fetcher has neither.

type CIMDResponse

type CIMDResponse struct {
	Status int
	Body   []byte
	Header http.Header
}

CIMDResponse is a fetched metadata document: its status (200, or 304 for a conditional request), body and headers.

type ClaimsFunc

type ClaimsFunc func(ctx context.Context, u auth.User, scopes []string) map[string]any

ClaimsFunc adds claims to the ID token and the userinfo answer for a user and the scopes they granted, better-auth's customIdTokenClaims and customUserInfoClaims. A registered or standard claim it returns overrides the plugin's, except sub, iss, aud, exp, iat and nonce, which are always the plugin's.

type ClaimsInfo

type ClaimsInfo struct {
	User     auth.User
	ClientID string
	Scopes   []string
}

ClaimsInfo is what an extension's ID token and userinfo claims are decided from.

type Client

type Client struct {
	RegisteredMetadata
	ClientID      string `json:"client_id"`
	Public        bool   `json:"public"`
	SkipConsent   bool   `json:"skip_consent"`
	RequirePKCE   bool   `json:"require_pkce"`
	IntrospectAny bool   `json:"introspect_any"`
	// ClientCredentialsScopes is the client_credentials ceiling.
	ClientCredentialsScopes []string `json:"client_credentials_scopes,omitempty"`
	// SecretExpiresAt is when the secret stops; nil never.
	SecretExpiresAt *time.Time      `json:"client_secret_expires_at,omitempty"`
	ReferenceID     string          `json:"reference_id,omitempty"`
	Metadata        json.RawMessage `json:"metadata,omitempty"`
	EndSession      bool            `json:"enable_end_session"`
	Disabled        bool            `json:"disabled"`
	OwnerID         string          `json:"owner_id,omitempty"`
	CreatedAt       time.Time       `json:"created_at"`
	UpdatedAt       time.Time       `json:"updated_at"`
}

Client is a registered client as its owner sees it, without the secret.

type ClientAuthMethod

type ClientAuthMethod struct {
	// Method is the token_endpoint_auth_method name clients register; it
	// may not be a built-in one.
	Method string
	// AssertionTypes are the client_assertion_type URIs (absolute) whose
	// assertion this method consumes; RFC 7523's jwt-bearer stays the
	// provider's. None means the method takes the client_id alone, plus
	// what Authenticate reads from the credentials (a TLS certificate).
	AssertionTypes []string
	// Authenticate decides whether the credentials prove the client. A
	// Confirmation binds the tokens issued to it (cnf); nil binds nothing.
	// Any error is invalid_client.
	Authenticate func(ctx context.Context, client Client, cred ClientCredentials) (*Confirmation, error)
}

ClientAuthMethod is a token endpoint authentication method an extension serves, better-auth's clientAuthentication strategies: mutual TLS (RFC 8705), an attestation, a client assertion of another type.

type ClientCredentials

type ClientCredentials struct {
	// ID and Secret are the client_id and client_secret.
	ID     string `json:"client_id"`
	Secret string `json:"client_secret"`
	// Method is how they arrived: AuthMethodBasic for the Authorization
	// header, AuthMethodPost for the form, AuthMethodNone for a client_id
	// alone. Handler sets it; it must match the client's registered method.
	Method string `json:"client_auth_method"`
	// Assertion is a private_key_jwt client assertion (Method
	// AuthMethodPrivateKeyJWT), or an extension method's.
	Assertion string `json:"client_assertion"`
	// AssertionType is the client_assertion_type of an extension method's
	// assertion; "" for private_key_jwt.
	AssertionType string `json:"client_assertion_type,omitempty"`
	// Certificates are the TLS client certificates of the connection, for
	// a mutual-TLS ClientAuthMethod. Handler fills them.
	Certificates []*x509.Certificate `json:"-"`
}

ClientCredentials is how a request authenticated its client at the token, revocation and introspection endpoints. Handler fills it from the Authorization header or the form; a Go caller fills it directly.

type ClientDiscoverer

type ClientDiscoverer struct {
	// ID is the discovery kind stored on the client; unique, not "cimd".
	ID string
	// Matches reports whether a client_id is this discovery's.
	Matches func(clientID string) bool
	// Resolve answers the client's metadata, checked as a registration is.
	// An error leaves the client unknown (and is logged).
	Resolve func(ctx context.Context, clientID string) (ClientMetadata, error)
	// Refresh is how long a resolved client is used before it is resolved
	// again; 0 resolves it once.
	Refresh time.Duration
}

ClientDiscoverer resolves a client_id that is not registered, better-auth's clientDiscovery (CIMD is one): the client is stored the first time, with its discovery kind, and resolved again after Refresh.

type ClientIDRequest

type ClientIDRequest struct {
	ClientID string `json:"client_id"`
}

ClientIDRequest names one client.

type ClientList

type ClientList struct {
	Clients []Client `json:"clients"`
}

ClientList is the owner's clients.

type ClientMetadata

type ClientMetadata struct {
	Name         string   `json:"client_name,omitempty"`
	RedirectURIs []string `json:"redirect_uris"`
	// GrantTypes defaults to authorization_code (and refresh_token when
	// the provider serves it).
	GrantTypes []string `json:"grant_types,omitempty"`
	// ResponseTypes may only be ["code"]; it is accepted for RFC 7591
	// clients that send it.
	ResponseTypes []string `json:"response_types,omitempty"`
	// AuthMethod defaults to client_secret_basic; none makes a public
	// client, which has no secret and must use PKCE.
	AuthMethod string `json:"token_endpoint_auth_method,omitempty"`
	// Scope is the space-separated scopes the client may request; empty is
	// every scope the provider supports.
	Scope     string   `json:"scope,omitempty"`
	ClientURI string   `json:"client_uri,omitempty"`
	LogoURI   string   `json:"logo_uri,omitempty"`
	TosURI    string   `json:"tos_uri,omitempty"`
	PolicyURI string   `json:"policy_uri,omitempty"`
	Contacts  []string `json:"contacts,omitempty"`
	// PostLogoutRedirectURIs are where the end-session endpoint may send
	// the browser after signing the user out; exact match, as redirects.
	PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris,omitempty"`
	// JWKS is the client's public key set (RFC 7517 {"keys": [...]}), for
	// private_key_jwt, which requires it. Public asymmetric keys only.
	JWKS json.RawMessage `json:"jwks,omitempty"`
	// JWKSURI is where the client publishes its key set instead of JWKS
	// (one or the other, never both): an https URL on an origin the app
	// trusts, or on the client_id's origin for a metadata document client.
	// The set is fetched on first use and cached (see JWKSFetcher).
	JWKSURI string `json:"jwks_uri,omitempty"`

	// SubjectType is "public" (the default) or "pairwise", which needs the
	// provider's PairwiseSecret and redirect URIs on one host.
	SubjectType string `json:"subject_type,omitempty"`
	// BackchannelLogoutURI receives a logout token when a session the
	// client holds tokens for ends (OpenID Connect Back-Channel Logout):
	// https, or http on a loopback host, no fragment.
	BackchannelLogoutURI string `json:"backchannel_logout_uri,omitempty"`
	// BackchannelLogoutSessionRequired says the client needs sid in the
	// logout token; this provider always sends it.
	BackchannelLogoutSessionRequired bool `json:"backchannel_logout_session_required,omitempty"`
	// DPoPBoundAccessTokens makes every token request carry a DPoP proof,
	// and every token the client gets bound to its key (RFC 9449 section 5).
	DPoPBoundAccessTokens bool `json:"dpop_bound_access_tokens,omitempty"`
	// Resources are the resources to link the new client to beyond
	// RegistrationDefaultResources; each must be a default or one of
	// RegistrationAllowedResources (invalid_target otherwise).
	Resources []string `json:"resources,omitempty"`
	// ApplicationType is "web" (the default) or "native", better-auth's
	// redirect rules: a web client uses https on non-loopback hosts only; a
	// native one https on non-loopback hosts, http on exactly localhost,
	// 127.0.0.1 or [::1], or a reverse-domain private-use scheme.
	ApplicationType string `json:"application_type,omitempty"`
	// SoftwareID, SoftwareVersion and SoftwareStatement are RFC 7591's
	// software identifiers, stored as given.
	SoftwareID        string `json:"software_id,omitempty"`
	SoftwareVersion   string `json:"software_version,omitempty"`
	SoftwareStatement string `json:"software_statement,omitempty"`
	// CredentialsScopes asks for a client_credentials ceiling when an owner
	// creates a client through the operations, better-auth's
	// client_credentials_scopes: allowed only when ClientPrivileges exists
	// and grants ActionConfigureCCScopes, refused (forbidden) for dynamic
	// registration without a session, and never taken from a metadata
	// document. Go callers set ClientSpec.ClientCredentialsScopes instead.
	CredentialsScopes []string `json:"client_credentials_scopes,omitempty"`
	// InitialAccessToken is the RFC 7591 Bearer token Handler read from the
	// registration request; never stored or returned.
	InitialAccessToken string `json:"-"`
	// Extra are the registration's members this provider does not define
	// (RFC 7591 extensions). Dynamic registration keeps them as the
	// client's Metadata, better-auth's metadata, without the names the
	// provider owns (the client's own fields, public, type, metadata).
	// Handler fills it.
	Extra map[string]any `json:"-"`
	// contains filtered or unexported fields
}

ClientMetadata is a client's registration, RFC 7591 section 2: what dynamic registration and create-client accept, and what a client's owner may change. Restricted fields (skip consent, PKCE relaxation, introspection of other clients' tokens) are not here: only ClientSpec, from Go, sets them.

It is an input only: the fields the provider defaults (client_name, grant_types, response_types, token_endpoint_auth_method, scope) are omitempty, so a generated client may leave them out. The answer carries RegisteredMetadata instead, where they are always present, because RFC 7591 section 3.2.1 returns every registered value. The two types have the same fields, so RegisteredMetadata(m) converts one to the other, and a field added to one and not the other stops that conversion compiling.

type ClientPrivilegesFunc

type ClientPrivilegesFunc func(ctx context.Context, action string, c *auth.Call, client *Client) bool

ClientPrivilegesFunc decides whether the signed-in user may perform an action on a client (nil for create and list), better-auth's clientPrivileges; false is ErrForbidden.

type ClientResourceLink struct {
	ClientID  string          `json:"client_id"`
	Resource  string          `json:"resource"`
	Metadata  json.RawMessage `json:"metadata,omitempty"`
	CreatedAt time.Time       `json:"created_at"`
}

ClientResourceLink is one client-resource link, better-auth's oauthClientResource row: no foreign keys (ADR-0002); deleting the client or the resource deletes its links in code.

type ClientSpec

type ClientSpec struct {
	ClientMetadata
	// SkipConsent marks a first-party client: the user is never asked.
	SkipConsent bool
	// OptionalPKCE lets a confidential client omit PKCE. A public client
	// always needs it, whatever this says.
	OptionalPKCE bool
	// IntrospectAny lets the client introspect tokens issued to any client:
	// a resource server.
	IntrospectAny bool
	// OwnerID is the user who manages the client through the operations;
	// "" is a client only Go code manages.
	OwnerID string
	// EnableEndSession lets the client sign the user out through the
	// end-session endpoint (RP-initiated logout). It needs auth.WithSecret,
	// which signs the logout confirmation; without one it is ErrNoSecret.
	EnableEndSession bool
	// ClientCredentialsScopes are the client_credentials ceiling,
	// better-auth's clientCredentialsScopes: without them the grant is
	// unauthorized_client, and an omitted scope takes them all. User scopes
	// are never allowed.
	ClientCredentialsScopes []string
	// SecretExpiresAt ends the client secret; nil never does.
	SecretExpiresAt *time.Time
	// ReferenceID names an owner other than a user (an organization, a
	// team), better-auth's referenceId; Metadata is the app's JSON.
	ReferenceID string
	Metadata    json.RawMessage
	// contains filtered or unexported fields
}

ClientSpec is ClientMetadata plus the restricted fields only the app's Go code may set, better-auth's admin create-client.

type Confirmation

type Confirmation struct {
	JKT     string `json:"jkt,omitempty"`
	X5TS256 string `json:"x5t#S256,omitempty"`
}

Confirmation is RFC 7800's cnf: a DPoP key thumbprint (RFC 9449) or a client certificate thumbprint (RFC 8705, which an extension ClientAuthMethod binds; the token type stays Bearer).

type Consent struct {
	ID         string   `json:"id"`
	ClientID   string   `json:"client_id"`
	ClientName string   `json:"client_name"`
	Scopes     []string `json:"scopes"`
	// Resources are the RFC 8707 resources the consent covers, better-auth's
	// consent resources: a request for another one asks again.
	Resources []string  `json:"resources,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Consent is one client a user has consented to, with the scopes.

type ConsentIDRequest

type ConsentIDRequest struct {
	ID string `json:"id"`
}

ConsentIDRequest names one of the signed-in user's consents.

type ConsentList

type ConsentList struct {
	Consents []Consent `json:"consents"`
}

ConsentList is the signed-in user's consents.

type ConsentRequest

type ConsentRequest struct {
	Query  string `json:"oauth_query"`
	Accept bool   `json:"accept"`
	// Scope, when set, grants only these of the requested scopes.
	Scope string `json:"scope,omitempty"`
	// Claims, when set, is an OpenID Connect claims request object
	// ({"userinfo": {...}}) granting only these of the requested userinfo
	// claims, better-auth's consent claims; a claim the request did not ask
	// for is invalid_request.
	Claims json.RawMessage `json:"claims,omitempty"`
}

ConsentRequest is the consent page's answer. Query is the authorize request's query string, which the page received; it is validated again in full, so nothing the page sends is trusted.

type ContinueRequest

type ContinueRequest struct {
	Query     string `json:"oauth_query"`
	Selected  bool   `json:"selected,omitempty"`
	Created   bool   `json:"created,omitempty"`
	PostLogin bool   `json:"post_login,omitempty"`
}

ContinueRequest resumes an authorization from a gate's page, better-auth's oauth2Continue: Query is the signed query the page received; Selected, Created and PostLogin say which gate was passed.

type EndSessionConfirmRequest

type EndSessionConfirmRequest struct {
	Action  string `json:"action"`
	Browser bool   `json:"-"`
}

EndSessionConfirmRequest is the confirmation form's post.

type EndSessionRequest

type EndSessionRequest struct {
	// IDTokenHint is an ID token this provider issued. With a valid one
	// naming the browser's user, the session ends at once; without one,
	// or with one for someone else, a browser is asked to confirm first.
	IDTokenHint string `json:"id_token_hint"`
	// ClientID names the client when there is no hint; with one, it must be
	// an audience of the hint.
	ClientID string `json:"client_id"`
	// PostLogoutRedirectURI is used only when the client registered it
	// exactly; otherwise the success page says it was not registered.
	PostLogoutRedirectURI string `json:"post_logout_redirect_uri"`
	State                 string `json:"state"`
	// Browser is true for a top-level browser navigation that accepts HTML,
	// which Handler decides from the request; only such a request may be
	// shown the confirmation page.
	Browser bool `json:"-"`
}

EndSessionRequest is an OpenID Connect RP-Initiated Logout request.

type EndSessionResult

type EndSessionResult struct {
	RedirectTo string `json:"redirect_to,omitempty"`
	// Page is PageConfirm, PageSuccess, or "" for a non-browser caller that
	// needs no page.
	Page string `json:"page,omitempty"`
	// Notice is shown on the success page.
	Notice string `json:"notice,omitempty"`
}

EndSessionResult is where to send the browser, or which page to show.

type Extension

type Extension struct {
	// ID names the extension in errors and logs; unique and not empty.
	ID string
	// Grants are extension grants, as WithGrant registers them.
	Grants []ExtensionGrant
	// Metadata adds members to the discovery document; a member the
	// provider sets (or an earlier extension added) is kept.
	Metadata map[string]any
	// Claims adds claims to the tokens and userinfo.
	Claims ExtensionClaims
	// ClientAuthentication adds token endpoint authentication methods.
	ClientAuthentication []ClientAuthMethod
	// ClientDiscovery resolves client_ids the provider has not stored.
	ClientDiscovery []ClientDiscoverer
}

Extension is one module's additions to the provider, better-auth's OAuthProviderExtension: extension grants, discovery fields, claims, client authentication methods and client discovery. Extend installs a list of them (better-auth's extensions). Everything an extension adds is additive: it never replaces what the provider or the app set, and between two extensions the first wins.

type ExtensionClaims

type ExtensionClaims struct {
	// AccessToken adds claims to JWT access tokens; the per-issuance
	// GrantDecision.AccessTokenClaims, AccessTokenClaims and a resource's
	// CustomClaims override them, in that order.
	AccessToken func(ctx context.Context, info AccessTokenClaimsInfo) map[string]any
	// IDToken adds claims to ID tokens; a claim a granted scope releases
	// (email, name) is never taken from it.
	IDToken func(ctx context.Context, info ClaimsInfo) map[string]any
	// UserInfo adds claims to the userinfo response, the same way.
	UserInfo func(ctx context.Context, info ClaimsInfo) map[string]any
}

ExtensionClaims are an extension's claims. Each is additive: a claim the provider, the app's own options, or an earlier extension set is kept, and the reserved claims (iss, sub, aud, exp, cnf and the rest) are never taken from it.

type ExtensionGrant

type ExtensionGrant struct {
	// Type is the grant_type, an absolute URI such as
	// "urn:ietf:params:oauth:grant-type:device_code".
	Type string
	// Handle validates the request and answers who the tokens are for. Its
	// errors reach the client: see ErrorNamespace.
	Handle func(ctx context.Context, client Client, in TokenRequest) (GrantDecision, error)
	// ErrorNamespace, when set, makes an *auth.Error whose code is
	// "<namespace>.<rest>" answer error "<rest>" on the wire, so a plugin's
	// own codes (device.authorization_pending) keep their RFC names.
	ErrorNamespace string
}

ExtensionGrant serves one extension grant_type at the token endpoint, better-auth's extendOAuthProvider({grants}): the device authorization grant (RFC 8628) is one. The provider authenticates the client and checks it registered the grant type before calling Handle; Handle decides who the tokens are for, and the provider issues them through the same path as every other grant (refresh and ID token rules unchanged).

type GateInfo

type GateInfo struct {
	Call     *auth.Call
	ClientID string
	Scopes   []string
}

GateInfo is what a gate decides from: the signed-in call, the client and the requested scopes.

type GrantDecision

type GrantDecision struct {
	// UserID is who the tokens act for; "" is the client itself, as in
	// client_credentials (no refresh token, no ID token, no user scope).
	UserID string
	// SessionID is recorded on the token row; optional.
	SessionID string
	// Scopes are the granted scopes; each must be allowed for the client
	// (and the resource), or the request is invalid_scope.
	Scopes []string
	// Resource is the RFC 8707 resource, or "".
	Resource string
	// AuthTime is when the user authenticated, for the ID token.
	AuthTime *time.Time
	// AccessTokenClaims are this issuance's JWT access token claims: over
	// the extensions', under AccessTokenClaims and the resource's.
	AccessTokenClaims map[string]any
	// IDTokenClaims are added to the ID token, never replacing a claim.
	IDTokenClaims map[string]any
}

GrantDecision is what an extension grant decided.

type GuardedToken

type GuardedToken struct {
	Subject  string
	ClientID string
	Scopes   []string
	Claims   map[string]json.RawMessage
}

GuardedToken is what ResourceGuard verified, in the request context (GuardedTokenFrom).

func GuardedTokenFrom

func GuardedTokenFrom(ctx context.Context) (GuardedToken, bool)

GuardedTokenFrom returns the token ResourceGuard verified.

type InitialAccessTokenFunc

type InitialAccessTokenFunc func(ctx context.Context, token string) (referenceID string, ok bool)

InitialAccessTokenFunc validates an RFC 7591 initial access token sent as a Bearer token to the registration endpoint, better-auth's validateInitialAccessToken: ok admits the registration (even when open registration is off), and referenceID becomes the client's ReferenceID.

type IntrospectRequest

type IntrospectRequest struct {
	Token            string `json:"token"`
	TokenTypeHint    string `json:"token_type_hint"`
	ClientID         string `json:"client_id"`
	ClientSecret     string `json:"client_secret"`
	ClientAuthMethod string `json:"client_auth_method"`
	// ClientAssertion is a private_key_jwt assertion; Handler checks
	// client_assertion_type.
	ClientAssertion string `json:"client_assertion"`
	// ClientAssertionType and ClientCertificates are for an extension
	// ClientAuthMethod (an assertion of its type, mutual TLS).
	ClientAssertionType string              `json:"client_assertion_type,omitempty"`
	ClientCertificates  []*x509.Certificate `json:"-"`
}

IntrospectRequest is RFC 7662 section 2.1, with the caller's client credentials.

type Introspection

type Introspection struct {
	Active    bool   `json:"active"`
	Scope     string `json:"scope,omitempty"`
	ClientID  string `json:"client_id,omitempty"`
	Subject   string `json:"sub,omitempty"`
	TokenType string `json:"token_type,omitempty"`
	ExpiresAt int64  `json:"exp,omitempty"`
	IssuedAt  int64  `json:"iat,omitempty"`
	Issuer    string `json:"iss,omitempty"`
	Audience  string `json:"aud,omitempty"`
	// Audiences are the resources of a token issued for several; aud is
	// then written as their array, and Audience holds them space-separated.
	Audiences []string `json:"-"`
	// Confirmation is cnf: the DPoP key a bound token needs.
	Confirmation *Confirmation `json:"cnf,omitempty"`
	// Extra are the token's resource's custom claims; a member named like
	// one of the fields above is ignored.
	Extra map[string]any `json:"-"`
}

Introspection is RFC 7662 section 2.2. An inactive token answers only active false.

func (Introspection) MarshalJSON

func (in Introspection) MarshalJSON() ([]byte, error)

MarshalJSON writes the fields and, beside them, Extra's claims that do not collide with a field or a protected claim.

type IssueRequest

type IssueRequest struct {
	// GrantType names the grant the tokens come from. Required.
	GrantType string
	// ClientID is the client the tokens are for; it must exist and be
	// enabled.
	ClientID string
	// UserID and SessionID are the user the tokens act for and the session
	// behind them; "" for a client's own token, which then cannot carry a
	// user scope.
	UserID, SessionID string
	Scopes            []string
	// Resource makes the access token a JWT for that resource (RFC 8707).
	Resource string
	// Nonce and AuthTime go into the ID token.
	Nonce    string
	AuthTime *time.Time
	// Confirmation binds the tokens (cnf): a DPoP key thumbprint or a
	// client certificate's, better-auth's issueTokens confirmation. The
	// token type becomes DPoP for a JKT.
	Confirmation *Confirmation
	// ReferenceID is the post-login reference the authorization was given
	// for.
	ReferenceID string
}

IssueRequest is what IssueTokens mints, better-auth's OAuthTokenIssueParams. It is a raw minting primitive: the caller has already authenticated the client and decided that these scopes, this user and this resource are allowed (CheckScopes helps); nothing here asks the user.

type JWKSFetcher

type JWKSFetcher func(ctx context.Context, rawURL string) ([]byte, error)

JWKSFetcher fetches a client's jwks_uri, answering the body of an exactly 200 response of a JSON media type. The default refuses every address that is not public, follows no redirect, gives up after 5 seconds, and reads at most 64 KiB. Replace it only for tests, or for an egress proxy that enforces the same rules.

type Metadata

type Metadata struct {
	// Extra are the extensions' members (Extension.Metadata), written beside
	// the provider's and never replacing one.
	Extra                             map[string]any `json:"-"`
	Issuer                            string         `json:"issuer"`
	AuthorizationEndpoint             string         `json:"authorization_endpoint"`
	TokenEndpoint                     string         `json:"token_endpoint"`
	UserInfoEndpoint                  string         `json:"userinfo_endpoint,omitempty"`
	JWKSURI                           string         `json:"jwks_uri"`
	RegistrationEndpoint              string         `json:"registration_endpoint,omitempty"`
	RevocationEndpoint                string         `json:"revocation_endpoint"`
	EndSessionEndpoint                string         `json:"end_session_endpoint,omitempty"`
	DeviceAuthorizationEndpoint       string         `json:"device_authorization_endpoint,omitempty"`
	IntrospectionEndpoint             string         `json:"introspection_endpoint"`
	ScopesSupported                   []string       `json:"scopes_supported"`
	ResponseTypesSupported            []string       `json:"response_types_supported"`
	ResponseModesSupported            []string       `json:"response_modes_supported"`
	GrantTypesSupported               []string       `json:"grant_types_supported"`
	SubjectTypesSupported             []string       `json:"subject_types_supported"`
	IDTokenSigningAlgValuesSupported  []string       `json:"id_token_signing_alg_values_supported,omitempty"`
	TokenEndpointAuthMethodsSupported []string       `json:"token_endpoint_auth_methods_supported"`
	IntrospectionAuthMethodsSupported []string       `json:"introspection_endpoint_auth_methods_supported"`
	RevocationAuthMethodsSupported    []string       `json:"revocation_endpoint_auth_methods_supported"`
	// The private_key_jwt assertion algorithms, every jwt.AlgValues entry,
	// for the three endpoints that authenticate clients.
	TokenAuthSigningAlgValuesSupported         []string `json:"token_endpoint_auth_signing_alg_values_supported"`
	IntrospectionAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported"`
	RevocationAuthSigningAlgValuesSupported    []string `json:"revocation_endpoint_auth_signing_alg_values_supported"`
	CodeChallengeMethodsSupported              []string `json:"code_challenge_methods_supported"`
	ClaimsSupported                            []string `json:"claims_supported,omitempty"`
	PromptValuesSupported                      []string `json:"prompt_values_supported"`
	IssParameterSupported                      bool     `json:"authorization_response_iss_parameter_supported"`
	CIMDSupported                              bool     `json:"client_id_metadata_document_supported,omitempty"`
	ClaimsParameterSupported                   bool     `json:"claims_parameter_supported,omitempty"`
	BackchannelLogoutSupported                 bool     `json:"backchannel_logout_supported,omitempty"`
	BackchannelLogoutSessionSupported          bool     `json:"backchannel_logout_session_supported,omitempty"`
	ACRValuesSupported                         []string `json:"acr_values_supported,omitempty"`
	RequestParameterSupported                  bool     `json:"request_parameter_supported"`
	RequestURIParameterSupported               bool     `json:"request_uri_parameter_supported"`
	DPoPSigningAlgValuesSupported              []string `json:"dpop_signing_alg_values_supported"`
}

Metadata is the authorization server metadata (RFC 8414) and OpenID Connect Discovery document.

func (Metadata) MarshalJSON

func (m Metadata) MarshalJSON() ([]byte, error)

MarshalJSON writes the document and, beside it, Extra's members that do not collide with one of its own.

type Option

type Option func(*Plugin)

Option configures the plugin; pass options to New.

func AccessTokenClaims

func AccessTokenClaims(fn AccessTokenClaimsFunc) Option

AccessTokenClaims installs an AccessTokenClaimsFunc.

func AccessTokenTTL

func AccessTokenTTL(d time.Duration) Option

AccessTokenTTL overrides DefaultAccessTokenTTL.

func AdvertisedClaims

func AdvertisedClaims(claims ...string) Option

AdvertisedClaims replaces claims_supported in discovery, better-auth's advertisedMetadata.claims_supported.

func AdvertisedScopes

func AdvertisedScopes(scopes ...string) Option

AdvertisedScopes replaces scopes_supported in discovery with a subset of Scopes, better-auth's advertisedMetadata.scopes_supported; the provider still serves every scope.

func AllowDynamicRegistration

func AllowDynamicRegistration() Option

AllowDynamicRegistration opens RFC 7591 registration at <base>/register to signed-in users, better-auth's allowDynamicClientRegistration. Off by default: anyone who can register a client can show your users a consent screen under a name they chose.

func AllowPublicClientPrelogin

func AllowPublicClientPrelogin() Option

AllowPublicClientPrelogin serves the public-client-prelogin operation: the consent or login page may read a client's public fields before the user signs in, with the signed query it was given, better-auth's allowPublicClientPrelogin.

func AllowUnauthenticatedRegistration

func AllowUnauthenticatedRegistration() Option

AllowUnauthenticatedRegistration lets a caller with no session register too, better-auth's allowUnauthenticatedClientRegistration, which MCP clients expect. It implies AllowDynamicRegistration. Such a client has no owner, so only the app's Go code can manage it.

func AssertionMaxLifetime

func AssertionMaxLifetime(d time.Duration) Option

AssertionMaxLifetime overrides DefaultAssertionMaxLifetime.

func BackchannelHTTPClient

func BackchannelHTTPClient(c *http.Client) Option

BackchannelHTTPClient replaces the client that delivers logout tokens, for tests or an egress proxy; it must not follow redirects. The default follows none and times out after 5 seconds.

func BasePath

func BasePath(path string) Option

BasePath moves the protocol endpoints from DefaultBasePath under the issuer.

func CacheResources

func CacheResources(ttl time.Duration) Option

CacheResources keeps the registered resources the provider reads when it issues, refreshes or introspects a token in this process's memory for ttl (0 is DefaultResourceCacheTTL, at most MaxResourceCacheTTL), better-auth's cachedResources. Without it each issuance reads the resource's row by primary key. Off by default. CacheResourcesIn keeps them in a store of the app's choosing instead, such as Redis.

CreateResource, UpdateResource, PatchResource, SeedResources and DeleteResource drop the entry once they have written, so this instance's own writes are seen at once (and every instance's, with a shared store). What stays true only up to the TTL, as in better-auth: a resource created, changed, disabled or deleted on another instance with a per-process cache (or by SQL), or inside a transaction a concurrent issuance read before it committed, is served as it was until the entry expires. Resource and Resources, the admin reads, always read the database. An identifier with no row is remembered only when it is a ProtectedResource declared in code, so a client asking for random identifiers cannot grow the cache. A failing store is logged and the database is read instead.

func CacheResourcesIn

func CacheResourcesIn(kv auth.SecondaryStorage, ttl time.Duration) Option

CacheResourcesIn is CacheResources in kv instead of this process's memory: any auth.SecondaryStorage, such as contrib/redis's NewStorage or the one the app already passes to auth.WithSecondaryStorage, so every instance sees one cache and a write on any instance drops the entry for all of them. Only Get, Set and Delete are used. Entries carry their own expiry, judged by the core's clock, and are stored with ttl so the store also forgets them. nil is refused (auth.ErrNilOption).

func Claims

func Claims(fn ClaimsFunc) Option

Claims adds the app's claims to ID tokens and userinfo answers.

func ClientIDMetadataDocuments

func ClientIDMetadataDocuments(opts ...CIMDOption) Option

ClientIDMetadataDocuments accepts client ids that are https URLs of client metadata documents. Off by default: it makes the provider fetch URLs a caller chooses, which the default fetcher confines to public addresses.

func ClientJWKSFetch

func ClientJWKSFetch(f JWKSFetcher) Option

ClientJWKSFetch replaces the jwks_uri fetcher; see JWKSFetcher.

func ClientPrivileges

func ClientPrivileges(fn ClientPrivilegesFunc) Option

ClientPrivileges installs a ClientPrivilegesFunc over the owner operations (create, get, list, update, rotate, delete).

func ClientReference

func ClientReference(fn func(ctx context.Context, c *auth.Call) string) Option

ClientReference makes clients belong to a reference (an organization, a team) instead of the user, better-auth's clientReference: fn returns the signed-in user's current reference (such as the active organization), a created client takes it as ReferenceID with no owning user, and the owner operations see the clients of that reference. "" falls back to the user.

func CodeTTL

func CodeTTL(d time.Duration) Option

CodeTTL overrides DefaultCodeTTL.

func ConsentPage

func ConsentPage(u string) Option

ConsentPage is where the authorize endpoint sends a signed-in user who has not yet consented to the client and scopes, with the authorize request's parameters appended. The page shows the client (PublicClient) and posts the user's answer to the consent operation, which answers the URL to send the browser to. Without a ConsentPage, a client that needs consent is answered consent_required; clients created with SkipConsent never need it.

func DPoPProofMaxAge

func DPoPProofMaxAge(d time.Duration) Option

DPoPProofMaxAge overrides DefaultDPoPProofMaxAge.

func DPoPSigningAlgorithms

func DPoPSigningAlgorithms(algs ...jwt.Alg) Option

DPoPSigningAlgorithms replaces DPoPAlgorithms, better-auth's dpop.signingAlgorithms: the proof algorithms accepted, advertised as dpop_signing_alg_values_supported and named in a DPoP challenge.

func DeviceAuthorizationEndpoint

func DeviceAuthorizationEndpoint(u string) Option

DeviceAuthorizationEndpoint advertises the RFC 8628 device authorization endpoint in discovery; the module serving it passes its URL.

func EncryptClientSecrets

func EncryptClientSecrets(e SecretEncryption) Option

EncryptClientSecrets also stores each new client secret encrypted, better-auth's storeClientSecret "encrypted", so RevealClientSecret can read it back (for client_secret_jwt-style use, or to show it again). Authentication still compares the hash in constant time. Off by default because a recoverable secret is a weaker store than a hash: whoever holds the key and a copy of the database holds every secret. auth.New logs a warning while it is on.

func EnforcePerClientResources

func EnforcePerClientResources(on bool) Option

EnforcePerClientResources(false) lets any client ask for any resource; the default (true, as better-auth) needs a client-resource link.

func Extend

func Extend(exts ...Extension) Option

Extend installs extensions, better-auth's extensions option.

func FormatRefreshToken

func FormatRefreshToken(f RefreshTokenFormat) Option

FormatRefreshToken installs a RefreshTokenFormat; both functions are required.

func GenerateAccessToken

func GenerateAccessToken(fn TokenGenerator) Option

GenerateAccessToken replaces the opaque access token generator.

func GenerateClientID

func GenerateClientID(fn func() string) Option

GenerateClientID replaces the client id generator, better-auth's generateClientId. It must return at least 22 characters and never an https URL (ErrShortGenerated otherwise, at registration).

func GenerateClientSecret

func GenerateClientSecret(fn func() string) Option

GenerateClientSecret replaces the random part of a client secret (after the ubos_ prefix), better-auth's generateClientSecret; at least 22 characters from a cryptographic source.

func GenerateRefreshToken

func GenerateRefreshToken(fn TokenGenerator) Option

GenerateRefreshToken replaces the refresh token generator.

func GrantTypes

func GrantTypes(grants ...string) Option

GrantTypes narrows DefaultGrantTypes. refresh_token needs authorization_code (ErrBadGrantTypes otherwise), as better-auth requires.

func HashStoredSecrets

func HashStoredSecrets(fn SecretHasher) Option

HashStoredSecrets replaces SHA-256 as the stored form of every secret; see SecretHasher. better-auth's "encrypted" client secrets exist for client_secret_jwt, which needs the plaintext back; this provider does not offer that method, so it keeps only one-way forms.

func IDTokenTTL

func IDTokenTTL(d time.Duration) Option

IDTokenTTL overrides DefaultIDTokenTTL.

func IdentifierValidator

func IdentifierValidator(fn func(identifier string) bool) Option

IdentifierValidator replaces the RFC 8707 identifier check (absolute URI, no fragment), better-auth's identifierValidator.

func Issuer

func Issuer(iss string) Option

Issuer is the provider's identifier, the iss of every ID token and the base of every endpoint URL: an absolute https URL with no query or fragment, such as "https://auth.example.com". Required. The jwt plugin's Issuer must be the same string.

func LoginPage

func LoginPage(u string) Option

LoginPage is where the authorize endpoint sends a browser that is not signed in, with the authorize request's parameters appended to its query. After signing the user in, the page sends the browser to Plugin.ResumeURL(those parameters). Required: a same-site path or a URL on a trusted origin.

func M2MAccessTokenTTL

func M2MAccessTokenTTL(d time.Duration) Option

M2MAccessTokenTTL is the lifetime of a client_credentials access token, better-auth's m2mAccessTokenExpiresIn; 0 takes AccessTokenTTL.

func MCP

func MCP() Option

MCP configures the provider for Model Context Protocol clients, better-auth's mcp(): open unauthenticated dynamic registration (MCP clients register themselves) and a MCPRefreshReuseInterval reuse window. Options after it override either. There is no separate MCP plugin: the resource side is RequireToken (in process) or ResourceGuard (another process), and ProtectedResource declares the MCP server's URL and scopes.

func NoDefaultRateLimits

func NoDefaultRateLimits() Option

NoDefaultRateLimits drops every DefaultRateLimits rule.

func PairwiseSecret

func PairwiseSecret(secret string) Option

PairwiseSecret enables pairwise subject identifiers, better-auth's pairwiseSecret: a client registered with subject_type "pairwise" sees each user under its own sub, so two clients cannot join their users by sub. At least 32 characters; changing it changes every pairwise sub.

func PostLogin

func PostLogin(g PostLoginGate) Option

PostLogin installs the post-login gate; it needs auth.WithSecret, which signs the marker the consent page carries once the gate is passed (auth.New refuses it without one, with ErrBadGate and ErrNoSecret).

func ProtectedResource

func ProtectedResource(resource string, scopes ...string) Option

ProtectedResource declares a resource server this provider issues tokens for, such as an MCP server: its canonical URL (RFC 8707 / 9728: https, or http on loopback, with no query or fragment) and the scopes it accepts. Handler then serves its RFC 9728 metadata, which MCP clients read to find the authorization server. better-auth's mcp({resource}).

func RateLimit

func RateLimit(endpoint auth.EndpointID, limit int, window time.Duration) Option

RateLimit sets the per-IP limit of one endpoint to limit calls per window, replacing its default. limit 0 removes the endpoint's default, better-auth's rateLimit.<endpoint>: false.

func RefreshReplay

func RefreshReplay(e SecretEncryption) Option

RefreshReplay makes a refresh token reused inside RefreshReuseInterval answer the response its first use got, better-auth's refreshTokenReuseInterval replay, instead of a new pair: the response is stored encrypted until the window ends, and a reuse asking for other scopes, another resource or another DPoP key is invalid_grant without revoking the family. Needs RefreshReuseInterval. Off by default because it stores issued tokens in a recoverable form; auth.New logs a warning while it is on.

func RefreshReuseInterval

func RefreshReuseInterval(d time.Duration) Option

RefreshReuseInterval lets a refresh token that was just rotated be presented again within d without revoking its family, for a client that refreshes from two tabs or retries after a lost response. Each such reuse issues a new pair; after d, reuse revokes the family as before. 0 (the default) allows no reuse. better-auth's refreshTokenReuseInterval, which replays the cached response instead: this library keeps no plaintext token to replay.

func RefreshTokenTTL

func RefreshTokenTTL(d time.Duration) Option

RefreshTokenTTL overrides DefaultRefreshTokenTTL.

func RefuseJWTRevocation

func RefuseJWTRevocation() Option

RefuseJWTRevocation answers the revocation of a still-valid JWT access token with unsupported_token_type (an expired or unknown one is a 200 no-op), better-auth's behaviour, which tells the client the token stays usable by offline verifiers until it expires. Off by default: the provider then revokes it, so in-process checks and introspection refuse it at once. auth.New logs a warning while it is on.

func RegistrationAllowedResources

func RegistrationAllowedResources(ids ...string) Option

RegistrationAllowedResources are the extra resources a registration may ask for in its resources member, beyond the defaults, better-auth's clientRegistrationAllowedResources.

func RegistrationAllowedScopes

func RegistrationAllowedScopes(scopes ...string) Option

RegistrationAllowedScopes widens what a registration may ask for beyond RegistrationDefaultScopes, better-auth's clientRegistrationAllowedScopes: a registration naming a scope outside both lists is invalid_scope. With neither option, any scope the provider supports may be registered.

func RegistrationDefaultResources

func RegistrationDefaultResources(ids ...string) Option

RegistrationDefaultResources are linked to every client created through registration, the operations or Go, better-auth's clientRegistrationDefaultResources.

func RegistrationDefaultScopes

func RegistrationDefaultScopes(scopes ...string) Option

RegistrationDefaultScopes are the scopes a dynamically registered client gets when its registration names none, better-auth's clientRegistrationDefaultScopes; nil gives it every scope (Scopes).

func RegistrationRequirePKCE

func RegistrationRequirePKCE(on bool) Option

RegistrationRequirePKCE(false) lets confidential clients created by dynamic registration omit PKCE, better-auth's clientRegistrationRequirePKCE (default true). Public clients always need it.

func RegistrationSecretExpiration

func RegistrationSecretExpiration(d time.Duration) Option

RegistrationSecretExpiration gives secrets of dynamically registered confidential clients a lifetime, better-auth's clientRegistrationClientSecretExpiration; 0 (the default) never expires.

func RegistrationSecretExpiresAt

func RegistrationSecretExpiresAt(t time.Time) Option

RegistrationSecretExpiresAt ends the secrets of dynamically registered confidential clients at one instant, better-auth's clientRegistrationClientSecretExpiration given a Date; a time already past on the app's clock (auth.WithClock) is refused at auth.New (ErrBadTTL). With RegistrationSecretExpiration too, the earlier end wins.

func RequestURIResolver

func RequestURIResolver(fn RequestURIResolverFunc) Option

RequestURIResolver accepts request_uri at authorize: every parameter comes from the stored request, except client_id, which the authorization URL carries. Without it, request_uri is request_uri_not_supported.

func RevokeAccessTokenOnly

func RevokeAccessTokenOnly() Option

RevokeAccessTokenOnly makes revoking an access token end that token only, better-auth's behaviour: its refresh token keeps working. Off by default, because a client that revokes an access token after a leak usually wants the whole grant gone, and RFC 7009 lets the server end both; auth.New logs a warning while it is on.

func ScopeExpirations

func ScopeExpirations(ttls map[string]time.Duration) Option

ScopeExpirations gives scopes shorter access-token lifetimes, better-auth's scopeExpirations: a token carrying several gets the earliest. A value longer than the default changes nothing.

func Scopes

func Scopes(scopes ...string) Option

Scopes replaces DefaultScopes with every scope the provider supports. Leaving ScopeOpenID out makes it a plain OAuth 2.1 server: no ID tokens, no userinfo and no openid-configuration document.

func SelectAccount

func SelectAccount(g SelectAccountGate) Option

SelectAccount installs the select-account gate.

func SelectAccountFallsBackToLogin

func SelectAccountFallsBackToLogin() Option

SelectAccountFallsBackToLogin sends prompt=select_account, when no SelectAccount page is configured, to the login page so the user can sign in as someone else; the default is better-auth's unsupported_prompt_select_account.

func SignupPage

func SignupPage(u string) Option

SignupPage is where prompt=create sends the browser, with the authorize parameters, better-auth's signup.page. Without it, prompt=create goes to LoginPage.

func SignupRedirect

func SignupRedirect(fn SignupRedirectFunc) Option

SignupRedirect installs the sign-up gate.

func TokenResponseFields

func TokenResponseFields(fn func(ctx context.Context, info TokenFieldsInfo) (map[string]any, error)) Option

TokenResponseFields adds members to the token response envelope (not to the token), better-auth's customTokenResponseFields; the standard members cannot be replaced, and an error from fn fails the request.

func TrustedClients

func TrustedClients(clients ...TrustedClient) Option

TrustedClients installs in-memory clients.

func UserInfoInsufficientScope

func UserInfoInsufficientScope() Option

UserInfoInsufficientScope answers userinfo for a token without openid with RFC 6750's 403 insufficient_scope instead of better-auth's 400 invalid_scope (the default).

func ValidateInitialAccessToken

func ValidateInitialAccessToken(fn InitialAccessTokenFunc) Option

ValidateInitialAccessToken installs an InitialAccessTokenFunc. Without one, a registration that presents a token is refused (invalid_token) rather than falling through to open registration.

func ValidateRedirectURI

func ValidateRedirectURI(fn RedirectValidator) Option

ValidateRedirectURI installs a RedirectValidator.

func WithGrant

func WithGrant(g ExtensionGrant) Option

WithGrant registers an extension grant and adds its type to the grant types the provider serves and advertises.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the OAuth 2.1 / OpenID Connect provider. Build it with New.

func New

func New(signer Signer, opts ...Option) *Plugin

New builds the provider. signer signs ID tokens and publishes its keys at <base>/jwks: usually the jwt plugin, which must then also be in auth.WithPlugins, before this plugin. Any other Signer that is itself a plugin must be registered before this one too; one that is not a plugin needs no registration.

func (*Plugin) AuthenticateClient

func (p *Plugin) AuthenticateClient(ctx context.Context, cred ClientCredentials, audience string) (Client, error)

AuthenticateClient checks a client's credentials as the token endpoint does, for a module that serves an endpoint of its own (a device authorization endpoint). audience is the absolute URL a private_key_jwt assertion must name (that endpoint's URL; the issuer is accepted too). Every failure is ErrInvalidClient.

func (*Plugin) Authorize

func (p *Plugin) Authorize(ctx context.Context, in AuthorizeRequest) (AuthorizeResult, error)

Authorize runs an authorization request from Go: the redirect to answer, or an error. A *RedirectError carries the redirect that reports it to the client; any other error must be shown to the user, never redirected.

func (*Plugin) CheckScopes

func (p *Plugin) CheckScopes(ctx context.Context, client Client, scopes []string, resource string) ([]string, error)

CheckScopes decides the scopes a client may be granted for resource ("" for none), for a module that issues through WithGrant: ErrInvalidScope for a scope the provider or the client does not allow; for a resource, ErrInvalidTarget when it is unknown, disabled or (EnforcePerClientResources) not linked to the client, and the scopes narrowed to its allowlist (ErrInvalidScope when none is left). It answers the effective scopes.

func (*Plugin) Client

func (p *Plugin) Client(ctx context.Context, clientID string) (Client, error)

Client returns any client by id, from Go.

func (p *Plugin) ClientResourceLinks(ctx context.Context, clientID string) ([]ClientResourceLink, error)

ClientResourceLinks lists the resources a client is linked to, with each link's metadata.

func (*Plugin) Clients

func (p *Plugin) Clients(ctx context.Context) (ClientList, error)

Clients lists the signed-in user's clients.

func (*Plugin) ConfirmEndSession

func (p *Plugin) ConfirmEndSession(ctx context.Context, in EndSessionConfirmRequest) (EndSessionResult, error)

ConfirmEndSession completes a confirmed logout from Go.

func (*Plugin) Consent

func (p *Plugin) Consent(ctx context.Context, in ConsentRequest) (AuthorizeResult, error)

Consent records the signed-in user's answer from Go; see ConsentRequest.

func (*Plugin) ConsentByID

func (p *Plugin) ConsentByID(ctx context.Context, id string) (Consent, error)

ConsentByID returns one of the signed-in user's consents.

func (*Plugin) Consents

func (p *Plugin) Consents(ctx context.Context) (ConsentList, error)

Consents lists the signed-in user's consents.

func (*Plugin) ConsumeClientAssertion

func (p *Plugin) ConsumeClientAssertion(ctx context.Context, clientID, assertion, audience string) error

ConsumeClientAssertion verifies a private_key_jwt client assertion (RFC 7523) for the client it names and spends its jti, better-auth's consumeClientAssertion: signed by a key of the client's jwks or jwks_uri, iss and sub the client, aud audience (the endpoint URL) or the issuer, exp within AssertionMaxLifetime, and a jti never seen before. Every failure is ErrInvalidClient.

func (*Plugin) Continue

func (p *Plugin) Continue(ctx context.Context, in ContinueRequest) (AuthorizeResult, error)

Continue resumes an authorization after a gate's page, from Go.

func (*Plugin) CreateClient

func (p *Plugin) CreateClient(ctx context.Context, spec ClientSpec) (Registration, error)

CreateClient registers a client from Go, with the restricted fields only trusted code may set (better-auth's admin create-client). It runs outside the operation pipeline on purpose, like jwt.Sign: nothing a request sends reaches it.

func (*Plugin) CreateOwnedClient

func (p *Plugin) CreateOwnedClient(ctx context.Context, in ClientMetadata) (Registration, error)

CreateOwnedClient registers a client the signed-in user owns. It needs a fresh sign-in, because the secret outlives the session that made it.

func (*Plugin) CreateResource

func (p *Plugin) CreateResource(ctx context.Context, r Resource) (Resource, error)

CreateResource registers a resource from Go, better-auth's admin POST /admin/oauth2/resources: invalid_target for a bad identifier, resource_exists for a duplicate.

func (*Plugin) DeleteClient

func (p *Plugin) DeleteClient(ctx context.Context, clientID string) error

DeleteClient deletes any client from Go, with its codes, tokens and consents.

func (*Plugin) DeleteOwnedClient

func (p *Plugin) DeleteOwnedClient(ctx context.Context, clientID string) error

DeleteOwnedClient deletes an owned client with its codes, tokens and consents.

func (*Plugin) DeleteResource

func (p *Plugin) DeleteResource(ctx context.Context, identifier string) error

DeleteResource removes a resource and its client links; tokens issued for it become inactive.

func (*Plugin) EndSession

func (p *Plugin) EndSession(ctx context.Context, in EndSessionRequest) (EndSessionResult, error)

EndSession runs an RP-initiated logout from Go.

func (*Plugin) Handler

func (p *Plugin) Handler(opts ...authhttp.Option) http.Handler

Handler serves the protocol endpoints and the discovery documents, at their paths under the issuer: mount it at the root of the issuer's host, or at each path. opts are the authhttp options the app's auth handler uses (cookie prefix, trusted proxies), so the authorize endpoint reads the same session. Every endpoint calls its operation through the pipeline; an unknown path answers 404.

func (*Plugin) HashToken

func (p *Plugin) HashToken(token string, kind SecretKind) []byte

HashToken is the stored form of a token or secret, better-auth's hashToken: SHA-256, or HashStoredSecrets' function. A caller uses it to find or revoke a row by a token's value; the provider never stores the value itself.

func (*Plugin) Init

func (p *Plugin) Init(a *auth.Auth) error

Init captures the core, checks the pages against the trusted origins, and checks that the jwt plugin signs with the provider's issuer.

func (*Plugin) Introspect

func (p *Plugin) Introspect(ctx context.Context, in IntrospectRequest) (Introspection, error)

Introspect runs an introspection request from Go.

func (*Plugin) IssueTokens

func (p *Plugin) IssueTokens(ctx context.Context, in IssueRequest) (TokenResponse, error)

IssueTokens mints an access token (and a refresh token with offline_access, and an ID token with openid) outside the token endpoint, better-auth's getOAuthProviderApi(...).issueTokens: for an app endpoint that runs its own grant. The resource's policy, the lifetimes, the token response fields and the hashed storage all apply as at the token endpoint.

func (*Plugin) LinkClient

func (p *Plugin) LinkClient(ctx context.Context, identifier, clientID string) (alreadyLinked bool, err error)

LinkClient lets a client ask for a resource; linking twice answers alreadyLinked true and changes nothing, as better-auth.

func (*Plugin) LinkClientWithMetadata

func (p *Plugin) LinkClientWithMetadata(ctx context.Context, identifier, clientID string, metadata json.RawMessage) (alreadyLinked bool, err error)

LinkClientWithMetadata is LinkClient with the link's metadata, better-auth's oauthClientResource.metadata: any JSON value (invalid_request otherwise). A link that exists already keeps its metadata.

func (*Plugin) Metadata

func (p *Plugin) Metadata() Metadata

Metadata builds the discovery document.

func (*Plugin) Name

func (p *Plugin) Name() string

Name is PluginName.

func (*Plugin) Operations

func (p *Plugin) Operations() []auth.Operation

Operations lists every operation; the self-served ones are wired but mounted only by Handler.

func (*Plugin) OptionErr

func (p *Plugin) OptionErr() error

OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).

func (*Plugin) OwnedClient

func (p *Plugin) OwnedClient(ctx context.Context, clientID string) (Client, error)

OwnedClient returns one of the signed-in user's clients; another user's is ErrClientNotFound.

func (*Plugin) PatchResource

func (p *Plugin) PatchResource(ctx context.Context, identifier string, patch ResourcePatch) (Resource, error)

PatchResource applies a ResourcePatch to the resource identifier names and returns the result; ErrInvalidTarget when there is none. Two patches racing each other can lose one field update; an admin edit is rare enough that the provider takes no lock for it.

func (*Plugin) PublicClientInfo

func (p *Plugin) PublicClientInfo(ctx context.Context, clientID string) (PublicClient, error)

PublicClientInfo returns what a consent page may show about a client.

func (*Plugin) PublicClientPrelogin

func (p *Plugin) PublicClientPrelogin(ctx context.Context, in PreloginRequest) (PublicClient, error)

PublicClientPrelogin returns a client's public fields to a page that holds the signed query the provider gave it, before sign-in; it needs AllowPublicClientPrelogin.

func (*Plugin) RateLimits

func (p *Plugin) RateLimits() []auth.RateRule

RateLimits declares the per-IP rules: the defaults unless dropped, with every RateLimit override on top.

func (*Plugin) Register

func (p *Plugin) Register(ctx context.Context, in ClientMetadata) (Registration, error)

Register runs RFC 7591 registration from Go, as Handler's register endpoint does: closed unless AllowDynamicRegistration, and open to a caller with no session only under AllowUnauthenticatedRegistration.

func (*Plugin) RequireToken

func (p *Plugin) RequireToken(o RequireOptions) func(http.Handler) http.Handler

RequireToken guards a resource server route, such as an MCP endpoint: it verifies the bearer access token (VerifyAccessToken) and the required scopes, and puts the token in the context (TokenFrom). A missing or bad token is 401 and a missing scope 403, each with the WWW-Authenticate Bearer challenge of RFC 6750 and RFC 9728 (resource_metadata, scope). better-auth's requireMcpAuth.

func (*Plugin) Requires

func (p *Plugin) Requires() []string

Requires names the signer when it is a plugin (the jwt plugin by default), so auth.New refuses a configuration that forgot to register it; a Signer that is not a plugin requires nothing.

func (*Plugin) Resource

func (p *Plugin) Resource(ctx context.Context, identifier string) (Resource, error)

Resource returns a registered resource; ErrInvalidTarget when there is none.

func (*Plugin) ResourceMetadataFor

func (p *Plugin) ResourceMetadataFor(resource string) (ResourceMetadata, bool)

ResourceMetadataFor builds the document for a declared resource; false when it was not declared with ProtectedResource. User scopes (openid, profile, email, offline_access) are never advertised, as better-auth does not: they are the provider's, not the resource's.

func (*Plugin) Resources

func (p *Plugin) Resources(ctx context.Context) ([]Resource, error)

Resources lists the registered resources.

func (*Plugin) ResumeURL

func (p *Plugin) ResumeURL(query url.Values) string

ResumeURL is the authorize endpoint with query, for the login page to send the browser back to once the user has signed in. query is the parameters the login page received.

func (*Plugin) RevealClientSecret

func (p *Plugin) RevealClientSecret(ctx context.Context, clientID string) (string, error)

RevealClientSecret reads a client's secret back under EncryptClientSecrets; ErrSecretNotRecoverable otherwise. It is Go-only: no operation exposes it.

func (*Plugin) Revoke

func (p *Plugin) Revoke(ctx context.Context, in RevokeRequest) error

Revoke runs a revocation request from Go.

func (*Plugin) RevokeConsent

func (p *Plugin) RevokeConsent(ctx context.Context, clientID string) error

RevokeConsent withdraws the signed-in user's consent to a client and revokes the client's tokens for them.

func (*Plugin) RotateSecret

func (p *Plugin) RotateSecret(ctx context.Context, clientID string) (Registration, error)

RotateSecret replaces an owned confidential client's secret; the old one stops at once.

func (*Plugin) SeedResources

func (p *Plugin) SeedResources(ctx context.Context, mode SeedMode, seeds ...ResourceSeed) error

SeedResources writes resources from the app's configuration into the registry, better-auth's resources option with resourceSeedMode: call it at startup, after migrations (better-auth seeds at init and defers when the table is missing; this library leaves the moment to the app, which knows when its migrations ran). An identifier the registry refuses is ErrInvalidTarget, and nothing after it is written. Safe to run on every start and from several processes at once: a concurrent insert of the same resource is treated as the row existing.

func (*Plugin) SessionEnded

func (p *Plugin) SessionEnded(ctx context.Context, userID, id string) error

SessionEnded ends what this provider issued on a session: its access tokens stop, its refresh tokens too unless offline_access was granted (Back-Channel Logout section 2.7), and every client with a backchannel_logout_uri is sent a logout token after the commit.

func (*Plugin) SessionReplaced

func (p *Plugin) SessionReplaced(ctx context.Context, old, new auth.Session) error

SessionReplaced moves tokens issued on a pending session to the session that replaced it, so a later logout of the new one reaches them.

func (*Plugin) SetClientDisabled

func (p *Plugin) SetClientDisabled(ctx context.Context, clientID string, disabled bool) error

SetClientDisabled pauses or resumes a client from Go. A disabled client fails authentication and its authorize requests; its tokens stop at introspection and userinfo.

func (*Plugin) Sweep

func (p *Plugin) Sweep(ctx context.Context, cutoff time.Time) (int64, error)

Sweep deletes codes and tokens that can no longer be used: expired codes, and token rows whose access and refresh tokens have both expired.

func (*Plugin) Tables

func (p *Plugin) Tables() []sqlb.Table

Tables declares the four tables and their aliases.

func (*Plugin) Token

func (p *Plugin) Token(ctx context.Context, in TokenRequest) (TokenResponse, error)

Token runs a token request from Go.

func (*Plugin) TypedMigrations

func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)

TypedMigrations creates the four tables for engine. Every hash is UNIQUE, so a lookup by hash finds at most one row; a consent is unique per user and client.

func (*Plugin) UnlinkClient

func (p *Plugin) UnlinkClient(ctx context.Context, identifier, clientID string) error

UnlinkClient removes a client-resource link.

func (*Plugin) UpdateClient

func (p *Plugin) UpdateClient(ctx context.Context, clientID string, spec ClientSpec) (Client, error)

UpdateClient replaces any client's metadata and restricted fields from Go, better-auth's admin update-client. The authentication method cannot change, as for an owner.

func (*Plugin) UpdateConsent

func (p *Plugin) UpdateConsent(ctx context.Context, in UpdateConsentRequest) (Consent, error)

UpdateConsent replaces one of the signed-in user's consents' scopes.

func (*Plugin) UpdateOwnedClient

func (p *Plugin) UpdateOwnedClient(ctx context.Context, in UpdateClientRequest) (Client, error)

UpdateOwnedClient replaces an owned client's metadata.

func (*Plugin) UpdateResource

func (p *Plugin) UpdateResource(ctx context.Context, r Resource) (Resource, error)

UpdateResource replaces a resource's policy; the identifier is the key and never changes.

func (*Plugin) UserInfo

func (p *Plugin) UserInfo(ctx context.Context, token string) (map[string]any, error)

UserInfo answers the OpenID Connect userinfo request from Go.

func (*Plugin) UserSessionsEnded

func (p *Plugin) UserSessionsEnded(ctx context.Context, userID, exceptID string) error

UserSessionsEnded is SessionEnded for every session of a user but one.

func (*Plugin) UserTables

func (p *Plugin) UserTables() []auth.UserTable

UserTables names every user column, so DeleteUser removes the user's clients, codes, tokens and consents.

func (*Plugin) Validate

func (p *Plugin) Validate(c *auth.Config) error

Validate checks the issuer, the pages, the scopes, the grants, the lifetimes, and that every feature that signs has auth.WithSecret.

func (*Plugin) ValidateCIMDDocument

func (p *Plugin) ValidateCIMDDocument(clientID string, document []byte) (ClientMetadata, error)

ValidateCIMDDocument checks a client metadata document fetched from clientID with this provider's CIMD rules, better-auth's validateCimdMetadata: the client registration it would make, or an ErrCIMDFetch error naming the rule. Without ClientIDMetadataDocuments it is ErrCIMDFetch.

func (*Plugin) VerifyAccessToken

func (p *Plugin) VerifyAccessToken(ctx context.Context, token string) (AccessToken, error)

VerifyAccessToken checks an access token for a resource server: it exists, is neither expired nor revoked, and its client is enabled. Every failure is ErrInvalidToken.

func (*Plugin) VerifyOAuthQuery

func (p *Plugin) VerifyOAuthQuery(raw string) error

VerifyOAuthQuery checks an oauth_query a page posted back: exactly one signature, valid under a current or previous secret (compared in constant time), and not expired. Without a secret no query is signed and every query passes. better-auth's verifyOAuthQueryParams.

func (*Plugin) VerifyPresentedToken

func (p *Plugin) VerifyPresentedToken(ctx context.Context, pt PresentedToken) (AccessToken, error)

VerifyPresentedToken is VerifyAccessToken with the DPoP rules: a bound token needs the DPoP scheme and a proof of its key for this request (with ath), and the DPoP scheme needs a bound token. Failures are ErrInvalidToken or ErrInvalidDPoPProof.

func (*Plugin) WriteInsufficientScope

func (p *Plugin) WriteInsufficientScope(w http.ResponseWriter, o RequireOptions, missing ...string)

WriteInsufficientScope answers a request a RequireToken route found to need more scopes once it looked at the arguments, better-auth's createInsufficientScopeError thrown from a handler: 403 insufficient_scope naming the missing scopes, with the route's challenge.

type PostLoginGate

type PostLoginGate struct {
	// Page is the post-login page, a same-site path or a trusted URL.
	Page string
	// ShouldRedirect decides whether the user must pass Page; required.
	ShouldRedirect func(ctx context.Context, info GateInfo) bool
	// ConsentReferenceID, when set, is the reference (an organization id)
	// a consent is given for: a consent for another reference asks again,
	// and the codes and tokens carry it (AccessToken.ReferenceID,
	// AccessTokenClaimsInfo.ReferenceID).
	ConsentReferenceID func(ctx context.Context, info GateInfo) string
}

PostLoginGate is better-auth's postLogin: a page after sign-in (choosing an organization or team) whose choice the consent and the tokens are tied to.

type PreloginRequest

type PreloginRequest struct {
	ClientID   string `json:"client_id"`
	OAuthQuery string `json:"oauth_query"`
}

PreloginRequest asks for a client's public fields before sign-in.

type PresentedToken

type PresentedToken struct {
	// Scheme is SchemeDPoP or "Bearer".
	Scheme string
	Token  string
	// Proof is the DPoP header; Method and URL are the request's, which the
	// proof must name.
	Proof  string
	Method string
	URL    string
	// Certificate is the connection's TLS client certificate, which a
	// token bound to one (cnf x5t#S256, RFC 8705) must come with.
	Certificate *x509.Certificate
}

PresentedToken is an access token as a request presented it.

type PublicClient

type PublicClient struct {
	ClientID  string   `json:"client_id"`
	Name      string   `json:"client_name"`
	ClientURI string   `json:"client_uri,omitempty"`
	LogoURI   string   `json:"logo_uri,omitempty"`
	TosURI    string   `json:"tos_uri,omitempty"`
	PolicyURI string   `json:"policy_uri,omitempty"`
	Contacts  []string `json:"contacts,omitempty"`
}

PublicClient is what a consent page may show about a client.

type RedirectError

type RedirectError struct {
	Err        error
	RedirectTo string
}

RedirectError is an authorization error that is answered by redirecting to the client's redirect URI with error and state (RFC 6749 section 4.1.2.1). Err is the reason; errors.Is sees through it.

func (*RedirectError) Error

func (e *RedirectError) Error() string

Error is the reason's message; the redirect itself is RedirectTo.

func (*RedirectError) Unwrap

func (e *RedirectError) Unwrap() error

Unwrap returns the reason, so errors.Is matches the error being redirected.

type RedirectValidator

type RedirectValidator func(uri string, registered []string, defaultResult bool) bool

RedirectValidator decides a redirect URI at authorize, better-auth's validateRedirectUri: it gets the requested URI, the client's registered ones and the built-in exact-match result, and its answer replaces that result, both for issuing the code and for whether early errors may be sent to the URI. It relaxes RFC 9700's exact matching, so accept only hosts you control.

type RefreshTokenFormat

type RefreshTokenFormat struct {
	Encode func(ctx context.Context, token, sessionID string) (string, error)
	Decode func(ctx context.Context, encoded string) (string, error)
}

RefreshTokenFormat encodes refresh tokens on the wire, better-auth's formatRefreshToken (a JWE carrying the session id, say). Encode receives the random token and the session id, Decode answers the random token back; PrefixRefreshToken goes before the encoding, and the stored hash is always the random token's, so a format can be added or changed without touching stored rows, and a token Decode refuses is simply unknown.

type RegisteredMetadata

type RegisteredMetadata struct {
	Name                   string          `json:"client_name"`
	RedirectURIs           []string        `json:"redirect_uris"`
	GrantTypes             []string        `json:"grant_types"`
	ResponseTypes          []string        `json:"response_types"`
	AuthMethod             string          `json:"token_endpoint_auth_method"`
	Scope                  string          `json:"scope"`
	ClientURI              string          `json:"client_uri,omitempty"`
	LogoURI                string          `json:"logo_uri,omitempty"`
	TosURI                 string          `json:"tos_uri,omitempty"`
	PolicyURI              string          `json:"policy_uri,omitempty"`
	Contacts               []string        `json:"contacts,omitempty"`
	PostLogoutRedirectURIs []string        `json:"post_logout_redirect_uris,omitempty"`
	JWKS                   json.RawMessage `json:"jwks,omitempty"`
	JWKSURI                string          `json:"jwks_uri,omitempty"`

	SubjectType                      string         `json:"subject_type,omitempty"`
	BackchannelLogoutURI             string         `json:"backchannel_logout_uri,omitempty"`
	BackchannelLogoutSessionRequired bool           `json:"backchannel_logout_session_required,omitempty"`
	DPoPBoundAccessTokens            bool           `json:"dpop_bound_access_tokens,omitempty"`
	Resources                        []string       `json:"resources,omitempty"`
	ApplicationType                  string         `json:"application_type,omitempty"`
	SoftwareID                       string         `json:"software_id,omitempty"`
	SoftwareVersion                  string         `json:"software_version,omitempty"`
	SoftwareStatement                string         `json:"software_statement,omitempty"`
	CredentialsScopes                []string       `json:"client_credentials_scopes,omitempty"`
	InitialAccessToken               string         `json:"-"`
	Extra                            map[string]any `json:"-"`
	// contains filtered or unexported fields
}

RegisteredMetadata is a client's registration as the provider answers it, RFC 7591 section 3.2.1: ClientMetadata's fields after the provider applied its defaults, with those defaulted fields always present on the wire (see ClientMetadata for what each field means). Client embeds it.

type Registration

type Registration struct {
	Client
	// Secret is a plain string on purpose: this is the one place it is
	// meant to be seen.
	Secret                string `json:"client_secret,omitempty"`
	ClientIDIssuedAt      int64  `json:"client_id_issued_at"`
	ClientSecretExpiresAt int64  `json:"client_secret_expires_at"`
}

Registration is the answer to a registration: the client and, for a confidential client, its secret, shown this once (RFC 7591 section 3.2.1).

type RequestURIResolverFunc

type RequestURIResolverFunc func(ctx context.Context, requestURI, clientID string) (url.Values, error)

RequestURIResolverFunc returns the stored parameters of a pushed authorization request (RFC 9126), better-auth's requestUriResolver; nil values mean the request_uri is unknown or expired.

type RequireOptions

type RequireOptions struct {
	// Resource is the protected resource this route belongs to: only tokens
	// issued for it (RFC 8707 resource, the JWT's aud) are accepted, and its
	// metadata URL is named in every challenge (RFC 9728 section 5.1).
	// Optional; without it any of this provider's access tokens is accepted.
	Resource string
	// Scopes must all be on the token; a missing one is 403
	// insufficient_scope naming every missing scope.
	Scopes []string
	// ScopeSatisfied replaces the exact scope match, better-auth's
	// isScopeSatisfied: given the token's scopes and one required scope, it
	// says whether the requirement is met (a hierarchy, a wildcard).
	ScopeSatisfied func(granted []string, required string) bool
	// JSONRPC answers a refusal in the JSON-RPC 2.0 error envelope MCP
	// clients expect, {"jsonrpc":"2.0","error":{"code":-32000,...},"id":null},
	// with the same status and WWW-Authenticate challenge.
	JSONRPC bool
	// ChallengeScopes are the scopes a 401 challenge names, better-auth's
	// challengeScopes; nil names Scopes. A 403 always names the missing
	// ones.
	ChallengeScopes []string
}

RequireOptions configures RequireToken.

type Resource

type Resource struct {
	// Identifier is the RFC 8707 resource: an absolute URI without a
	// fragment (or what IdentifierValidator accepts). It never changes.
	Identifier string `json:"identifier"`
	Name       string `json:"name,omitempty"`
	AccessTTL  *int64 `json:"access_token_ttl,omitempty"`
	RefreshTTL *int64 `json:"refresh_token_ttl,omitempty"`
	// AllowedScopes narrows the scopes a token for this resource gets (an
	// intersection); nil allows any.
	AllowedScopes []string `json:"allowed_scopes,omitempty"`
	// CustomClaims are added to its JWT access tokens and introspection;
	// reserved claims are ignored.
	CustomClaims map[string]any `json:"custom_claims,omitempty"`
	// DPoPRequired makes every token for it DPoP-bound.
	DPoPRequired bool `json:"dpop_bound_access_tokens_required,omitempty"`
	// Disabled refuses new tokens for it (invalid_target); issued ones keep
	// working until they expire.
	Disabled bool `json:"disabled,omitempty"`
	// SigningAlgorithm pins the algorithm of its JWT access tokens (one the
	// jwt plugin has a key for: its signing key's or an AlgorithmKeys one),
	// better-auth's signingAlgorithm; "" is the signing key's.
	SigningAlgorithm string `json:"signing_algorithm,omitempty"`
	// SigningKeyID pins the key of its JWT access tokens by key id (the jwt
	// plugin's signing key or an AlgorithmKeys key, of SigningAlgorithm when
	// both are set), better-auth's signingKeyId; a request for several
	// resources whose pins differ is invalid_request.
	SigningKeyID string `json:"signing_key_id,omitempty"`
	// PolicyVersion counts the policy's writes, better-auth's policyVersion:
	// 1 at creation, one more at every update, so a cache keyed on it knows
	// a policy changed. Set by the provider; a value passed in is ignored.
	PolicyVersion int64           `json:"policy_version"`
	Metadata      json.RawMessage `json:"metadata,omitempty"`
	CreatedAt     time.Time       `json:"created_at"`
	UpdatedAt     time.Time       `json:"updated_at"`
}

Resource is a protected resource's token policy, better-auth's OAuthResourceInput. Nil fields fall back to the provider's defaults.

type ResourceGuard

type ResourceGuard struct {
	// Issuer and Audience are what a JWT access token must name: the
	// provider and this resource. Both required.
	Issuer   string
	Audience string
	// Keys resolves key ids, usually jwt.NewRemoteKeys(<issuer>/oauth2/jwks,
	// nil).Lookup.
	Keys func(kid string) (crypto.PublicKey, bool)
	// IntrospectURL, ClientID and ClientSecret verify opaque tokens by
	// introspection (client_secret_basic); empty refuses opaque tokens.
	IntrospectURL, ClientID, ClientSecret string
	// Force introspects every token, JWTs too, better-auth's
	// remoteVerify.force: a revoked JWT is refused at once rather than at
	// its expiry. It needs IntrospectURL.
	Force bool
	// AllowMissingAudience accepts an introspected token that names no
	// audience, better-auth's remoteVerify.allowMissingAudience: a token
	// issued without a resource. Off, such a token is refused, since nothing
	// says it was meant for this resource.
	AllowMissingAudience bool
	// ChallengeScopes are the scopes a 401 challenge names; nil names
	// Scopes.
	ChallengeScopes []string
	// HTTPClient makes the introspection request; nil is a client with a
	// 5-second timeout.
	HTTPClient *http.Client
	// Scopes must all be granted; ScopeSatisfied replaces the exact match,
	// better-auth's isScopeSatisfied.
	Scopes         []string
	ScopeSatisfied func(granted []string, required string) bool
	// ResourceMetadataURL is named in challenges; JSONRPC answers in the
	// MCP JSON-RPC error envelope.
	ResourceMetadataURL string
	JSONRPC             bool
	// Now is the clock; nil is time.Now.
	Now func() time.Time
}

ResourceGuard protects a resource server that runs apart from the provider (an MCP server in another process), better-auth's createMcpProtectedRequestHandler: it needs no database and no Plugin. A JWT access token (typ at+jwt) is verified against the provider's keys, issuer and this resource's audience; an opaque one, when IntrospectURL is set, by RFC 7662 introspection (better-auth's remoteVerify). The required scopes are then checked, and refusals carry the RFC 6750 / RFC 9728 challenge.

func (ResourceGuard) Middleware

func (g ResourceGuard) Middleware(next http.Handler) http.Handler

Middleware guards next.

type ResourceMetadata

type ResourceMetadata struct {
	Resource               string   `json:"resource"`
	AuthorizationServers   []string `json:"authorization_servers"`
	BearerMethodsSupported []string `json:"bearer_methods_supported"`
	ScopesSupported        []string `json:"scopes_supported,omitempty"`
	// DPoPSigningAlgValuesSupported are the DPoP proof algorithms the
	// provider's resources accept (RFC 9728 section 2); a separate
	// resource server sets its own.
	DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"`
}

ResourceMetadata is the RFC 9728 document.

func ProtectedResourceMetadata

func ProtectedResourceMetadata(resource string, authorizationServers, scopes []string) (ResourceMetadata, error)

ProtectedResourceMetadata builds an RFC 9728 document for a resource served apart from the provider, better-auth's getProtectedResourceMetadata: the user scopes (openid, profile, email, offline_access) are not a resource's and are refused (ErrBadScopes).

type ResourcePatch

type ResourcePatch struct {
	Name             *string
	AccessTTL        **int64
	RefreshTTL       **int64
	AllowedScopes    *[]string
	CustomClaims     *map[string]any
	DPoPRequired     *bool
	Disabled         *bool
	SigningAlgorithm *string
	SigningKeyID     *string
	Metadata         *json.RawMessage
}

ResourcePatch is a partial update of a resource, better-auth's PATCH /admin/oauth2/resources/:identifier: a nil field keeps the stored value. The identifier is the key and cannot change. A pointer to a nil slice or map (AllowedScopes, CustomClaims) clears it; Metadata set to JSON null clears the metadata.

type ResourceSeed

type ResourceSeed struct {
	Identifier string
	ResourcePatch
}

ResourceSeed is one resource SeedResources writes: its identifier and the policy fields the configuration names (a nil field is not named).

type RevokeRequest

type RevokeRequest struct {
	Token            string `json:"token"`
	TokenTypeHint    string `json:"token_type_hint"`
	ClientID         string `json:"client_id"`
	ClientSecret     string `json:"client_secret"`
	ClientAuthMethod string `json:"client_auth_method"`
	// ClientAssertion is a private_key_jwt assertion; Handler checks
	// client_assertion_type.
	ClientAssertion string `json:"client_assertion"`
	// ClientAssertionType and ClientCertificates are for an extension
	// ClientAuthMethod (an assertion of its type, mutual TLS).
	ClientAssertionType string              `json:"client_assertion_type,omitempty"`
	ClientCertificates  []*x509.Certificate `json:"-"`
}

RevokeRequest is RFC 7009 section 2.1, with the caller's client credentials.

type SecretEncryption

type SecretEncryption struct {
	Encrypt func(plaintext string) (string, error)
	Decrypt func(ciphertext string) (string, error)
}

SecretEncryption seals and opens a value the provider must be able to read back: client secrets under EncryptClientSecrets, cached token responses under RefreshReplay. Encrypt must be authenticated encryption (an AEAD such as AES-GCM, under a key from the app's secret store) so a changed ciphertext fails Decrypt instead of opening to something else.

type SecretHasher

type SecretHasher func(secret string, kind SecretKind) []byte

SecretHasher computes the stored form of a client secret, code, access token or refresh token, better-auth's storeClientSecret and storeTokens { hash }: an HMAC with a key from the app's secret store (a pepper), say, so a copy of the database alone cannot even confirm a guessed token. It must be deterministic, because tokens are looked up by their hash, and return exactly 32 bytes, the width of the hash columns on every engine (SHA-256's size). Changing it invalidates every stored secret.

type SecretKind

type SecretKind string

SecretKind names what a stored secret is, for a SecretHasher that hashes them differently.

const (
	SecretClient  SecretKind = "client_secret"
	SecretCode    SecretKind = "authorization_code"
	SecretAccess  SecretKind = "access_token"
	SecretRefresh SecretKind = "refresh_token"
)

The secrets the provider stores hashed.

type SeedMode

type SeedMode string

SeedMode is what SeedResources does with a resource that already has a row, better-auth's resourceSeedMode.

const (
	// SeedInsertOnly leaves an existing row alone, so an admin's edits
	// survive a restart. The default.
	SeedInsertOnly SeedMode = "insertOnly"
	// SeedMerge sets the fields the seed names and keeps the others.
	SeedMerge SeedMode = "merge"
	// SeedOverwrite replaces the whole policy with the seed's, unnamed
	// fields cleared: the configuration is the single source of truth.
	SeedOverwrite SeedMode = "overwrite"
)

The seed modes. A missing row is inserted in every mode.

type SelectAccountGate

type SelectAccountGate struct {
	// Page is the picker, a same-site path or a URL on a trusted origin.
	Page string
	// ShouldRedirect, when set, sends the user to Page without
	// prompt=select_account too.
	ShouldRedirect func(ctx context.Context, info GateInfo) bool
}

SelectAccountGate is better-auth's selectAccount: the account picker a signed-in user is sent to for prompt=select_account, or whenever ShouldRedirect says so.

type Signer

type Signer interface {
	Sign(ctx context.Context, in jwt.SignRequest) (jwt.TokenResult, error)
	PublicKeys() jwt.JWKS
}

Signer signs the provider's ID tokens and JWT access tokens and lists the public keys the provider publishes at <base>/jwks, so a relying party can verify what Sign produced. *jwt.Plugin is the default and satisfies it; a KMS, an HSM or a key service an app already runs can stand in, without registering the jwt plugin or serving its token route.

The contract a custom Signer must keep, because relying parties check every part of it: the token's iss is SignRequest's issuer, which New verifies at start-up by signing a probe and comparing it with Issuer; the claims are sub, aud, iat, exp and the request's Claims, with typ the request's Type ("" is "JWT"); the header's kid names a key PublicKeys lists, and the first listed key's alg is the one ID tokens use, because at_hash is computed with its hash. Sign is called from trusted code only.

type SignupRedirectFunc

type SignupRedirectFunc func(ctx context.Context, info GateInfo) (redirect bool, page string)

SignupRedirectFunc is better-auth's signup.shouldRedirect: true sends a signed-in user to more registration steps, at page or, when page is "", SignupPage.

type TokenFieldsInfo

type TokenFieldsInfo struct {
	GrantType string
	ClientID  string
	Scopes    []string
	// UserID is "" for a client's own token.
	UserID string
}

TokenFieldsInfo is what TokenResponseFields receives.

type TokenGenerator

type TokenGenerator func() string

TokenGenerator returns the random part of a new token (after its prefix), better-auth's generateOpaqueAccessToken and generateRefreshToken: at least 22 characters from a cryptographic source, or the issuance fails with ErrShortGenerated.

type TokenRequest

type TokenRequest struct {
	GrantType    string `json:"grant_type"`
	Code         string `json:"code"`
	RedirectURI  string `json:"redirect_uri"`
	CodeVerifier string `json:"code_verifier"`
	RefreshToken string `json:"refresh_token"`
	// DeviceCode is the RFC 8628 device_code, for the device grant served
	// through WithGrant.
	DeviceCode string `json:"device_code"`
	// DPoPProof is the request's DPoP header; Handler fills it.
	DPoPProof string `json:"dpop_proof"`
	Scope     string `json:"scope"`
	// Resource is the RFC 8707 resource; at a code exchange or refresh it
	// must be the one the grant was issued for.
	Resource     string `json:"resource"`
	ClientID     string `json:"client_id"`
	ClientSecret string `json:"client_secret"`
	// ClientAuthMethod is how the client authenticated: AuthMethodBasic,
	// AuthMethodPost or AuthMethodNone. It must be the client's registered
	// method.
	ClientAuthMethod string `json:"client_auth_method"`
	// ClientAssertion is a private_key_jwt assertion; Handler checks
	// client_assertion_type.
	ClientAssertion string `json:"client_assertion"`
	// ClientAssertionType and ClientCertificates are for an extension
	// ClientAuthMethod (an assertion of its type, mutual TLS).
	ClientAssertionType string              `json:"client_assertion_type,omitempty"`
	ClientCertificates  []*x509.Certificate `json:"-"`
}

TokenRequest is a token request, RFC 6749 sections 4.1.3, 4.4.2 and 6. Handler fills the client credentials from the Authorization header or the form, and Method with how they came.

type TokenResponse

type TokenResponse struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	ExpiresIn   int64  `json:"expires_in"`
	// ExpiresAt is when the access token expires, in Unix seconds,
	// better-auth's expires_at.
	ExpiresAt    int64  `json:"expires_at,omitempty"`
	RefreshToken string `json:"refresh_token,omitempty"`
	IDToken      string `json:"id_token,omitempty"`
	Scope        string `json:"scope"`
	// Extra are the TokenResponseFields additions; a standard member's
	// name is ignored.
	Extra map[string]any `json:"-"`
}

TokenResponse is the token endpoint's answer, RFC 6749 section 5.1. The tokens are plain strings: this is the one place they are meant to be seen.

func (TokenResponse) MarshalJSON

func (r TokenResponse) MarshalJSON() ([]byte, error)

MarshalJSON writes the standard members and, beside them, Extra.

type TrustedClient

type TrustedClient struct {
	ClientSpec
	// ClientID is the client_id; required.
	ClientID string
	// Secret is the client secret of a confidential client (held only as
	// its hash); a public one (token_endpoint_auth_method none) has none.
	Secret string
}

TrustedClient is a client defined in Go and held in memory, never stored, better-auth's cachedTrustedClients: a first-party app that ships with the server. The client operations cannot change or delete it (ErrForbidden from the admin ones, unknown to the owner ones), and it shadows a stored client with the same client_id.

type UpdateClientRequest

type UpdateClientRequest struct {
	ClientID string         `json:"client_id"`
	Metadata ClientMetadata `json:"metadata"`
	// ClientCredentialsScopes, when set, replaces the client_credentials
	// ceiling; it needs ActionConfigureCCScopes, like CredentialsScopes at
	// creation.
	ClientCredentialsScopes *[]string `json:"client_credentials_scopes,omitempty"`
}

UpdateClientRequest replaces an owned client's metadata. The authentication method cannot change, as in better-auth: a public client that became confidential would have no secret.

type UpdateConsentRequest

type UpdateConsentRequest struct {
	ID    string `json:"id"`
	Scope string `json:"scope"`
}

UpdateConsentRequest replaces a consent's scopes. Every scope must be one the client may request; narrowing does not revoke tokens already issued.

type UserInfoRequest

type UserInfoRequest struct {
	AccessToken string `json:"access_token"`
	// Scheme, DPoPProof, Method and URL describe how the token was
	// presented, for a DPoP-bound one; "" Scheme is Bearer. Handler fills
	// them.
	Scheme    string `json:"-"`
	DPoPProof string `json:"-"`
	Method    string `json:"-"`
	URL       string `json:"-"`
	// Certificate is the connection's TLS client certificate, for a token
	// bound to one.
	Certificate *x509.Certificate `json:"-"`
}

UserInfoRequest carries the access token, from the Authorization header or the form (Handler refuses both at once).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL