oauth

package
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: 35 Imported by: 0

README

github.com/ubgo/auth/plugin/oauth

Lets people sign in with Google, GitHub, Apple or any OpenID Connect issuer (Okta, Auth0, Keycloak, Entra ID, Zitadel), link those accounts to an existing user, sign in from a native app with the provider's ID token, sign out at the provider too, and optionally keep the provider's tokens for calling its API later.

Install

go get github.com/ubgo/auth

The package is part of the core module github.com/ubgo/auth. It adds no dependency beyond the core's own (github.com/ubgo/sqlb, golang.org/x/crypto): provider calls use net/http and encoding/json from the standard library.

Wire it

From examples/oauth:

a, err := auth.New(
	auth.WithDB(db, sqlb.SQLiteDialect{}),
	auth.WithBaseURL(baseURL),
	auth.WithPlugins(
		email.New(email.Login(), email.Recover(), email.WithMailer(m)),
		password.New(),
		oauth.New(oauth.WithProviders(oauth.GitHub(os.Getenv("GITHUB_CLIENT_ID"), os.Getenv("GITHUB_CLIENT_SECRET")))),
	),
)
// ...
mux.Handle("/auth/", authhttp.Handler(a, authhttp.Prefix("/auth")))

The plugin works alone (it is a sign-in method by itself), but with the email plugin a provider that vouches for a verified address that is already a verified identity signs in to that account instead of creating a second one. Register BaseURL + "/auth/oauth/<provider>/callback" as the redirect URI at each provider.

auth.New fails when:

Condition Error
No provider passed ErrNoProviders: "oauth: no providers"
A provider name does not match auth.KindNamePattern (^[a-z][a-z0-9_]{0,30}$) "oauth: provider name %q must match ..."
Neither auth.WithBaseURL nor oauth.CallbackURL is set ErrNoCallbackURL: "oauth: callback URLs need auth.WithBaseURL or oauth.CallbackURL"
StoreTokens got a key that is not auth.SealKeySize (32) bytes, or no key while auth.WithSecret is not set ErrBadTokenKey: "oauth: StoreTokens needs a 32-byte key"
An *OAuth2 lacks AuthURL, TokenURL (unless GetToken replaces the exchange), ClientID or a profile source (FetchProfile, or IDToken.ProfileFromToken), names an unknown TokenAuth, or is a PublicClient with a ClientSecret or ClientSecretFunc ErrProviderIncomplete: "oauth: a provider needs AuthURL, TokenURL (or GetToken), ClientID, FetchProfile (or IDToken.ProfileFromToken) and a known TokenAuth (no secret for a PublicClient)"
An *OIDCProvider has a Fallback with some endpoint set but no AuthURL, TokenURL or UserInfoURL ErrFallbackIncomplete
AllowProviderInitiated or RestartProviderInitiated names a provider that is not configured, both name the same one, or AllowProviderInitiated names an *OAuth2 without an IDToken key set or a custom provider that is not an InitiatedExchanger ErrProviderInitiatedConfig
SkipBrokenProviders is set and every provider was left out ErrAllProvidersBroken, wrapping each provider's reason
An IDTokenConfig has a JWKSURL but no Issuers, or ProfileFromToken without a JWKSURL; an *OIDCProvider with a hand-set IDToken.JWKSURL and no Issuers ErrIDTokenConfig: "oauth: IDToken needs JWKSURL and Issuers (ProfileFromToken needs both)"
ErrorURL is not a path on this site or a trusted origin ErrBadReturnTo (from Init)
Neither auth.WithBaseURL nor oauth.PostLogoutURL is set ErrNoCallbackURL

Built-in providers

The library reads no environment variables; the names below are the ones examples/oauth uses.

Constructor Route segment Needs Scopes Endpoints Example env
oauth.Google(clientID, clientSecret) google OAuth client id and secret from Google Cloud openid email profile GoogleAuthURL, GoogleTokenURL, GoogleUserInfoURL GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
oauth.GitHub(clientID, clientSecret) github OAuth app client id and secret read:user user:email GitHubAuthURL, GitHubTokenURL, GitHubAPIURL (/user, /user/emails) GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
oauth.OIDC(name, issuer, clientID, clientSecret) name (you choose, e.g. okta) Issuer URL plus client id and secret openid email profile Discovered from issuer + "/.well-known/openid-configuration" at the first start; jwks_uri and end_session_endpoint too OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET
oauth.Apple(clientID, secret, audiences...) apple The Services ID as client id; secret from oauth.AppleClientSecret(teamID, clientID, keyID, oauth.ParseAppleKey(p8)); the iOS bundle ids as extra audiences none; oauth.AppleWithEmail (same arguments) requests email name and takes Apple's form post (see below) AppleAuthURL, AppleTokenURL, AppleJWKSURL, issuer AppleIssuer APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_KEY_FILE
oauth.Microsoft(tenant, clientID, clientSecret) microsoft Entra ID app registration; tenant is a tenant id or domain, or common, organizations, consumers openid email profile MicrosoftAuthURLFormat, MicrosoftTokenURLFormat, MicrosoftUserInfoURL; subject oid; ID tokens for a single tenant MICROSOFT_TENANT, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET
oauth.Discord, oauth.Facebook, oauth.Spotify, oauth.Dropbox, oauth.Atlassian, oauth.Kakao, oauth.Naver, oauth.Kick, oauth.Linear, oauth.Polar, oauth.Vercel, oauth.Patreon, oauth.Gumroad, oauth.HubSpot, oauth.Yandex (clientID, clientSecret) the lowercase name OAuth client id and secret from the provider the provider's defaults (in each constructor's doc) Exported <Name>AuthURL, <Name>TokenURL, <Name>ProfileURL constants; the secret in the form —
oauth.X, oauth.Zoom, oauth.Figma, oauth.Cloudflare (clientID, clientSecret); oauth.Reddit(clientID, clientSecret, userAgent); oauth.PayPal(clientID, clientSecret, sandbox) x, zoom, figma, cloudflare, reddit, paypal As above; Reddit also needs a User-Agent naming the app the provider's defaults As above, with TokenAuth: ClientSecretBasic because these token endpoints take HTTP Basic; PayPal's sandbox switches to the sandbox endpoints —
oauth.Slack, oauth.LinkedIn, oauth.Twitch, oauth.LINE, oauth.HuggingFace, oauth.Roblox, oauth.Paybin, oauth.Railway (clientID, clientSecret); oauth.GitLab, oauth.Salesforce, oauth.Okta, oauth.Keycloak, oauth.Zitadel (issuer, clientID, clientSecret); oauth.Auth0(domain, …); oauth.Cognito(region, userPoolID, …) the lowercase name Client id and secret; the issuer where it is yours openid email profile (Roblox openid profile; Twitch adds user:read:email and the claims request its userinfo needs) oauth.OIDC over the issuer constants SlackIssuer, LinkedInIssuer, GitLabIssuer, TwitchIssuer, LINEIssuer, HuggingFaceIssuer, RobloxIssuer, PaybinIssuer, RailwayIssuer, SalesforceIssuer / SalesforceSandboxIssuer —
oauth.TikTok(clientKey, clientSecret), oauth.Notion(clientID, clientSecret), oauth.VK(clientID, clientSecret), oauth.WeChat(appID, appSecret, lang) tiktok, notion, vk, wechat TikTok: the client key (sent as client_key, better-auth's clientKey) and secret of a Login Kit for Web app. Notion: a public integration's OAuth client id and secret. VK ID: the app id and secret. WeChat: a website app's AppID and AppSecret, and the QR page language (WeChatLangCN, the default for "", or WeChatLangEN) TikTok user.info.basic (comma-joined); Notion none (owner=user); VK email phone; WeChat snsapi_login TikTokAuthURL, TikTokTokenURL, TikTokUserInfoURL, TikTokRevokeURL; NotionAuthURL, NotionTokenURL (JSON body, HTTP Basic), NotionMeURL with Notion-Version NotionVersion; VKAuthURL, VKTokenURL (with the callback's device_id and state), VKUserInfoURL (posted form); WeChatAuthURL (appid, #wechat_redirect), WeChatTokenURL and WeChatRefreshURL (GETs with appid and secret), WeChatUserInfoURL TIKTOK_CLIENT_KEY, NOTION_CLIENT_ID, VK_CLIENT_ID, WECHAT_APP_ID and their secrets
oauth.Dub(clientID, clientSecret), oauth.Instagram(appID, appSecret), oauth.Snapchat(clientID, clientSecret), oauth.Zoho(region, clientID, clientSecret) dub, instagram, snapchat, zoho Dub: an OAuth app's client id and secret (better-auth's dub plugin oauth option). Instagram: the app id and secret of a Meta app with Instagram API with Instagram Login (professional accounts only). Snapchat: a Login Kit app's OAuth client id and secret. Zoho: a client from the Zoho API console and its data centre, a oauth.ZohoRegion (ZohoUS, ZohoEU, ZohoIndia, ZohoAustralia, ZohoJapan, ZohoUK, ZohoCanada, ZohoSaudi, ZohoChina; ZohoRegionValues lists them) Dub user.read; Instagram instagram_business_basic (comma-joined); Snapchat user.external_id user.display_name user.bitmoji.avatar as full URLs under SnapchatScopePrefix; Zoho openid email profile DubAuthURL, DubTokenURL, DubUserInfoURL; InstagramAuthURL, InstagramTokenURL, InstagramMeURL; SnapchatAuthURL, SnapchatTokenURL (HTTP Basic), SnapchatRevokeURL, SnapchatMeURL (GraphQL {me{externalId displayName bitmoji{avatar}}}); Zoho discovered from the region's issuer DUB_CLIENT_ID, INSTAGRAM_APP_ID, SNAPCHAT_CLIENT_ID, ZOHO_CLIENT_ID and their secrets

Apple has no userinfo endpoint, so its profile comes from the ID token the token endpoint returns, verified against Apple's keys (IDToken.ProfileFromToken); its client secret is a signed assertion, renewed every day by AppleClientSecret. oauth.Apple requests no scopes, so its web flow yields the subject alone; the native flow (the id-token route) carries the email when the person shared it. oauth.AppleWithEmail requests email and name, which Apple answers by response_mode=form_post: a cross-site POST to the callback URL, which carries no SameSite=Lax flow cookie. The plugin therefore serves POST at every form-post provider's callback route as a relay that answers a redirect to the GET callback with the same code, state and user fields; the browser follows it as a top-level GET, the one cross-site request the flow cookie travels with, and the GET callback checks the state as always. The email comes from the ID token (a private relay address when the person hid theirs); the name comes from the user field Apple sends once, on first consent, read by oauth.ApplePostedUser into Profile.Name (and into Profile.Email, unverified, only when the token had none). Register the same callback URL at Apple either way.

The presets' shapes follow each provider's documented API and what better-auth v1.7.7 reads, pinned by fixture tests (TestPresets_ProfileReaders), not by calls to the live provider. A preset marks the email verified only when the provider's answer says so: Discord (verified), Dropbox, Atlassian, Kakao, Patreon, Zoom (verified), X (a confirmed address) and every OpenID Connect issuer with email_verified do; Facebook, Spotify, Figma, Naver, Kick, Linear, Cloudflare, Gumroad, HubSpot, Yandex and Microsoft (Graph's userinfo has no flag) do not, so their addresses never link to an existing user by themselves. TrustedProviders("facebook", "linear") trusts the ones you choose. Reddit returns no email. oauth.Presets lists every route segment. Google One Tap and the Google mobile SDKs: set g.IDToken = oauth.GoogleIDTokens(audiences...) on oauth.Google (keys at GoogleJWKSURL, both Google issuers), or oauth.GoogleIDTokensFor(hostedDomain, …) to refuse tokens outside one Workspace domain.

TikTok, Notion and WeChat run without PKCE (NoPKCE, below), because their web flows document none and better-auth sends none: the state stays single use and bound to the browser, and the client secret still guards the exchange, but a code stolen in transit could be injected into another browser's callback. VK ID runs PKCE. TikTok and WeChat send no email, so their users are created with no identity; Notion's and VK's addresses are unverified. WeChat's token endpoints are GETs that carry the app secret in the query string, as WeChat documents them; keep that host out of any proxy that logs URLs. VK ID's refresh grant also needs the device_id, which the plugin does not keep after the callback, so Tokens cannot refresh a VK token: request a fresh sign-in when it expires, or implement oauth.Refresher on a provider of your own. Sources: each provider's own documentation, read on 2026-10-06 (TikTok's through the Internet Archive, since developers.tiktok.com answers 503), and better-auth v1.7.7's source; the shapes are pinned by TestTikTok_*, TestNotion_*, TestVK_* and TestWeChat_*, not by live calls.

Dub, Instagram, Snapchat and Zoho (2026-10-08) are the better-auth community and partner plugins @dub/better-auth, better-auth-instagram, better-auth-snapchat and better-auth-zoho. Dub and Snapchat run PKCE; Instagram runs without it (NoPKCE), as its documentation has none. None of Dub, Instagram or Snapchat sends an email, so their users are created with no identity; the community plugins invent <id>@instagram.com and <id>@snapchat.com instead, which these presets do not, because a made-up address would look like a real one. Instagram's token answer is not RFC 6749 (documented wrapped in a data array, also seen flat), so the preset reads either; the token is the one-hour short-lived one (InstagramTokenLifetime) and does not refresh, and force_reauth is an allowed per-request parameter. Dub's userinfo carries the authorized workspace, kept in Profile.Claims["workspace"]. Zoho signs in the users of the one data centre the client was registered in. Sources: each provider's documentation and the four npm packages, read on 2026-10-08; pinned by TestDub_*, TestInstagram_*, TestSnapchat_*, TestZohoRegions and TestPresetsP3_Constructors, not by live calls.

Any other provider: fill an *oauth.OAuth2 (ID, ClientID, ClientSecret, AuthURL, TokenURL, Scopes, FetchProfile), or implement the oauth.Provider interface (Name, AuthCodeURL, Exchange). Implementing oauth.Refresher enables token refresh in Tokens; implementing oauth.Discoverer makes the start operation call Discover first; implementing oauth.IDTokenVerifier gives the provider an id-token route and oauth.Logouter a logout route; oauth.ProfileFetcher lets Profile read the provider's profile with stored tokens, oauth.Revoker lets unlink revoke them, and oauth.IssuerChecker makes the callback require the RFC 9207 iss. *OAuth2 and *OIDCProvider implement all seven, plus oauth.Labeler (the display name), oauth.Loader (what SkipBrokenProviders loads at start) and oauth.InitiatedExchanger (provider-initiated sign-in). Prove a provider of your own with the conformance kit oauthtest.Run (oauthtest); to reuse a goth provider instead, wrap it with authgoth.Wrap from the separate module contrib/goth. oauth.FormPoster (*OAuth2 with FormPost) gets the POST relay at its callback route and oauth.PostedProfile (*OAuth2 with PostedUser) is handed the callback's user field. GitHub's email comes from /user/emails, the address GitHub marks primary, with GitHub's own verified flag. Set TokenAuth, AuthParams, TokenParams, OptionalScopes, TokenLifetime, IDToken, EndSessionURL and ClientSecretFunc on an *oauth.OAuth2 as the provider needs; a Provider of your own implements oauth.ParamsProvider too to accept prompt, loginHint and extra scopes per request.

A provider whose authorization URL depends on the request that starts the flow implements oauth.RequestAuthURLer: start then calls AuthCodeURLForRequest(ctx, state, challenge, redirectURI, params) with the request's context instead of AuthCodeURL, so the provider can read a header or the host with auth.RequestHeader and auth.RequestHost. Request parameters still need a ParamsProvider. plugin/oauthproxy uses it for its per-request skip header.

Options

Option What it does Default
WithProviders(ps ...Provider) Adds providers; one start and one callback operation each none (required)
WithHTTPClient(hc *http.Client) Client for every provider call. Provider calls never follow a redirect (ErrRedirectRefused, logged; the flow fails with oauth.provider_failed), so a misconfigured or hostile endpoint cannot bounce the server to an internal address; a client with no CheckRedirect of its own is used as a copy with that policy &http.Client{Timeout: DefaultHTTPTimeout} (10s), refusing redirects
WithStateTTL(d time.Duration) How long a user may spend at the provider before the callback is refused DefaultStateTTL (10 minutes)
StoreTokensWith(s) StoreTokens with the tokens sealed by an auth.Sealer (Vault transit, AWS KMS) instead of an in-memory key; wins over auth.WithSealer, loses to a StoreTokens key. A seal error fails the sign-in; an open error is returned by Tokens off
StoreTokens(key ...[]byte) Keeps each account's access and refresh token, sealed with AES-256-GCM under key, or with no key under the app's auth.WithSealer when set, else the keys derived from auth.WithSecret (label auth.KeyLabelOAuthTokens, current secret first, so a rotated secret still opens old tokens); read back with Tokens off: provider tokens are never stored
CallbackURL(fn func(provider string) string) The redirect URI sent to each provider BaseURL + DefaultMountPrefix + "/oauth/<provider>/callback" (DefaultMountPrefix is /auth)
NoSignUp(providers ...string) Callbacks never create a user (for the named providers, or all when none is named): a linked account signs in, a verified address matching a verified identity links and signs in, a signed-in user may link; anything else is ErrSignUpDisabled off: strangers get an account
RequireVerifiedEmail() A sign-in or sign-up whose profile has no email or an unverified one is ErrEmailUnverified; link flows are unaffected off: an unverified address creates an unverified identity and signs in
RequireEmail() A sign-in or sign-up whose profile has no email at all is ErrEmailNotFound (oauth.email_not_found), before anything is written; an unverified address is admitted; link flows are unaffected off: a provider account without an email creates a user with no identity
VerifyEmailBeforeSignIn() better-auth's requireEmailVerification: the user is created or resolved as usual, but no session is issued while the user has no verified email; the browser lands on the error page (errorTo, ErrorURL, auth.WithErrorURL, else the landing page) with ?error=oauth.email_not_verified and the result has emailVerificationRequired: true; the id-token and provider-initiated routes are held the same way; no email at all is ErrEmailNotFound. The person verifies the address with plugin/email's public resend and verify routes and signs in again off
AllowUnlinkingAll() Unlink may remove the user's last sign-in method (better-auth's allowUnlinkingAll); the user can then come back only through account recovery off: the last method is auth.last_sign_in_method
LinkUnverifiedLocalEmail() better-auth's requireLocalEmailVerified: false: a verified provider address also links to a local account holding it unverified. Escape hatch: Init logs WarnLinkUnverifiedLocal at every start, because anyone can pre-register a victim's address with a password of their own and the victim's later provider sign-in lands in that account (pre-hijacking) off: such a link is oauth.account_exists
StateInCookie() better-auth's storeStateStrategy: "cookie": the state, PKCE verifier and flow data are sealed into the flow cookie (key derived from auth.WithSecret under auth.KeyLabelOAuthState), so a start writes no row; bound to the provider and the state TTL. Needs auth.WithSecret and refuses SkipStateCookieCheck (ErrStateCookieKey) off: a verification row
UpdateProfileOnSignIn() Every sign-in and link copies the profile's name and image onto the user (name cut to auth.MaxNameLength; an image only when https:// and within auth.MaxImageLength) off: the profile fills the user at creation only
NoImplicitLinking() The automatic link by verified email is off: a matching address is ErrAccountExists even when both sides are verified; people link from settings off: both-sides-verified matches link automatically
TrustedProviders(providers ...string) The named providers' addresses count as verified for linking even when the profile does not say so; the local identity must still be verified. Only for a provider you know verifies addresses: otherwise it is an account takeover none
RequireSameEmailOnLink() A link flow whose provider address is not one of the signed-in user's identities is ErrEmailMismatch off: a signed-in user may link any provider account
ErrorURL(path) A failed callback redirects the browser to this page on this site with the error code in the query (?error=oauth.denied, appended by the core's Call.RedirectOnError); a request may name another page with StartRequest.ErrorTo off: a failed callback answers the error as JSON
PostLogoutURL(fn func(provider string) string) The post_logout_redirect_uri sent with a provider logout; register it at the provider. A logout request's own returnTo replaces it for that request the base URL
ExplicitSignUp(providers ...string) A callback or id-token sign-in creates a user only when the request asked (signUp=true on start, "signUp":true on the id-token route); otherwise a stranger is ErrSignUpNotRequested. Linked accounts, verified-email matches and link flows are unaffected; NoSignUp wins where both apply off: a stranger signs up implicitly
ValidateProfile(fn ProfileHook) Runs before a callback or id-token sign-in writes anything, with the ProfileEvent: provider, profile (Claims included), the action (sign_in, sign_up, link), the user id (empty for a sign-up), the start's data, ProviderInitiated for a sign-in the provider started, and Reauthentication for a signed-in user confirming who they are with a linked account (the action is then sign_in, so a sign-in rule holds it too; it also runs on that callback and on an ID-token re-authentication). An *auth.Error it returns is answered as it is; any other error is logged and answered as ErrProfileRefused. Several run in order none
SkipBrokenProviders() A provider whose configuration fails its check, or whose discovery document or key set cannot be loaded, is logged at Error and left out (its routes, the providers list, every lookup by name); the others keep working. auth.New then loads every discovery document and key set at once, in parallel, through the Loader interface (*OAuth2 and *OIDCProvider implement it); a Fallback in use counts as loaded. A skipped provider comes back when the app restarts with it fixed. Every provider broken is still ErrAllProvidersBroken off: a bad configuration fails auth.New, and an unreachable issuer fails the flow that needed it (oauth.provider_failed)
SkipStateCookieCheck() A callback is accepted without the flow cookie matching its state; the state is still required, single use, and checked against its stored row, provider and expiry. better-auth's account.skipStateCookieCheck. It opens login CSRF: an attacker starts a flow, signs in at the provider as themself and sends the victim the callback URL, and the victim is signed in to the attacker's account. Init logs a warning at every start while it is on. Prefer authhttp's CookieDomain or the desktop hand-off for a callback the flow cookie cannot reach off: the flow cookie must match
AllowProviderInitiated(provider, more...) The named providers may start a sign-in themselves (a tile on an Okta, Entra ID or Workspace dashboard): a callback with a code and no state is exchanged without PKCE, and accepted only when the token response's ID token verifies (signature, issuer, audience, lifetime), carries no nonce, and names the profile's subject. The person is signed in only as the user already linked to that subject, or as a new user (NoSignUp, ExplicitSignUp, RequireVerifiedEmail and the hooks still apply, and ProfileEvent.ProviderInitiated is true); a matching address is ErrAccountExists, never a link; a signed-in browser is signed out first; it lands on /. Only for providers that verify ID tokens. Login forgery is the accepted risk: anyone holding a code for their own account can sign someone else's browser in as themselves off for every provider
RestartProviderInitiated(provider, more...) better-auth's allowIdpInitiated: for the named providers, a callback with a code and no state drops the code unread and answers an ordinary start in its place (fresh state bound to the browser, PKCE), so the browser returns to the provider, which signs a person who is signed in there straight through, and the next callback is checked as usual; it lands on /. No login-forgery risk, works for every provider, costs one more round trip. A provider may not be named by both modes off for every provider
OnResolved(fn ResolvedHook) Runs after the flow resolved its user, in the same transaction and before the session is issued, with the ResolvedEvent (the ProfileEvent plus Created and Linked); an error rolls the user back. For writing provider claims into the app's own columns with auth.SetUserColumn none
(always) Once a callback, id-token or provider-initiated sign-in resolved its user, or a re-authentication callback checked its account, the call is annotated (auth.Call.Annotate) under AnnotationAction (oauth.action) with an AccountAction: sign_in, sign_up, link (a signed-in user linked it) link_email (linked because the verified addresses matched) or reauthenticate (a re-authentication callback or ID token: nothing signed in, created or linked), so an After or Finally hook, plugin/audit's included, records what happened with no app wiring
WithIDTokenReplayGuard(g IDTokenReplayGuard) Replaces the guard that accepts each ID token once (native sign-in and ID-token re-authentication). Claim(ctx, IDTokenClaim) gets the claim: the hex SHA-256 of the token's signed part (header and payload, never the signature, which ES256 lets be spelled twice), the provider, and the token's exp plus 30 seconds; it returns nil the first time and ErrIDTokenReplayed after, exactly one of concurrent claims winning; any other error fails the request. IDTokenReplayGuardFunc adapts a function. nil is refused a verification row of purpose oauth.id_token_claim in the core's verification store, inside the operation's transaction, swept by auth sweep; auth.New refuses (ErrIDTokenReplayConfig) a store that cannot refuse a duplicate (auth.ExclusiveVerificationStore: the SQL table, memory, contrib/redis)
AllowIDTokenReplay() Turns the replay check off by name: a captured ID token signs in, or re-authenticates its user, until it expires. Logs WarnIDTokenReplayAllowed at start. Not with WithIDTokenReplayGuard (ErrIDTokenReplayConfig) off: every token is accepted once
WithServerContext(fn ServerContextFunc) better-auth's addOAuthServerContext: fn(ctx, call, provider) runs at every start and id-token sign-in and returns server-set values (a tenant id the request resolved), stored with the state where no client can set them and handed back in CallbackResult.ServerContext (Go only, never in the JSON) and ProfileEvent.ServerContext; several run in order, a later key wins; an error refuses the start; at most 1024 characters as JSON (ErrServerContextTooLong) none
ExposeAccessTokens() Adds POST /oauth/access-token, better-auth's getAccessToken from the client: the signed-in user's own access token, refreshed when due, with its expiry, granted scopes and ID token, never the refresh token. The risk: it moves a bearer credential for another service into the browser, where any script on the app's origin (one XSS) can read it and call the provider as the user until it expires; turn it on only when the page itself must call the provider. Needs StoreTokens (ErrExposeNeedsStoreTokens) off: provider tokens reach Go code only (Tokens)

Per-provider fields on *oauth.OAuth2 (and the same names on *oauth.OIDCProvider, copied onto the discovered endpoints):

Field What it does Default
TokenAuth How the token endpoint is authenticated to: ClientSecretPost (secret in the form), ClientSecretBasic (HTTP Basic, both halves form-encoded per RFC 6749 2.3.1), PublicClient (client_id only, PKCE alone; a ClientSecret with it fails auth.New), PrivateKeyJWT (an RFC 7523 assertion from ClientAssertion; a ClientSecret with it fails auth.New) ClientSecretPost
ClientAssertion func(ctx, tokenURL) (string, error) Signs the client assertion for PrivateKeyJWT: oauth.ClientAssertion(clientID, kid, key) signs ES256 with a P-256 key or RS256 with an RSA key, iss and sub the client id, aud the token endpoint, a fresh jti, valid 5 minutes. Required with PrivateKeyJWT, refused with any other TokenAuth (ErrAssertionConfig) none
FetchProfileFromTokens func(ctx, hc, TokenSet) (Profile, error) Reads the profile from the whole token set, TokenSet.Raw (the token endpoint's JSON) included, for a provider whose profile call needs more than the access token; wins over FetchProfile none
RefreshParams url.Values Added to refresh requests only, on top of TokenParams; the grant's own keys are ignored none
AuthorizationIssuer string The RFC 9207 iss every callback must carry; a callback without it or with another is ErrStateInvalid. Filled on an *OIDCProvider when discovery publishes authorization_response_iss_parameter_supported none: iss is not checked
RevokeURL string The RFC 7009 revocation endpoint; unlink revokes the stored tokens there (the refresh token when there is one, else the access token), best effort and logged. Discovered (revocation_endpoint) on an *OIDCProvider none: nothing is revoked
AuthParams url.Values Added to every authorization URL (prompt, access_type, hd, display, …); the protocol's own keys (state, client_id, redirect_uri, response_type, scope, code_challenge, code_challenge_method) are ignored none
TokenParams url.Values Added to every token and refresh request; the grant's own keys (grant_type, code, redirect_uri, code_verifier, refresh_token, client_id, client_secret) are ignored none
OptionalScopes []string Scopes a start may add through StartRequest.Scopes, for incremental authorization; any other requested scope is ErrBadScope none
OptionalParams []string Authorization parameters a start may set through StartRequest.Params (display, ui_locales, acr_values, …), better-auth's per-request additionalParams; any other is ErrBadParam, and the protocol's own (state, redirect_uri, …) never pass none
TokenLifetime time.Duration Access-token lifetime assumed when the token response has no expires_in, so Tokens can refresh in time 0: expiry unknown, no refresh
IDToken IDTokenConfig JWKSURL and Issuers make the provider accept ID tokens on its id-token route; Audiences are accepted aud values besides ClientID (an iOS bundle id); ProfileFromToken reads the profile from the token response's id_token instead of FetchProfile; RequireNonce refuses a token without a nonce claim; HostedDomain refuses a token whose Google hd claim is another Workspace domain. On an *OIDCProvider, JWKSURL and Issuers come from discovery when left empty zero: no ID tokens
EndSessionURL string The provider's RP-initiated logout endpoint, for the logout route; discovered on an *OIDCProvider none: logout answers ErrLogoutUnsupported
ClientSecretFunc func(ctx) (string, error) Computes the client secret per token request, for a provider whose secret is a signed assertion (AppleClientSecret) none: ClientSecret is sent
Headers http.Header Added to every token, refresh and revocation request (a User-Agent naming the app, an API version header); Authorization and Content-Type are ignored, so a header can never replace the credentials or mislabel the body. On an *OIDCProvider, DiscoveryHeaders go on the discovery request instead (Authorization ignored there too: the document is public) none
FormPost bool Sends response_mode=form_post; the provider's answer arrives as a POST at the callback route, which the plugin relays into the GET callback (see Apple above). AuthParams cannot set response_mode off: the answer is a GET query
PostedUser func(raw string, p *Profile) Reads the user field a form-post callback carries into the profile after the exchange (oauth.ApplePostedUser); the field comes from the browser, so a reader must never mark an address verified from it none: the field is ignored
GetToken func(ctx, code, verifier, redirectURI string) (TokenSet, error) Replaces the built-in code exchange, for a token endpoint that is not RFC 6749 (a GET, a JSON body): it gets the callback's code, the flow's PKCE verifier ("" on a provider-initiated sign-in) and the redirect URI. The state, PKCE, the profile read from the returned tokens, linking and the session stay the plugin's. Its error comes out of Exchange unchanged and the callback answers oauth.provider_failed, the reason logged; a set without an access token is a failed exchange; TokenLifetime fills a zero expiry. TokenURL is then needed only for Refresh none: the plugin posts the grant to TokenURL
DisplayName string The provider's name for people, listed by GET /oauth/providers for a sign-in page's buttons (Label() returns it, or the id when empty; a custom provider implements Labeler). Every preset sets one: Google, GitHub, Hugging Face, Amazon Cognito, … the id
NoPKCE bool Per provider, never global; better-auth's pkce: false: no code_challenge on the authorization URL and no code_verifier at the exchange, for a provider whose web flow has none and might refuse it (the TikTok, Notion and WeChat presets set it). The state stays single use and browser-bound and the secret still guards the exchange; what is lost is RFC 7636's defence against a code stolen in transit and injected into another browser's callback. Refused with PublicClient (ErrPublicClientPKCE), where PKCE is all that ties the code to the client off: PKCE S256 on every flow
ClientIDParam, ClientSecretParam string The names the client id and secret travel under on the authorization URL and in the token, refresh and revocation forms (TikTok client_key, better-auth's clientKey; WeChat appid and secret) client_id, client_secret
ScopeSeparator string, AuthURLFragment string How scopes are joined in the scope parameter (TikTok and WeChat take a comma), and a #fragment the authorization URL must end with (WeChat's wechat_redirect) a space; no fragment
CallbackParams []CallbackParam Callback query parameters the token request carries too: CallbackParamDeviceID (VK ID's device_id) and CallbackParamState; any other is ErrCallbackParam. A provider of your own reads the same values with oauth.CallbackValuesOf(ctx) in its Exchange (authgate sets them too) none: the RFC 6749 exchange
RefreshCallbackParams []CallbackParam Callback values the refresh grant needs again (VK ID's device_id): the exchange puts them in TokenSet.RefreshParams, StoreTokens keeps them sealed in refresh_params_sealed (migration 0004_account_refresh_params), and the plugin sends them through ParamRefresher.RefreshWithParams on every refresh none; oauth.VK sets device_id

On an *oauth.OIDCProvider only:

Field What it does Default
Fallback Endpoints Static endpoints (AuthURL, TokenURL, UserInfoURL required; JWKSURL, EndSessionURL, RevokeURL, IssParameterSupported optional) used ONLY while the discovery document cannot be fetched: a transport error, a status other than 2xx, or a body that is not JSON. Each use is logged at Warn, and discovery is tried again after one minute. A document that arrives but names another issuer or lacks an endpoint is still refused: the issuer answered, and what it said is wrong none: a failed discovery fails that flow with oauth.provider_failed
Logger *slog.Logger Receives the fallback warning; the plugin sets the core's logger when it is nil slog.Default() outside the plugin

oauth.GoogleProfileFor(url, hostedDomain) is GoogleProfile restricted to one Google Workspace domain: a userinfo whose hd claim differs is refused (ErrHostedDomain, seen by the user as oauth.provider_failed). The hd authorization parameter is only a hint; this is the enforcement. oauth.UserInfoProfile(url, subjectClaim) reads any OpenID-shaped userinfo answer and takes another claim as the subject (oid for Entra ID, user_id for PayPal); OIDCProvider.SubjectClaim does the same for a discovered issuer. Every built-in profile reader fills Profile.Claims with the raw answer, so a hook can read a provider-specific field.

oauth.MicrosoftPhoto(fetch, size) is better-auth's profilePhotoSize: it wraps a Microsoft FetchProfile so the profile image is the person's Graph photo (/me/photos/<size>x<size>/$value, MicrosoftPhotoURLFormat) as a data: URL; size is one of MicrosoftPhotoSizes (48 to 648, else ErrPhotoSize). A missing or unreadable photo, or one longer than MaxPhotoDataURL characters (the image column on MySQL), leaves the image as it was and never fails the sign-in; in practice the 48 and 64 sizes fit. Not wrapping it is better-auth's disableProfilePhoto, and our default.

Routes and Go API

Paths are under the mount prefix (/auth above). A Query is served as GET, a Mutation as POST.

Route Go API Requirement Rate limit
GET /oauth/<provider>/start?returnTo=/path&link=true operation oauth.StartEndpoint(provider) (oauth.<provider>-start); (*Plugin).Start(ctx, provider, StartRequest) from Go, with ctx carrying the request whose response gets the flow cookie Public (link=true, LinkFlag, needs a signed-in user) 30 per IP per minute (startIPLimit, startIPWindow)
GET /oauth/<provider>/start?prompt=consent&loginHint=a%40b.co&scopes=calendar+drive the same operation Public; prompt, loginHint and scopes (space-separated, each in the provider's OptionalScopes) need a ParamsProvider, which *OAuth2 and *OIDCProvider are same
GET /oauth/<provider>/start?params=display%3Dpopup the same operation Public; params is a query string of further parameters, each named in the provider's OptionalParams (OptionalParamList, the ParamsAllowLister interface), else oauth.bad_param
GET /oauth/<provider>/start?data=ref%3D42&errorTo=/oops the same operation Public; data (at most 1024 characters) comes back in CallbackResult.Data; errorTo is a path on this site a failed callback redirects to same
GET /oauth/<provider>/start?newUserTo=/welcome&signUp=true the same operation Public; newUserTo is where a callback that created the user lands instead of returnTo (same rules); signUp=true (SignUpFlag) is what ExplicitSignUp requires before a user is created same
GET /oauth/<provider>/callback operation oauth.CallbackEndpoint(provider) (oauth.<provider>-callback); (*Plugin).Callback(ctx, provider, CallbackRequest) from Go, with ctx carrying the request whose flow cookie is checked; a provider not in use is ErrProviderUnknown for both Public, plus the flow cookie set by start; iss must match the provider's AuthorizationIssuer when it has one; user is handed to a PostedProfile provider. For a provider named in AllowProviderInitiated or RestartProviderInitiated, a request with a code and no state is a provider-initiated sign-in instead (see Options) none; 30 per IP per minute for a provider named in either
POST /oauth/<provider>/callback (form body code, state, user, error) operation oauth.FormPostEndpoint(provider) (oauth.<provider>-form-post); only for a FormPoster Public; reads no cookie, writes nothing, and answers a redirect to the GET callback with the same fields (a user over 1024 characters is dropped), so the flow cookie reaches the real callback none
POST /oauth/<provider>/id-token {"idToken","nonce","signUp","accessToken"} operation oauth.IDTokenEndpoint(provider) (oauth.<provider>-id-token); only for an IDTokenVerifier Public; a signed-in caller links instead of signing in; an accessToken is taken only when the ID token's at_hash binds it (else oauth.id_token_invalid), and is stored under StoreTokens; each token is accepted once (oauth.id_token_replayed after) 30 per IP per minute
POST /oauth/<provider>/logout {"returnTo"} (LogoutRequest; its provider may be omitted here, and another provider's name is ErrLogoutProvider) operation oauth.LogoutEndpoint(provider) (oauth.<provider>-logout); only for a Logouter SignedIn; returnTo is optional, passes the same TrustsReturnURL rule as a sign-in's returnTo (a path is made absolute against the base URL), and is sent as post_logout_redirect_uri instead of PostLogoutURL; an untrusted one is ErrBadReturnTo and the session is not ended none
POST /oauth/logout {"provider":"okta","returnTo":"/bye"} operation oauth.EndpointLogout (oauth.logout); exists when any provider is a Logouter SignedIn; the per-provider route's code path, returnTo check included, for the provider named in the body. No provider is ErrLogoutProvider (nothing is guessed from the linked accounts); one that is not configured or has no logout is ErrLogoutUnsupported; the session survives every refusal. On a provider's own route provider may be omitted, and naming another is ErrLogoutProvider none
sign-in buttons (*Plugin).SignInMethods() (authhttp.UIMethod), read by authui's sign-in and sign-up pages One authhttp.SignInMethod per provider in use, in configuration order: ID oauth.<provider>, Kind oauth, label "Continue with ", Route the provider's GET start route; none for a provider skipped or whose start is switched off, none before auth.New none
GET /oauth/providers operation oauth.EndpointProviders; (*Plugin).Providers() from Go Public; {"providers":[{"id":"google","name":"Google"}]}, in configuration order (ProviderInfo), ids and display names only; a provider SkipBrokenProviders left out is not listed none
POST /oauth/unlink {"provider":"github","accountId":"acc_…"} (*Plugin).Unlink(ctx, provider); (*Plugin).UnlinkAccount(ctx, provider, accountID) for one of several accounts of a provider (accountId from /oauth/accounts; an id that is not the caller's is ErrNotLinked). Without an id the oldest account of the provider goes FreshSignIn none
GET /oauth/accounts operation oauth.EndpointAccounts; (*Plugin).Accounts(ctx, userID) for any user; each account carries scopes, the scopes the provider said it granted (with StoreTokens; [] otherwise), never a token SignedIn none
POST /oauth/reauthenticate {"provider":"google","returnTo":"/settings"} operation oauth.EndpointReauthenticate (oauth.reauthenticate); (*Plugin).Reauthenticate(ctx, in). Re-authentication with a linked provider: answers {"url"} (no redirect) and binds the flow to the current session; the provider's callback then makes that session fresh (auth.Reauthenticate, the second factor asked as at OAuth sign-in) and lands on returnTo with ?reauth=fresh or second_factor plus reauthTicket, never issuing a session. Any provider account not linked to the session's user is ErrReauthNotLinked, on errorTo (default returnTo) with ?error= SignedIn, origin-checked; refused for a pending session, an API key and an impersonation 30 per IP per minute, 10 per user per 15 minutes
POST /oauth/reauthenticate-id-token {"provider":"apple","idToken":"…","nonce":"…"} operation oauth.EndpointReauthenticateIDToken (oauth.reauthenticate-id-token), present when a provider verifies ID tokens; (*Plugin).ReauthenticateIDToken(ctx, in). Re-authentication for a native app that cannot follow a redirect: the ID token is verified as at native sign-in (verifyNative: key set, issuer, audience, lifetime, nonce rule, at_hash for an access token) and must name an account linked to the signed-in user (ErrReauthNotLinked otherwise; nothing linked or created). Answers auth.Reauthentication (fresh, or the second factor's ticket), never issuing a session. Each token is accepted once, shared with native sign-in (WithIDTokenReplayGuard), so a used token is oauth.id_token_replayed SignedIn; refused for a pending session, an API key and an impersonation before the token is verified 30 per IP per minute, 10 per user per 15 minutes
POST /oauth/access-token {"provider":"google"} operation oauth.EndpointAccessToken (oauth.access_token); only with ExposeAccessTokens. Answers {"accessToken","accessTokenExpiresAt","scopes","idToken"}, refreshed when due; a provider not linked or not configured is ErrNoTokens SignedIn, origin-checked; only the caller's own account 30 per IP per minute
Go only (*Plugin).Tokens(ctx, userID, provider) none (app code) none
Go only (*Plugin).ImportAccount(ctx, ImportAccount{UserID, Provider, ProviderAccountID, Email}): links an account from another system to an existing user without a sign-in, for an import; ErrProviderUnknown for a provider not configured, ErrAccountLinkedElsewhere when the provider account is already linked none (app code) none
Go only (*Plugin).Refresh(ctx, userID, provider): renews the stored tokens now, whatever their expiry (ErrCannotRefresh without a refresh token or a Refresher) none (app code) none
Go only (*Plugin).Profile(ctx, userID, provider): the provider's current profile read with the stored tokens, for a ProfileFetcher (ErrProfileUnsupported otherwise); nothing is written to the user none (app code) none

Start answers 302 to the provider and sets the flow cookie. Callback answers 302 to returnTo (default /) and, unless it was a link flow, issues a session. The id-token route answers the same CallbackResult as JSON with the session issued (a cookie, or Set-Auth-Token in bearer mode) and no redirect: it is for an app, not a browser following links; no provider tokens are stored on that path. The ID token is checked for signature (RS256, ES256 or EdDSA against the provider's key set, refetched for an unknown kid at most once a minute and at least once a day), iss, aud, exp and iat (with 30 seconds of leeway), and nonce: the claim must equal the request's nonce or its SHA-256 hex (Apple's native flow hashes it); a token with a nonce needs the request to carry one, and a request with a nonce needs the token to carry it. Logout ends the session here through the core's own sign-out, then answers 302 to the provider's end-session endpoint with id_token_hint (when StoreTokens kept one), client_id and post_logout_redirect_uri (the request's returnTo, else PostLogoutURL); a Go caller gets the URL in LogoutResult. Accounts are listed oldest first, and accounts linked in the same instant in id order, so every engine returns the same order. CallbackResult also reports Created, Linked and SecondFactorRequired. Tokens refreshes an access token that expires within RefreshMargin (1 minute) when the account has a refresh token and the provider is a Refresher. An API key is refused by unlink and accounts (auth.ErrAPIKeyRefused).

Tables and columns

The plugin owns auth_account (TableNameAccount, alias acc), created by migration 0001_account (owner oauth):

Column Constant Meaning
id ColAccountID Primary key, acc_...
user_id ColAccountUserID Owner; deleted with the user by a.DeleteUser (declared in UserTables; no foreign key)
provider ColAccountProvider Provider name, e.g. github
subject ColAccountSubject The provider's stable id for the person, never the email
email ColAccountEmail Address the provider reported, default ''
created_at ColAccountCreated When linked
access_token_sealed ColAccountAccess Sealed access token; '' unless StoreTokens
refresh_token_sealed ColAccountRefresh Sealed refresh token; '' unless StoreTokens
token_expires_at ColAccountExpires Access token expiry, NULL when unknown
token_scope ColAccountScope The scope the provider said it granted at the last exchange or refresh, '' when it did not say (migration 0003_account_token_scope)
refresh_token_expires_at ColAccountRefreshExpires When the refresh token expires, for providers that say (refresh_token_expires_in, refresh_expires_in); NULL when unknown (migration 0003)
id_token_sealed ColAccountIDToken Sealed ID token for id_token_hint; NULL unless StoreTokens and the provider returned one. Added by migration 0002_account_id_token

Constraints: UNIQUE (provider, subject), one provider account belongs to one user. Index auth_account_user_idx on user_id. Typed columns for queries are exported as AccountID, AccountUserID, and so on. These columns are not in the core's stable-column contract (stability_test.go covers auth_user, auth_identity and auth_session only), so treat them as readable but not frozen across minor versions.

Errors

Variable Code HTTP When
ErrStateInvalid oauth.state_invalid 400 Missing, foreign, spent or expired state; state from another provider; link flow finished by a different or signed-out user
ErrProviderDenied oauth.denied 400 The provider returned error (the user cancelled)
ErrProviderFailed oauth.provider_failed 502 Discovery or the code exchange failed; details are logged, never sent
ErrAccountExists oauth.account_exists 409 The provider address matches an identity but the provider did not verify it
ErrAccountLinkedElsewhere oauth.linked_elsewhere 409 The provider account belongs to another user
ErrNotLinked oauth.not_linked 404 Unlink of a provider the user has not linked
ErrReauthNotLinked oauth.reauth_not_linked 403 Re-authentication with a provider the user has no account at, or whose callback or ID token brought back a provider account not linked to the signed-in user (another user's, or one never linked)
ErrBadReturnTo oauth.bad_return_to 400 returnTo or errorTo is neither a path on this site, an absolute URL on a trusted origin, nor a deep link in a scheme auth.WithTrustedOrigins lists ("myapp://"); the rule is the core's TrustsReturnURL
ErrRefreshFailed oauth.refresh_failed 502 Tokens could not refresh an expiring token
ErrNoTokens oauth.no_tokens 404 Tokens without StoreTokens, without a linked account, or for an account linked before tokens were stored
ErrExchange none none Wrapped in every provider-call failure; logged, the client sees ErrProviderFailed
ErrHostedDomain none none GoogleProfileFor: the account's hd is not the required domain; wraps ErrExchange
ErrProviderIncomplete none none Configuration, returned by auth.New: an *OAuth2 lacking AuthURL, TokenURL, ClientID or FetchProfile, an unknown TokenAuth, or a PublicClient with a secret
ErrSignUpDisabled oauth.sign_up_disabled 403 NoSignUp is set and the callback would have created a user
ErrEmailUnverified oauth.email_unverified 403 RequireVerifiedEmail is set and the profile's email is missing or unverified (not in a link flow)
ErrBadScope oauth.bad_scope 400 StartRequest.Scopes names a scope outside the provider's OptionalScopes
ErrBadParam oauth.bad_param 400 StartRequest.Params does not parse, is too long, or names a parameter outside the provider's OptionalParams
ErrEmailNotFound oauth.email_not_found 403 RequireEmail or VerifyEmailBeforeSignIn is set and the profile carries no email (not in a link flow)
ErrEmailNotVerified oauth.email_not_verified 403 Never returned: the code a sign-in held by VerifyEmailBeforeSignIn lands with (?error=), beside emailVerificationRequired: true
ErrRequestParams oauth.params_unsupported 400 prompt, loginHint or scopes sent to a Provider that is not a ParamsProvider
ErrEmailMismatch oauth.email_mismatch 409 RequireSameEmailOnLink is set and the provider address is missing, unparsable, or not an identity of the signed-in user
ErrIDTokenReplayed oauth.id_token_replayed 401 The ID token was already accepted once, by a native sign-in or an ID-token re-authentication; the replay guard refused it and reported token.replay to the security stream
ErrIDTokenInvalid oauth.id_token_invalid 401 The ID token is malformed, uses an unlisted algorithm, names an unknown key, fails its signature, is expired or not yet valid, names another issuer or audience, or its nonce does not match; one answer for every reason, the reason is logged
ErrIDTokenUnsupported oauth.id_token_unsupported 400 The provider has no IDTokenConfig (an issuer whose discovery document has no jwks_uri)
ErrNonceRequired oauth.nonce_required 401 IDToken.RequireNonce is set and the token carries no nonce
ErrLogoutUnsupported oauth.logout_unsupported 404 Logout at a provider with no end-session endpoint; on the general route, a provider that is not configured or has no logout
ErrLogoutProvider oauth.logout_provider 400 The general logout route without a provider, or a provider's own route with another provider in the body
ErrDataTooLong oauth.data_too_long 400 StartRequest.Data is longer than 1024 characters
ErrSignUpNotRequested oauth.sign_up_not_requested 403 ExplicitSignUp applies and the flow would create a user without signUp=true
ErrProfileRefused oauth.profile_refused 403 A ValidateProfile hook refused with an error that is not an *auth.Error; the reason is logged
ErrCannotRefresh oauth.cannot_refresh 404 Refresh for an account with no refresh token, or at a provider that is not a Refresher
ErrProfileUnsupported oauth.profile_unsupported 404 Profile at a provider that is not a ProfileFetcher, or whose profile exists only in the exchange's ID token
ErrProviderUnknown, ErrImportSubject none none ImportAccount for a provider the plugin was not built with, or without a user id or provider account id
ErrAssertionConfig, ErrAssertionKey none none Configuration: PrivateKeyJWT without ClientAssertion or the reverse (returned by auth.New); a key for ClientAssertion that is neither ECDSA P-256 nor RSA
ErrJWKSFailed none none The provider's key set could not be read; the client sees ErrProviderFailed
ErrFallbackIncomplete, ErrProviderInitiatedConfig, ErrAllProvidersBroken, ErrIDTokenReplayConfig none none Configuration, returned by auth.New: WithIDTokenReplayGuard beside AllowIDTokenReplay, or the default replay guard over a verification store that cannot refuse a duplicate; an incomplete OIDC Fallback; AllowProviderInitiated on a provider that cannot verify ID tokens, or either provider-initiated mode on an unknown provider or both on one; every provider left out under SkipBrokenProviders
ErrIDTokenConfig, ErrAppleKey, ErrAppleSecret none none Configuration: a bad IDTokenConfig (returned by auth.New); a .p8 file that is not a P-256 PKCS #8 key; AppleClientSecret with an empty team id, client id, key id or a nil key
ErrPublicClientPKCE, ErrCallbackParam, ErrExposeNeedsStoreTokens, ErrPhotoSize none none Configuration: NoPKCE with PublicClient, or a CallbackParams entry outside CallbackParamValues (both returned by auth.New); ExposeAccessTokens without StoreTokens (returned by auth.New); MicrosoftPhoto with a size Graph does not serve
ErrServerContextTooLong none 500 A WithServerContext function returned more than 1024 characters as JSON; the start fails

Unlinking the last way into an account answers the core's auth.last_sign_in_method (409).

Security notes

Property Pinned by
PKCE S256 on every flow unless the provider sets NoPKCE (never with PublicClient); the verifier is sent at the exchange and differs from the challenge TestPKCEChallengeIsS256, TestOAuth_SignInCreatesUserThenLinksByVerifiedEmail, TestNoPKCE_LeavesTheChallengeAndVerifierOut, TestValidate_NoPKCEAndCallbackParams
The client secret never appears in the browser URL TestAuthCodeURL_CarriesStateAndChallenge
A callback is accepted only in the browser that started it (flow cookie), the login-CSRF defence TestOAuth_CallbackNeedsTheFlowCookie
State is single-use, bound to its provider, and expires TestTokensOAuth_CallbackRefusals
Automatic linking only when both the provider address and the existing identity are verified TestOAuth_UnverifiedProviderEmailDoesNotLink, TestOAuth_SignInCreatesUserThenLinksByVerifiedEmail
A provider account never moves to a second user, including under a concurrent callback TestTokensOAuth_ReturningLinkingAndUnlinking, TestTokensOAuth_LosingARaceIsLinkedElsewhere
returnTo accepts a same-site path, a trusted origin or a trusted deep-link scheme, and nothing else (no //host, no backslash) TestSafeReturnTo, the core's TrustsReturnURL tests
OIDC discovery refuses a document naming another issuer TestOIDC_DiscoversAndChecksTheIssuer
Stored tokens are sealed and bound to their account row; a changed key or tampered column fails to open (auth.ErrSealedCorrupt) TestOAuth_StoreTokensSealsAndReadsBack, TestTokensOAuth_Tokens
A Sealer (StoreTokensWith, auth.WithSealer) seals and opens in that order of precedence below a StoreTokens key, binds each token to its account, and its errors fail the write or the read, never yield an empty token TestStoreTokens_SealerPrecedence, TestStoreTokens_SealerErrorsFailClosed
Without a key, tokens seal under the app secret's derived key; a rotated secret keeps them opening, a dropped one does not, an explicit key wins TestStoreTokens_DerivesTheKeyFromTheAppSecret, TestTokensOAuth_StoreTokensUnderTheAppSecret
Unlink never removes the last way in TestOAuth_UnlinkRefusesTheLastWayIn
Re-authentication refreshes the session it was started on, with no new session or token, only with a provider account linked to that session's user; another user's account, an unlinked one, a replayed state, another session, an impersonation and a signed-out callback are refused, and the second factor is asked as at OAuth sign-in TestOAuthReauth_RefreshesTheSameSession, TestOAuthReauth_OnlyALinkedAccountOfThisUser, TestOAuthReauth_RefusedCallers, TestOAuthReauth_AsksTheSecondFactorAsSignInDoes
Each ID token is accepted once: a replayed one is oauth.id_token_replayed (401 over HTTP), issues nothing and reaches the security stream as token.replay; a token refused by verification is not spent; of concurrent posts of one token exactly one signs in; the claim survives a sweep until exp plus leeway; a key-value verification store refuses it too; WithIDTokenReplayGuard is asked with the key, provider and expiry and its refusal or failure stops the call; AllowIDTokenReplay lets a token in twice; both together, a nil guard and a store that keeps duplicates fail auth.New TestOAuthIDTokenReplay_SignInOnce, TestOAuthIDTokenReplay_ConcurrentSignIn, TestOAuthIDTokenReplay_ClaimHeldUntilExpiry, TestOAuthIDTokenReplay_SwapAndEscapeHatch, TestOAuthReauthIDToken_RealVerification, TestIDTokenKey, TestIDTokenUntil
ID-token re-authentication refreshes the bearer or cookie session it is posted on, with no new session or token, only with a verified token of an account linked to that user; another user's token, an unlinked account, another audience, issuer or key, an expired token, a nonce mismatch, an unbound access token, a pending session and an impersonation are refused, the second factor is asked with the oauth method, and refused proofs reach the security stream TestOAuthReauthIDToken_OnlyALinkedAccountOfThisUser, TestOAuthReauthIDToken_RealVerification, TestOAuthReauthIDToken_RefusedCallers, TestOAuthReauthIDToken_AsksTheSecondFactor, TestOAuthReauthIDToken_HTTPBearerAndCookie
Every database failure on each flow reaches the caller TestTokensOAuth_Faults, TestTokensOAuth_NativeFaults
An ID token verifies only under its own algorithm and a listed one (none and HMAC are refused), with its issuer, audience, lifetime and nonce checked; a tampered payload fails TestIDToken_VerifiesEveryAcceptedAlgorithm, TestIDToken_Rejections, TestIDToken_Nonce
An unknown kid refetches the key set at most once a minute, so forged tokens cannot flood the provider TestIDToken_KeySetRefreshIsBounded
The profile taken from an id_token at the exchange is bound to the flow by the PKCE challenge as nonce TestExchange_ProfileFromIDTokenUsesTheChallengeAsNonce
Apple's client secret verifies under the .p8 key and is renewed before it expires TestApple_KeyAndClientSecret
The id-token route creates, signs in or links under the callback's rules, refuses a wrong token or nonce, and is rate-limited per IP TestTokensOAuth_IDTokenSignIn, TestTokensOAuth_IDTokenRespectsSignUpAndEmailSwitches, TestTokensOAuth_IDTokenRateLimit
Logout ends the session here before sending the browser to the provider, and a provider that cannot be reached does not keep the session alive TestTokensOAuth_Logout
A logout's returnTo is sent only when trusted, a path becomes absolute against the base URL (refused when there is none), and an untrusted one leaves the session alive TestTokensOAuth_LogoutReturnTo, TestLogoutReturn_PathNeedsBaseURL
Every built-in provider shape passes the conformance kit: Google, GitHub, *OAuth2 with userinfo, with an ID-token profile, as a public client and with a GetToken hook, oauth.OIDC (also on its Fallback) and the Okta, Keycloak, GitLab and Zitadel presets, Discord and Zoom TestConformance_* in oauthtest
A ValidateProfile refusal writes nothing, and a failing OnResolved rolls the new user back; both hooks run on the id-token route too TestTokensOAuth_ProfileHooks
RequireEmail refuses an email-less profile before writing; VerifyEmailBeforeSignIn keeps the user, issues no session until the address is verified on the callback, id-token and provider-initiated routes, and signs in after verification TestOAuth_RequireEmail, TestOAuth_VerifyEmailBeforeSignIn (integration, three engines)
An id-token accessToken is stored only when at_hash binds it (the OpenID Connect Core example vector included) TestBindAccessToken, TestOAuth_IDTokenAccessToken
params forwards only allow-listed names, never a protocol parameter; provider calls refuse redirects on the default and an app client TestStartParams_AllowListed, TestProviderCalls_RefuseRedirects
AllowUnlinkingAll lets the last method go; the default refuses TestOAuth_AllowUnlinkingAll
Under ExplicitSignUp a stranger is refused without the flag, on the redirect and the id-token route, and NoSignUp still wins TestTokensOAuth_ExplicitSignUp
A failed callback without ErrorURL or errorTo lands on the core's auth.WithErrorURL page, and still answers JSON when none is set TestTokensOAuth_FailedCallbackFallsBackToTheCoreErrorURL
A callback whose iss is missing or another issuer's is refused when the provider announces one (RFC 9207) TestTokensOAuth_NewUserToAndIssuerCheck
The client assertion verifies under the registered key, names the token endpoint as audience, carries a fresh jti, and a TokenParams entry cannot replace it TestClientAssertion_ES256AndRS256, TestTokenAuth_PrivateKeyJWT
Unlink revokes the stored tokens at the provider and stands when the provider is down; the refresh token is preferred TestTokensOAuth_UnlinkRevokes, TestRevoke_RFC7009
A Google ID token from another Workspace domain is refused under HostedDomain TestGoogleIDTokens_HostedDomain
An imported account signs its user in, never a second user, and an already-linked provider account is refused TestTokensOAuth_ImportAccount
Every database failure on hooks, Refresh, Profile, ImportAccount and a revoking unlink reaches the caller TestTokensOAuth_Round3Faults
The general logout route names its provider (never guessed), shares the per-provider route's returnTo check, and every refusal leaves the session alive TestTokensOAuth_GeneralLogoutRoute, TestLogoutAny_RefusesWithoutGuessing
The discovery fallback is used only while the document is unreachable, never for a document naming another issuer or lacking an endpoint, and discovery is retried after the interval TestFallback_*, TestConformance_OIDCFallback
SkipBrokenProviders leaves out exactly the providers that fail their check, discovery or key set, from routes, lists and lookups; without it nothing is fetched at auth.New and a bad configuration still fails it TestSkipBroken_*
GetToken replaces the exchange without bypassing PKCE: it is handed the flow's verifier, its error comes back unchanged, and the conformance kit passes through it TestGetToken_*, TestConformance_OAuth2GetToken, TestTokensOAuth_ProvidersFallbackSkipAndGetTokenOverHTTP
A provider-initiated sign-in happens only for a provider named in AllowProviderInitiated (a capable provider that was not named, and every other provider, still refuse a state-less callback), verifies the ID token with no nonce allowed and the profile's subject bound to it, never links by address, and replaces the browser's session only once the person is known TestTokensOAuth_ProviderInitiatedSignIn, TestExchangeInitiated_*, TestVerifyInitiated_NonceRules, TestAllowProviderInitiated_Validation
RestartProviderInitiated never exchanges the tile's code: it answers a fresh flow bound to the browser, whose callback is checked as usual TestTokensOAuth_RestartProviderInitiated, TestRestartProviderInitiated_Validation
Server context is set by the server only, crosses the redirect intact, reaches the hooks and the callback's Go result, and never the callback's JSON TestTokensOAuth_ServerContextCrossesTheRedirect, TestTokensOAuth_ServerContextFailures
The access-token route exists only with ExposeAccessTokens and StoreTokens, needs a session, answers only the caller's own token, is rate-limited per IP, and never sends the refresh token; the accounts list shows granted scopes and no token TestTokensOAuth_ExposeAccessTokens, TestTokensOAuth_AccessTokenRouteIsRateLimited, TestTokensOAuth_AccountsListGrantedScopes, TestTokensOAuth_Round6Faults
The callback's device_id and state reach a provider's exchange (VK ID) on the plugin and on authgate TestTokensOAuth_CallbackValuesReachTheExchange, TestGate_CallbackValuesReachTheExchange
A wired plugin offers one GET button per provider in configuration order, labelled by display name and pointing at the start route; a provider whose start is switched off gets none; an unwired plugin offers nothing TestSignInMethods_OneButtonPerProvider

Limits, honestly: on the redirect flow the profile comes from the userinfo endpoint with the access token (OpenID Connect makes it authoritative), so the id_token is checked only when IDToken.ProfileFromToken is set. The key-set fetch ignores the provider's cache headers and uses its own bounds. The id-token route stores no provider tokens. RFC 9207 iss on the callback is checked when the provider announces it (AuthorizationIssuer, see the options table). Provider responses are read up to maxProviderBody (1 MiB). Two callers refreshing at once with a provider that rotates refresh tokens can make the slower one get ErrRefreshFailed; it should call again. Start is rate-limited per process unless the app passes auth.WithRateStore.

Example

Documentation

Overview

Package oauth signs users in with identity providers such as Google and GitHub. It owns auth_account, one row per linked provider account.

state and PKCE are always on. The state is a single-use core verification token that is also bound to the browser through the flow effect, so a callback is accepted only in the browser that started it — the defence against login CSRF. Linking to an existing account happens automatically only when both sides are verified; otherwise the user signs in first and links from settings.

Index

Constants

View Source
const (
	//ds:def id=appleauthurl-t2vj6kcd owner=@khanakia stability=stable
	AppleAuthURL = "https://appleid.apple.com/auth/authorize"
	//ds:def id=appletokenurl-n8wq4rzm owner=@khanakia stability=stable
	AppleTokenURL = "https://appleid.apple.com/auth/token"
	//ds:def id=applejwksurl-b5xk7pqt owner=@khanakia stability=stable
	AppleJWKSURL = "https://appleid.apple.com/auth/keys"
	// AppleIssuer is the iss of every Apple ID token and the aud of the
	// client-secret assertion.
	//ds:def id=appleissuer-m4zr9hvw owner=@khanakia stability=stable
	AppleIssuer = "https://appleid.apple.com"
)

Apple endpoints (Sign in with Apple REST API).

View Source
const (
	EndpointUnlink auth.EndpointID = "oauth.unlink"
	//ds:def id=endpointaccounts-j8bwb2tj owner=@khanakia stability=stable
	EndpointAccounts auth.EndpointID = "oauth.accounts"
	// EndpointLogout is the general provider logout, POST /logout with the
	// provider named in the body; it exists when any provider is a
	// Logouter, beside each one's own /<provider>/logout.
	//ds:def id=endpointlogout-h3wq8nzk owner=@khanakia stability=stable
	EndpointLogout auth.EndpointID = "oauth.logout"
	// EndpointProviders lists the configured providers' ids and display
	// names, GET /providers, public.
	//ds:def id=endpointproviders-p6xk2wtm owner=@khanakia stability=stable
	EndpointProviders auth.EndpointID = "oauth.providers"
)

Operation ids. Start and callback ids are per provider: "oauth.<provider>-start" and "oauth.<provider>-callback".

View Source
const (
	//ds:def id=tablenameaccount-wkmv8x3c owner=@khanakia stability=stable
	TableNameAccount  = "auth_account"
	TableAliasAccount = "acc"
	//ds:def id=colaccountid-7gm3bq4c owner=@khanakia stability=stable
	ColAccountID = "id"
	//ds:def id=colaccountuserid-hn74aaqn owner=@khanakia stability=stable
	ColAccountUserID = "user_id"
	//ds:def id=colaccountprovider-bdjpuq2m owner=@khanakia stability=stable
	ColAccountProvider = "provider"
	//ds:def id=colaccountsubject-e25223he owner=@khanakia stability=stable
	ColAccountSubject = "subject"
	//ds:def id=colaccountemail-un48a32q owner=@khanakia stability=stable
	ColAccountEmail = "email"
	//ds:def id=colaccountcreated-r3epfyb7 owner=@khanakia stability=stable
	ColAccountCreated = "created_at"
	// The token columns are empty unless StoreTokens is set.
	//ds:def id=colaccountaccess-dxys2my7 owner=@khanakia stability=stable
	ColAccountAccess = "access_token_sealed"
	//ds:def id=colaccountrefresh-mjswm325 owner=@khanakia stability=stable
	ColAccountRefresh = "refresh_token_sealed"
	//ds:def id=colaccountexpires-wg2zr8ha owner=@khanakia stability=stable
	ColAccountExpires = "token_expires_at"
	// ColAccountIDToken is the sealed ID token, NULL unless StoreTokens is
	// set and the provider returned one; added by migration 0002.
	//ds:def id=colaccountidtoken-5w2kjn8p owner=@khanakia stability=stable
	ColAccountIDToken = "id_token_sealed"
	// ColAccountScope is the scope the provider said it granted at the last
	// exchange or refresh, ” when it did not say; migration 0003.
	//ds:def id=colaccountscope-q7mt3xkd owner=@khanakia stability=stable
	ColAccountScope = "token_scope"
	// ColAccountRefreshExpires is when the refresh token expires, NULL when
	// the provider did not say; migration 0003.
	//ds:def id=colaccountrefreshexpires-b4nw8ztp owner=@khanakia stability=stable
	ColAccountRefreshExpires = "refresh_token_expires_at"
	// ColAccountRefreshParams holds the sealed TokenSet.RefreshParams (VK
	// ID's device_id), NULL for every other provider; migration 0004.
	//ds:def id=colaccountrefreshparams-3nq8wk5x owner=@khanakia stability=stable
	ColAccountRefreshParams = "refresh_params_sealed"
)

The plugin's table.

View Source
const (
	// DefaultStateTTL bounds how long a user may spend at the provider. Ten
	// minutes covers a password manager and a second factor there; a longer
	// window only lengthens the life of a leaked state.
	//ds:def id=defaultstatettl-zsf722py owner=@khanakia stability=stable
	DefaultStateTTL = 10 * time.Minute
	// DefaultHTTPTimeout bounds each call to a provider, so a hung provider
	// fails the callback instead of holding the request open.
	DefaultHTTPTimeout = 10 * time.Second
	// DefaultMountPrefix is where the default callback URL assumes the auth
	// handler is mounted.
	//ds:def id=defaultmountprefix-4xrrxrkb owner=@khanakia stability=stable
	DefaultMountPrefix = "/auth"
)

Defaults.

View Source
const (
	//ds:def id=dubauthurl-p3d7kx2m owner=@khanakia stability=stable
	DubAuthURL = "https://app.dub.co/oauth/authorize"
	//ds:def id=dubtokenurl-p3d4wq8n owner=@khanakia stability=stable
	DubTokenURL = "https://api.dub.co/oauth/token"
	//ds:def id=dubuserinfourl-p3d9mx3k owner=@khanakia stability=stable
	DubUserInfoURL = "https://api.dub.co/oauth/userinfo"
)

Dub endpoints (dub.co/docs/integrations/quickstart, read 2026-10-08), the ones @dub/better-auth's oauth option configures.

View Source
const (
	//ds:def id=instagramauthurl-p3i2kw7q owner=@khanakia stability=stable
	InstagramAuthURL = "https://www.instagram.com/oauth/authorize"
	//ds:def id=instagramtokenurl-p3i8nx4t owner=@khanakia stability=stable
	InstagramTokenURL = "https://api.instagram.com/oauth/access_token"
	// InstagramMeURL is the Graph API profile of the token's account, with
	// the fields the preset reads.
	//ds:def id=instagrammeurl-p3i5qk9w owner=@khanakia stability=stable
	InstagramMeURL = "https://graph.instagram.com/me?fields=id,user_id,username,name,profile_picture_url,account_type"
	// InstagramTokenLifetime is the life of the short-lived token the code
	// exchange returns, which carries no expires_in (one hour, as Instagram
	// documents it).
	//ds:def id=instagramtokenlifetime-p3i3wn6k owner=@khanakia stability=stable
	InstagramTokenLifetime = time.Hour
)

Instagram endpoints: the Instagram API with Instagram Login (developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login, read 2026-10-08), the ones better-auth-instagram configures.

View Source
const (
	//ds:def id=snapchatauthurl-p3s8wk2n owner=@khanakia stability=stable
	SnapchatAuthURL = "https://accounts.snapchat.com/accounts/oauth2/auth"
	//ds:def id=snapchattokenurl-p3s3nx7q owner=@khanakia stability=stable
	SnapchatTokenURL = "https://accounts.snapchat.com/accounts/oauth2/token"
	//ds:def id=snapchatrevokeurl-p3s6qw4k owner=@khanakia stability=stable
	SnapchatRevokeURL = "https://accounts.snapchat.com/accounts/oauth2/revoke"
	//ds:def id=snapchatmeurl-p3s9kx5t owner=@khanakia stability=stable
	SnapchatMeURL = "https://kit.snapchat.com/v1/me"
	// SnapchatScopePrefix is how Snapchat names a scope: the full URL.
	//ds:def id=snapchatscopeprefix-p3s2wn8x owner=@khanakia stability=stable
	SnapchatScopePrefix = "https://auth.snapchat.com/oauth2/api/"
)

Snapchat endpoints: Login Kit's server-side OAuth2 flow (developers.snap.com/snap-kit/login-kit/Tutorials/web, read 2026-10-08) and the Snap Kit /v1/me GraphQL read better-auth-snapchat uses.

View Source
const (
	//ds:def id=googleauthurl-ekqypyzt owner=@khanakia stability=stable
	GoogleAuthURL = "https://accounts.google.com/o/oauth2/v2/auth"
	//ds:def id=googletokenurl-ktw4f4uv owner=@khanakia stability=stable
	GoogleTokenURL = "https://oauth2.googleapis.com/token"
	//ds:def id=googleuserinfourl-5nkm522q owner=@khanakia stability=stable
	GoogleUserInfoURL = "https://openidconnect.googleapis.com/v1/userinfo"
	// GoogleJWKSURL publishes the keys Google signs ID tokens with.
	//ds:def id=googlejwksurl-h4tm9wqz owner=@khanakia stability=stable
	GoogleJWKSURL = "https://www.googleapis.com/oauth2/v3/certs"
)

Google endpoints.

View Source
const (
	//ds:def id=githubauthurl-etwyp97v owner=@khanakia stability=stable
	GitHubAuthURL = "https://github.com/login/oauth/authorize"
	//ds:def id=githubtokenurl-2ww26mbk owner=@khanakia stability=stable
	GitHubTokenURL = "https://github.com/login/oauth/access_token"
	//ds:def id=githubapiurl-5sv8a6v5 owner=@khanakia stability=stable
	GitHubAPIURL = "https://api.github.com"
)

GitHub endpoints.

View Source
const (
	//ds:def id=slackissuer-k2wq8tnm owner=@khanakia stability=stable
	SlackIssuer = "https://slack.com"
	//ds:def id=linkedinissuer-x7mq3bkz owner=@khanakia stability=stable
	LinkedInIssuer = "https://www.linkedin.com/oauth"
	//ds:def id=gitlabissuer-p4nt9wqr owner=@khanakia stability=stable
	GitLabIssuer = "https://gitlab.com"
	//ds:def id=twitchissuer-v8kx2mqn owner=@khanakia stability=stable
	TwitchIssuer = "https://id.twitch.tv/oauth2"
	//ds:def id=lineissuer-h3zr7xkt owner=@khanakia stability=stable
	LINEIssuer = "https://access.line.me"
	//ds:def id=huggingfaceissuer-b6wn4pqm owner=@khanakia stability=stable
	HuggingFaceIssuer = "https://huggingface.co"
	//ds:def id=robloxissuer-t9mk5xqw owner=@khanakia stability=stable
	RobloxIssuer = "https://apis.roblox.com/oauth"
	//ds:def id=salesforceissuer-n2qt8wkr owner=@khanakia stability=stable
	SalesforceIssuer = "https://login.salesforce.com"
	// SalesforceSandboxIssuer is the issuer of Salesforce sandbox orgs.
	//ds:def id=salesforcesandboxissuer-m5xk3nqz owner=@khanakia stability=stable
	SalesforceSandboxIssuer = "https://test.salesforce.com"
	// PaybinIssuer and RailwayIssuer were read from their discovery
	// documents on 2026-10-03: both publish a userinfo endpoint, S256 and
	// client_secret_post; Railway also announces the RFC 9207 iss parameter.
	//ds:def id=paybinissuer-k8wq3xtn owner=@khanakia stability=stable
	PaybinIssuer = "https://idp.paybin.io"
	//ds:def id=railwayissuer-m4nt7xkq owner=@khanakia stability=stable
	RailwayIssuer = "https://backboard.railway.com"
)

OpenID Connect issuers. Each preset is OIDC with the issuer's own default scopes; every one of them publishes a discovery document with a userinfo endpoint.

View Source
const (
	//ds:def id=microsoftauthurlformat-x4kn8wqt owner=@khanakia stability=stable
	MicrosoftAuthURLFormat = "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize"
	//ds:def id=microsofttokenurlformat-m7wq2xkn owner=@khanakia stability=stable
	MicrosoftTokenURLFormat = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
	//ds:def id=microsoftjwksurlformat-t2kq9wnx owner=@khanakia stability=stable
	MicrosoftJWKSURLFormat = "https://login.microsoftonline.com/%s/discovery/v2.0/keys"
	//ds:def id=microsoftissuerformat-k8xn3wqm owner=@khanakia stability=stable
	MicrosoftIssuerFormat = "https://login.microsoftonline.com/%s/v2.0"
	//ds:def id=microsoftuserinfourl-w3qt7xkn owner=@khanakia stability=stable
	MicrosoftUserInfoURL = "https://graph.microsoft.com/oidc/userinfo"
)

Microsoft Entra ID endpoints. The tenant is a tenant id or domain, or one of the multi-tenant aliases common, organizations and consumers.

View Source
const (
	//ds:def id=discordauthurl-n8kq4wxt owner=@khanakia stability=stable
	DiscordAuthURL = "https://discord.com/oauth2/authorize"
	//ds:def id=discordtokenurl-q3wn7xkm owner=@khanakia stability=stable
	DiscordTokenURL = "https://discord.com/api/oauth2/token"
	//ds:def id=discorduserurl-x6tk2wqn owner=@khanakia stability=stable
	DiscordUserURL = "https://discord.com/api/users/@me"
)

Discord endpoints.

View Source
const (
	//ds:def id=facebookauthurl-w7kn3xqt owner=@khanakia stability=stable
	FacebookAuthURL = "https://www.facebook.com/v24.0/dialog/oauth"
	//ds:def id=facebooktokenurl-t4xq8wkn owner=@khanakia stability=stable
	FacebookTokenURL = "https://graph.facebook.com/v24.0/oauth/access_token"
	//ds:def id=facebookprofileurl-m2wk9xqn owner=@khanakia stability=stable
	FacebookProfileURL = "https://graph.facebook.com/v24.0/me?fields=id,name,email,picture.type(large)"
)

Facebook endpoints (Graph API v24.0).

View Source
const (
	//ds:def id=spotifyauthurl-k3wq7xnt owner=@khanakia stability=stable
	SpotifyAuthURL = "https://accounts.spotify.com/authorize"
	//ds:def id=spotifytokenurl-x9kn2wqt owner=@khanakia stability=stable
	SpotifyTokenURL = "https://accounts.spotify.com/api/token"
	//ds:def id=spotifyprofileurl-n6tq4wkx owner=@khanakia stability=stable
	SpotifyProfileURL = "https://api.spotify.com/v1/me"
)

Spotify endpoints.

View Source
const (
	//ds:def id=dropboxauthurl-t7kq3wxn owner=@khanakia stability=stable
	DropboxAuthURL = "https://www.dropbox.com/oauth2/authorize"
	//ds:def id=dropboxtokenurl-m9xn8wqk owner=@khanakia stability=stable
	DropboxTokenURL = "https://api.dropboxapi.com/oauth2/token"
	//ds:def id=dropboxprofileurl-q2wt4xkn owner=@khanakia stability=stable
	DropboxProfileURL = "https://api.dropboxapi.com/2/users/get_current_account"
)

Dropbox endpoints.

View Source
const (
	//ds:def id=xauthurl-k6wn2xqt owner=@khanakia stability=stable
	XAuthURL = "https://x.com/i/oauth2/authorize"
	//ds:def id=xtokenurl-n4kq9wxt owner=@khanakia stability=stable
	XTokenURL = "https://api.x.com/2/oauth2/token"
	//ds:def id=xprofileurl-w8tk3xqn owner=@khanakia stability=stable
	XProfileURL = "https://api.x.com/2/users/me?user.fields=profile_image_url,confirmed_email"
)

X (Twitter) endpoints.

View Source
const (
	//ds:def id=redditauthurl-t3xk7wqn owner=@khanakia stability=stable
	RedditAuthURL = "https://www.reddit.com/api/v1/authorize"
	//ds:def id=reddittokenurl-m8wq4xkt owner=@khanakia stability=stable
	RedditTokenURL = "https://www.reddit.com/api/v1/access_token"
	//ds:def id=redditprofileurl-x2nk9wqt owner=@khanakia stability=stable
	RedditProfileURL = "https://oauth.reddit.com/api/v1/me"
)

Reddit endpoints.

View Source
const (
	//ds:def id=atlassianauthurl-k4nq8wxt owner=@khanakia stability=stable
	AtlassianAuthURL = "https://auth.atlassian.com/authorize?audience=api.atlassian.com&prompt=consent"
	//ds:def id=atlassiantokenurl-q9xk3wnt owner=@khanakia stability=stable
	AtlassianTokenURL = "https://auth.atlassian.com/oauth/token"
	//ds:def id=atlassianprofileurl-w2tk7xqn owner=@khanakia stability=stable
	AtlassianProfileURL = "https://api.atlassian.com/me"
)

Atlassian endpoints.

View Source
const (
	//ds:def id=zoomauthurl-x8kq2wnt owner=@khanakia stability=stable
	ZoomAuthURL = "https://zoom.us/oauth/authorize"
	//ds:def id=zoomtokenurl-t5wn9xkq owner=@khanakia stability=stable
	ZoomTokenURL = "https://zoom.us/oauth/token"
	//ds:def id=zoomprofileurl-m3xk6wqt owner=@khanakia stability=stable
	ZoomProfileURL = "https://api.zoom.us/v2/users/me"
)

Zoom endpoints.

View Source
const (
	//ds:def id=figmaauthurl-w9kn4xqt owner=@khanakia stability=stable
	FigmaAuthURL = "https://www.figma.com/oauth"
	//ds:def id=figmatokenurl-k2xq8wnt owner=@khanakia stability=stable
	FigmaTokenURL = "https://api.figma.com/v1/oauth/token"
	//ds:def id=figmaprofileurl-n6wt3xkq owner=@khanakia stability=stable
	FigmaProfileURL = "https://api.figma.com/v1/me"
)

Figma endpoints.

View Source
const (
	//ds:def id=kakaoauthurl-x4wq7knt owner=@khanakia stability=stable
	KakaoAuthURL = "https://kauth.kakao.com/oauth/authorize"
	//ds:def id=kakaotokenurl-m8kn3xqw owner=@khanakia stability=stable
	KakaoTokenURL = "https://kauth.kakao.com/oauth/token"
	//ds:def id=kakaoprofileurl-q2tx9wkn owner=@khanakia stability=stable
	KakaoProfileURL = "https://kapi.kakao.com/v2/user/me"
)

Kakao endpoints.

View Source
const (
	//ds:def id=naverauthurl-w3kq8xnt owner=@khanakia stability=stable
	NaverAuthURL = "https://nid.naver.com/oauth2.0/authorize"
	//ds:def id=navertokenurl-t9xn2wkq owner=@khanakia stability=stable
	NaverTokenURL = "https://nid.naver.com/oauth2.0/token"
	//ds:def id=naverprofileurl-n5wk7xqt owner=@khanakia stability=stable
	NaverProfileURL = "https://openapi.naver.com/v1/nid/me"
)

Naver endpoints.

View Source
const (
	//ds:def id=kickauthurl-k8wn4xqt owner=@khanakia stability=stable
	KickAuthURL = "https://id.kick.com/oauth/authorize"
	//ds:def id=kicktokenurl-x3kq7wnt owner=@khanakia stability=stable
	KickTokenURL = "https://id.kick.com/oauth/token"
	//ds:def id=kickprofileurl-m7tx2wkq owner=@khanakia stability=stable
	KickProfileURL = "https://api.kick.com/public/v1/users"
)

Kick endpoints.

View Source
const (
	//ds:def id=linearauthurl-w5kq3xnt owner=@khanakia stability=stable
	LinearAuthURL = "https://linear.app/oauth/authorize"
	//ds:def id=lineartokenurl-n8xk7wqt owner=@khanakia stability=stable
	LinearTokenURL = "https://api.linear.app/oauth/token"
	//ds:def id=lineargraphqlurl-q4wn2xkt owner=@khanakia stability=stable
	LinearGraphQLURL = "https://api.linear.app/graphql"
)

Linear endpoints.

View Source
const (
	//ds:def id=cloudflareauthurl-k3nq8wxt owner=@khanakia stability=stable
	CloudflareAuthURL = "https://dash.cloudflare.com/oauth2/auth"
	//ds:def id=cloudflaretokenurl-t7wk2xqn owner=@khanakia stability=stable
	CloudflareTokenURL = "https://dash.cloudflare.com/oauth2/token"
	//ds:def id=cloudflareprofileurl-m2xq9wkt owner=@khanakia stability=stable
	CloudflareProfileURL = "https://api.cloudflare.com/client/v4/user"
)

Cloudflare endpoints.

View Source
const (
	//ds:def id=paypalauthurl-q7kn4wxt owner=@khanakia stability=stable
	PayPalAuthURL = "https://www.paypal.com/signin/authorize"
	//ds:def id=paypaltokenurl-x2wq8knt owner=@khanakia stability=stable
	PayPalTokenURL = "https://api-m.paypal.com/v1/oauth2/token"
	//ds:def id=paypalprofileurl-n9tk3xwq owner=@khanakia stability=stable
	PayPalProfileURL = "https://api-m.paypal.com/v1/identity/openidconnect/userinfo?schema=openid"
	//ds:def id=paypalsandboxauthurl-k4xq7wnt owner=@khanakia stability=stable
	PayPalSandboxAuthURL = "https://www.sandbox.paypal.com/signin/authorize"
	//ds:def id=paypalsandboxtokenurl-w6nk2xqt owner=@khanakia stability=stable
	PayPalSandboxTokenURL = "https://api-m.sandbox.paypal.com/v1/oauth2/token"
	//ds:def id=paypalsandboxprofileurl-t3wq9xkn owner=@khanakia stability=stable
	PayPalSandboxProfileURL = "https://api-m.sandbox.paypal.com/v1/identity/openidconnect/userinfo?schema=openid"
)

PayPal endpoints, live and sandbox.

View Source
const (
	//ds:def id=polarauthurl-x8wk3nqt owner=@khanakia stability=stable
	PolarAuthURL = "https://polar.sh/oauth2/authorize"
	//ds:def id=polartokenurl-k2tq7wxn owner=@khanakia stability=stable
	PolarTokenURL = "https://api.polar.sh/v1/oauth2/token"
	//ds:def id=polarprofileurl-n6xk9wqt owner=@khanakia stability=stable
	PolarProfileURL = "https://api.polar.sh/v1/oauth2/userinfo"
)

Polar endpoints.

View Source
const (
	//ds:def id=vercelauthurl-t9kq4wxn owner=@khanakia stability=stable
	VercelAuthURL = "https://vercel.com/oauth/authorize"
	//ds:def id=verceltokenurl-q3xn8wkt owner=@khanakia stability=stable
	VercelTokenURL = "https://api.vercel.com/login/oauth/token"
	//ds:def id=vercelprofileurl-m7wq2xkn owner=@khanakia stability=stable
	VercelProfileURL = "https://api.vercel.com/login/oauth/userinfo"
)

Vercel endpoints.

View Source
const (
	//ds:def id=patreonauthurl-w2kq9xnt owner=@khanakia stability=stable
	PatreonAuthURL = "https://www.patreon.com/oauth2/authorize"
	//ds:def id=patreontokenurl-x7nk4wqt owner=@khanakia stability=stable
	PatreonTokenURL = "https://www.patreon.com/api/oauth2/token"
	//ds:def id=patreonprofileurl-n3wq8xkt owner=@khanakia stability=stable
	PatreonProfileURL = "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=email,full_name,image_url,is_email_verified"
)

Patreon endpoints.

View Source
const (
	//ds:def id=gumroadauthurl-k9wn3xqt owner=@khanakia stability=stable
	GumroadAuthURL = "https://gumroad.com/oauth/authorize"
	//ds:def id=gumroadtokenurl-t4kq8wxn owner=@khanakia stability=stable
	GumroadTokenURL = "https://api.gumroad.com/oauth/token"
	//ds:def id=gumroadprofileurl-m8xq2wkt owner=@khanakia stability=stable
	GumroadProfileURL = "https://api.gumroad.com/v2/user"
)

Gumroad endpoints.

View Source
const (
	//ds:def id=hubspotauthurl-w7xk4nqt owner=@khanakia stability=stable
	HubSpotAuthURL = "https://app.hubspot.com/oauth/authorize"
	//ds:def id=hubspottokenurl-n2kq9wxt owner=@khanakia stability=stable
	HubSpotTokenURL = "https://api.hubapi.com/oauth/v1/token"
	// HubSpotTokenInfoURL is the endpoint that describes an access token,
	// the token itself appended; it is the only place HubSpot tells who the
	// token belongs to.
	//ds:def id=hubspottokeninfourl-q6wn3xkt owner=@khanakia stability=stable
	HubSpotTokenInfoURL = "https://api.hubapi.com/oauth/v1/access-tokens/"
)

HubSpot endpoints.

View Source
const (
	//ds:def id=yandexauthurl-k4nq7wxt owner=@khanakia stability=stable
	YandexAuthURL = "https://oauth.yandex.ru/authorize"
	//ds:def id=yandextokenurl-x9wk3nqt owner=@khanakia stability=stable
	YandexTokenURL = "https://oauth.yandex.ru/token"
	//ds:def id=yandexprofileurl-m3tq8wkn owner=@khanakia stability=stable
	YandexProfileURL = "https://login.yandex.ru/info?format=json"
)

Yandex endpoints.

View Source
const (
	ParamReauth        = "reauth"
	ParamReauthTicket  = "reauthTicket"
	ReauthFresh        = "fresh"
	ReauthSecondFactor = "second_factor"
)

The landing page's query after a re-authentication callback, beside what ReturnTo carried: ParamReauth is ReauthFresh or ReauthSecondFactor, and with the latter ParamReauthTicket is the ticket the second factor's re-authentication operation takes (twofactor.reauthenticate). The ticket is single use, bound to this session and valid for auth.PendingSessionTTL, so in the URL it is worth what an authorization code is. A refused callback lands on ErrorTo, or on ReturnTo when the start named none, with `?error=<code>`.

View Source
const (
	//ds:def id=tiktokauthurl-r6kq2wxn owner=@khanakia stability=stable
	TikTokAuthURL = "https://www.tiktok.com/v2/auth/authorize/"
	//ds:def id=tiktoktokenurl-m3xw8kqt owner=@khanakia stability=stable
	TikTokTokenURL = "https://open.tiktokapis.com/v2/oauth/token/"
	//ds:def id=tiktokrevokeurl-h7nq4xwk owner=@khanakia stability=stable
	TikTokRevokeURL = "https://open.tiktokapis.com/v2/oauth/revoke/"
	// TikTokUserInfoURL asks for the user.info.basic fields only: open_id
	// (the subject), union_id, the large avatar and the display name.
	// TikTok has no email.
	//ds:def id=tiktokuserinfourl-t2wk9xqm owner=@khanakia stability=stable
	TikTokUserInfoURL = "https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_large_url,display_name"
)

TikTok endpoints (Login Kit for Web; developers.tiktok.com/doc/login-kit-web, oauth-user-access-token-management, tiktok-api-v2-get-user-info).

View Source
const (
	//ds:def id=notionauthurl-k5wq8xnt owner=@khanakia stability=stable
	NotionAuthURL = "https://api.notion.com/v1/oauth/authorize"
	//ds:def id=notiontokenurl-x3nk7wqm owner=@khanakia stability=stable
	NotionTokenURL = "https://api.notion.com/v1/oauth/token"
	//ds:def id=notionmeurl-q8tw2kxn owner=@khanakia stability=stable
	NotionMeURL = "https://api.notion.com/v1/users/me"
	// NotionVersion is the Notion-Version header the profile call sends,
	// the version better-auth v1.7.7 pins.
	//ds:def id=notionversion-n4xq9wkt owner=@khanakia stability=stable
	NotionVersion = "2022-06-28"
)

Notion endpoints (developers.notion.com/docs/authorization).

View Source
const (
	//ds:def id=vkauthurl-c6nw2xkq owner=@khanakia stability=stable
	VKAuthURL = "https://id.vk.ru/authorize"
	//ds:def id=vktokenurl-f8kq3wxn owner=@khanakia stability=stable
	VKTokenURL = "https://id.vk.ru/oauth2/auth"
	//ds:def id=vkuserinfourl-j2xw7kqt owner=@khanakia stability=stable
	VKUserInfoURL = "https://id.vk.ru/oauth2/user_info"
)

VK ID endpoints (id.vk.ru/about/business/go/docs, "API description").

View Source
const (
	//ds:def id=wechatauthurl-v3kq8xwn owner=@khanakia stability=stable
	WeChatAuthURL = "https://open.weixin.qq.com/connect/qrconnect"
	//ds:def id=wechattokenurl-b7xw2kqn owner=@khanakia stability=stable
	WeChatTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token"
	//ds:def id=wechatrefreshurl-g4nq9xkt owner=@khanakia stability=stable
	WeChatRefreshURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token"
	//ds:def id=wechatuserinfourl-z8kw3xqm owner=@khanakia stability=stable
	WeChatUserInfoURL = "https://api.weixin.qq.com/sns/userinfo"
)

WeChat endpoints (developers.weixin.qq.com, Website App WeChat Login).

View Source
const (
	WeChatLangCN = "cn"
	WeChatLangEN = "en"
)

WeChat languages for the QR login page (better-auth's lang option).

View Source
const AnnotationAction = "oauth.action"

AnnotationAction is the Call annotation (auth.Call.Annotate) a callback, id-token or provider-initiated sign-in records once it resolved its user, so an After or Finally hook, plugin/audit's included, can tell what the flow did without reading its input or result. The value is an AccountAction.

View Source
const EndpointAccessToken auth.EndpointID = "oauth.access_token"

EndpointAccessToken is the ExposeAccessTokens route, POST /access-token.

View Source
const EndpointReauthenticate auth.EndpointID = "oauth.reauthenticate"

EndpointReauthenticate starts a re-authentication with a linked provider: POST /reauthenticate, signed in. It answers the provider URL for the page to navigate to; it never redirects, because a fetch would follow the redirect.

View Source
const EndpointReauthenticateIDToken auth.EndpointID = "oauth.reauthenticate-id-token"

EndpointReauthenticateIDToken re-authenticates the signed-in user with a provider ID token from a native SDK: POST /reauthenticate-id-token, signed in. The answer is auth.Reauthentication, as password.reauthenticate answers; when the second factor is due, its Ticket goes to the second factor's re-authentication operation with the code.

View Source
const LinkFlag = "true"

LinkFlag is the value of StartRequest.Link that starts a link flow.

View Source
const MaxPhotoDataURL = 4096

MaxPhotoDataURL is the longest data URL MicrosoftPhoto puts in Profile.Image: 4096 characters, the user image column on MySQL (VARCHAR(4096)), so the photo fits on every engine. A larger photo is left out and the profile keeps its image; a 48-pixel photo fits, a large one rarely does.

View Source
const MicrosoftPhotoURLFormat = "https://graph.microsoft.com/v1.0/me/photos/%dx%d/$value"

MicrosoftPhotoURLFormat is the Graph photo of the signed-in person at a size (%d twice).

View Source
const PluginName = "oauth"

PluginName is the plugin's permanent identity.

View Source
const PurposeIDTokenClaim auth.Purpose = "oauth.id_token_claim"

PurposeIDTokenClaim is the verification purpose of the default guard's claims. Its Identifier is the provider name, and the token's hash is IDTokenClaim.Key, so the stored row holds no part of the token.

View Source
const PurposeState auth.Purpose = "oauth.state"

PurposeState is the verification purpose of a flow's state. Its Identifier is the provider name; Meta is a JSON-encoded flowMeta.

View Source
const RefreshMargin = time.Minute

RefreshMargin is how long before expiry Tokens refreshes an access token, so a token handed to a caller does not expire on its way to the provider.

View Source
const SignUpFlag = "true"

SignUpFlag is the value of StartRequest.SignUp that asks for a sign-up.

View Source
const UIMethodKind = "oauth"

UIMethodKind is the SignInMethod.Kind of every provider's button, so a page styles them as one group.

View Source
const WarnIDTokenReplayAllowed = "oauth: AllowIDTokenReplay is set; a captured ID token can be used again until it expires"

WarnIDTokenReplayAllowed is logged at start-up under AllowIDTokenReplay.

View Source
const WarnLinkUnverifiedLocal = "" /* 139-byte string literal not displayed */

WarnLinkUnverifiedLocal is logged by Init under LinkUnverifiedLocalEmail.

View Source
const WarnStateCookieCheckOff = "" /* 138-byte string literal not displayed */

WarnStateCookieCheckOff is logged by Init under SkipStateCookieCheck.

Variables

View Source
var (
	// ErrAppleKey is a .p8 file that is not a PEM-encoded PKCS #8 P-256 key.
	ErrAppleKey = errors.New("oauth: the Apple key is a PEM-encoded PKCS #8 ECDSA P-256 private key")
	// ErrAppleSecret is a configuration error: AppleClientSecret with an
	// empty team id, client id or key id, or a nil key.
	ErrAppleSecret = errors.New("oauth: AppleClientSecret needs teamID, clientID, keyID and a key")
)

Apple client-secret errors.

View Source
var (
	// ErrAssertionKey is a key that is neither an ECDSA P-256 nor an RSA
	// private key, the two kinds a token endpoint accepts for a client
	// assertion (ES256, RS256).
	ErrAssertionKey = errors.New("oauth: ClientAssertion needs an ECDSA P-256 or RSA private key")
	// ErrAssertionConfig is a configuration error: an *OAuth2 with
	// PrivateKeyJWT and no ClientAssertion, or a ClientAssertion with another
	// TokenAuth, where it would never be sent.
	ErrAssertionConfig = errors.New("oauth: ClientAssertion goes with TokenAuth PrivateKeyJWT, and PrivateKeyJWT needs one")
)

Client-assertion errors.

View Source
var (
	// ErrIDTokenInvalid covers every ID token that must not be trusted:
	// malformed, an unlisted algorithm, an unknown key, a bad signature,
	// expired, another issuer or audience, or a nonce mismatch. The reason is
	// wrapped for the log; the client sees one answer.
	ErrIDTokenInvalid = auth.NewError("oauth.id_token_invalid", http.StatusUnauthorized, "The sign-in token was not accepted.")
	// ErrIDTokenUnsupported is an id-token sign-in for a provider that has no
	// IDTokenConfig (an *OAuth2 without one, an OIDC issuer whose discovery
	// document has no jwks_uri).
	ErrIDTokenUnsupported = auth.NewError("oauth.id_token_unsupported", http.StatusBadRequest, "This provider does not accept sign-in tokens.")
	// ErrNonceRequired is an ID token without a nonce claim while
	// RequireIDTokenNonce is set.
	ErrNonceRequired = auth.NewError("oauth.nonce_required", http.StatusUnauthorized, "The sign-in token carries no nonce.")
	// ErrJWKSFailed is a key-set fetch or parse failure; the user sees
	// ErrProviderFailed and the log the reason.
	ErrJWKSFailed = errors.New("oauth: provider key set could not be read")
)

Errors of ID-token sign-in.

TableAccount and its typed columns.

View Source
var (
	ErrStateInvalid           = auth.NewError("oauth.state_invalid", http.StatusBadRequest, "This sign-in link has expired. Start again.")
	ErrProviderDenied         = auth.NewError("oauth.denied", http.StatusBadRequest, "Sign-in was cancelled at the provider.")
	ErrProviderFailed         = auth.NewError("oauth.provider_failed", http.StatusBadGateway, "The provider could not be reached. Try again.")
	ErrAccountExists          = auth.NewError("oauth.account_exists", http.StatusConflict, "An account with this email already exists. Sign in, then link this provider in settings.")
	ErrAccountLinkedElsewhere = auth.NewError("oauth.linked_elsewhere", http.StatusConflict, "This provider account is linked to another user.")
	ErrNotLinked              = auth.NewError("oauth.not_linked", http.StatusNotFound, "Not found.")
	ErrBadReturnTo            = auth.NewError("oauth.bad_return_to", http.StatusBadRequest, "The return address must be a path on this site or a trusted origin.")
	ErrRefreshFailed          = auth.NewError("oauth.refresh_failed", http.StatusBadGateway, "The provider did not renew access. Link the account again.")
	ErrNoTokens               = auth.NewError("oauth.no_tokens", http.StatusNotFound, "No provider tokens are stored for this account.")
	ErrBadTokenKey            = errors.New("oauth: StoreTokens needs a 32-byte key, or auth.WithSecret to derive one")
	// ErrSignUpDisabled is a callback that would create a user while
	// NoSignUp is set.
	ErrSignUpDisabled = auth.NewError("oauth.sign_up_disabled", http.StatusForbidden, "Sign-up with this provider is not open. Sign in to an existing account first.")
	// ErrEmailUnverified is a provider profile whose email is not verified
	// while RequireVerifiedEmail is set.
	ErrEmailUnverified = auth.NewError("oauth.email_unverified", http.StatusForbidden, "The provider has not verified your email address.")
	// ErrEmailNotFound is a provider profile with no email at all while
	// RequireEmail or VerifyEmailBeforeSignIn is set: better-auth's
	// email_not_found. RequireVerifiedEmail answers it ErrEmailUnverified.
	ErrEmailNotFound = auth.NewError("oauth.email_not_found", http.StatusForbidden, "The provider did not share an email address. Allow access to your email and try again.")
	// ErrEmailNotVerified is the code a sign-in held by
	// VerifyEmailBeforeSignIn lands with (`?error=oauth.email_not_verified`):
	// the user exists, and no session was issued until the address is
	// verified. better-auth's email_not_verified. It is a code in a redirect
	// and a flag in the result, never a failed call, because the user the
	// callback created must not be rolled back.
	ErrEmailNotVerified = auth.NewError("oauth.email_not_verified", http.StatusForbidden, "Verify your email address, then sign in again.")
	// ErrBadParam is a StartRequest.Params that does not parse, is too long,
	// or names a parameter the provider does not list in OptionalParams.
	ErrBadParam = auth.NewError("oauth.bad_param", http.StatusBadRequest, "A requested sign-in parameter is not available for this provider.")
	// ErrBadScope is a StartRequest.Scopes entry the provider does not list
	// in OptionalScopes.
	ErrBadScope = auth.NewError("oauth.bad_scope", http.StatusBadRequest, "A requested scope is not available for this provider.")
	// ErrRequestParams is a start with prompt, loginHint or scopes for a
	// Provider that is not a ParamsProvider, so the parameters would be
	// dropped silently.
	ErrRequestParams = auth.NewError("oauth.params_unsupported", http.StatusBadRequest, "This provider does not take sign-in options.")
	// ErrEmailMismatch is a link flow, under RequireSameEmailOnLink, whose
	// provider address belongs to nobody or to another user.
	ErrEmailMismatch = auth.NewError("oauth.email_mismatch", http.StatusConflict, "The provider account uses an address that is not on this account.")
	// ErrProviderIncomplete is a configuration error: an *OAuth2 without an
	// AuthURL, a TokenURL (unless GetToken replaces the exchange), a
	// ClientID or FetchProfile (unless the profile comes from the ID token),
	// or an unknown TokenAuth, or a PublicClient with a secret or a
	// ClientSecretFunc.
	ErrProviderIncomplete = errors.New("oauth: a provider needs AuthURL, TokenURL (or GetToken), ClientID, FetchProfile (or IDToken.ProfileFromToken) and a known TokenAuth (no secret for a PublicClient)")
	// ErrIDTokenConfig is a configuration error: an IDTokenConfig with a
	// JWKSURL but no Issuers, or ProfileFromToken without a JWKSURL.
	ErrIDTokenConfig = errors.New("oauth: IDToken needs JWKSURL and Issuers (ProfileFromToken needs both)")
	// ErrLogoutUnsupported is a logout for a provider with no end-session
	// endpoint: an *OAuth2 without EndSessionURL, or an issuer whose
	// discovery document publishes none.
	ErrLogoutUnsupported = auth.NewError("oauth.logout_unsupported", http.StatusNotFound, "This provider has no sign-out endpoint.")
	// ErrDataTooLong is a StartRequest.Data longer than maxFlowDataLength.
	ErrDataTooLong = auth.NewError("oauth.data_too_long", http.StatusBadRequest, "The data to carry through sign-in is too long.")
	// ErrSignUpNotRequested is a callback that would create a user under
	// ExplicitSignUp while the request did not ask for a sign-up.
	ErrSignUpNotRequested = auth.NewError("oauth.sign_up_not_requested", http.StatusForbidden, "No account is linked to this provider account. Use sign-up to create one.")
	// ErrProfileRefused is a ValidateProfile hook that refused the profile
	// with an error that is not an *auth.Error; the hook's reason is logged.
	ErrProfileRefused = auth.NewError("oauth.profile_refused", http.StatusForbidden, "This account cannot sign in here.")
	// ErrCannotRefresh is Refresh for an account with no refresh token, or
	// at a provider that is not a Refresher.
	ErrCannotRefresh = auth.NewError("oauth.cannot_refresh", http.StatusNotFound, "This account's tokens cannot be refreshed.")

	// ErrProviderUnknown is an ImportAccount for a provider the plugin was
	// not built with, where nothing could ever sign in with the row, or a
	// Start or Callback for a provider that is not in use.
	ErrProviderUnknown = errors.New("oauth: provider is not configured")
	// ErrImportSubject is an ImportAccount with an empty user id or provider
	// account id.
	ErrImportSubject = errors.New("oauth: ImportAccount needs a user id and a provider account id")

	ErrNoProviders   = errors.New("oauth: no providers")
	ErrNoCallbackURL = errors.New("oauth: callback URLs need auth.WithBaseURL or oauth.CallbackURL")
)

Errors.

AccountActionValues is every AccountAction, for documentation and tests.

CallbackParamValues is every CallbackParam, for validation.

View Source
var ErrAllProvidersBroken = errors.New("oauth: every provider is broken")

ErrAllProvidersBroken is auth.New under SkipBrokenProviders when every provider was left out: there is nothing left to keep working, so the failure is loud. It wraps each provider's reason.

View Source
var ErrCallbackParam = errors.New("oauth: unknown callback parameter")

ErrCallbackParam is a configuration error: a CallbackParams entry that is not one of CallbackParamValues.

View Source
var ErrExchange = errors.New("oauth: provider exchange failed")

ErrExchange is a failed code exchange or profile read. Its text is for the log; the client sees ErrProviderFailed.

View Source
var ErrExposeNeedsStoreTokens = errors.New("oauth: ExposeAccessTokens needs StoreTokens")

ErrExposeNeedsStoreTokens is ExposeAccessTokens without StoreTokens: the route would only ever answer ErrNoTokens.

View Source
var ErrFallbackIncomplete = errors.New("oauth: an OIDC Fallback needs AuthURL, TokenURL and UserInfoURL")

ErrFallbackIncomplete is a configuration error: an OIDCProvider.Fallback with some endpoint set but no AuthURL, TokenURL or UserInfoURL, which could not sign anyone in.

View Source
var ErrHostedDomain = fmt.Errorf("%w: the account is not in the required Google Workspace domain", ErrExchange)

ErrHostedDomain is a Google userinfo whose hd claim is not the Workspace domain the app requires. It wraps ErrExchange, so the user sees ErrProviderFailed and the log says which domain answered.

View Source
var ErrIDTokenReplayConfig = errors.New("oauth: ID-token replay protection")

ErrIDTokenReplayConfig refuses a replay configuration that cannot work: a guard given together with AllowIDTokenReplay, or the default guard over a verification store that does not refuse duplicates.

View Source
var ErrIDTokenReplayed = auth.NewError("oauth.id_token_replayed", http.StatusUnauthorized, "This sign-in token was already used. Sign in with the provider again.")

ErrIDTokenReplayed refuses an ID token that was already accepted once, by a native sign-in or an ID-token re-authentication. It says only that this token is spent, which its holder can learn by using it, so it reveals no account. The app asks the provider's SDK for a new token and tries again.

View Source
var ErrLogoutProvider = auth.NewError("oauth.logout_provider", http.StatusBadRequest, "Name the provider to sign out of.")

ErrLogoutProvider is a logout request that names no provider on the general route, or another provider than its own route's.

View Source
var ErrPhotoSize = errors.New("oauth: MicrosoftPhoto size must be one of MicrosoftPhotoSizes")

ErrPhotoSize is MicrosoftPhoto given a size Graph does not serve.

View Source
var ErrProfileUnsupported = auth.NewError("oauth.profile_unsupported", http.StatusNotFound, "This provider does not offer a profile to read.")

ErrProfileUnsupported is a profile read on a provider that has no profile call: its profile comes only from the ID token of the exchange.

View Source
var ErrProviderInitiatedConfig = errors.New("oauth: AllowProviderInitiated needs a configured provider that verifies ID tokens (an OIDC provider, or OAuth2 with IDToken)")

ErrProviderInitiatedConfig is a configuration error: AllowProviderInitiated or RestartProviderInitiated names a provider that is not configured, both name the same one, or AllowProviderInitiated names one that cannot verify an ID token (an *OAuth2 without IDToken, or a custom Provider that is not an InitiatedExchanger). Without a verified ID token nothing would vouch for a code no request of this app asked for.

View Source
var ErrPublicClientPKCE = errors.New("oauth: NoPKCE needs a client secret; a public client has nothing else tying the code to it")

ErrPublicClientPKCE is a configuration error: NoPKCE on a provider with TokenAuth PublicClient. With no secret and no PKCE, anyone holding the code could redeem it.

View Source
var ErrReauthNotLinked = auth.NewError("oauth.reauth_not_linked", http.StatusForbidden, "Continue with the provider account that is linked to this account.")

ErrReauthNotLinked refuses a re-authentication with a provider the user has no account linked at, or whose callback brought back a provider account that is not linked to the signed-in user (another person's, or one never linked). One code for every case, so the answer does not tell which accounts exist.

View Source
var ErrRedirectRefused = errors.New("oauth: provider answered with a redirect; server-side provider calls do not follow redirects")

ErrRedirectRefused is the error a provider call fails with when the provider answered with an HTTP redirect: the token, profile, discovery, key-set, refresh and revocation endpoints are called from the server, and following a redirect there would let whoever controls one of those URLs (a misconfigured issuer, a compromised discovery document) bounce the server to an internal address (SSRF). better-auth refuses them the same way. The flow fails with ErrProviderFailed and this error is logged; configure the final endpoint URL.

View Source
var ErrServerContextTooLong = errors.New("oauth: WithServerContext values exceed 1024 characters as JSON")

ErrServerContextTooLong is a server context whose JSON exceeds maxServerContextLength: a programming error of the app's function, so it is a plain error (a 500), not a client answer.

View Source
var ErrStateCookieKey = errors.New("oauth: StateInCookie needs auth.WithSecret, and cannot be combined with SkipStateCookieCheck")

ErrStateCookieKey is StateInCookie without auth.WithSecret to derive its key from, or combined with SkipStateCookieCheck.

MicrosoftPhotoSizes is every MicrosoftPhotoSize.

View Source
var Presets = []string{
	"google", "github", "apple", "microsoft", "discord", "facebook", "slack", "linkedin", "gitlab", "twitch", "line",
	"huggingface", "roblox", "salesforce", "auth0", "okta", "keycloak", "zitadel", "cognito", "spotify", "dropbox",
	"x", "reddit", "atlassian", "zoom", "figma", "kakao", "naver", "kick", "linear", "cloudflare", "paypal", "polar",
	"vercel", "patreon", "gumroad", "hubspot", "yandex", "paybin", "railway", "tiktok", "notion", "vk", "wechat",
	"dub", "instagram", "snapchat", "zoho",
}

Presets lists every provider constructor's route segment, for documentation and for a test that keeps the README's table complete.

ProfileActionValues is every ProfileAction, for documentation and tests.

TokenAuthValues is every TokenAuth, for validation and documentation.

ZohoRegionValues lists every ZohoRegion, for a configuration screen and for the test that keeps the set closed.

Functions

func AppleClientSecret

func AppleClientSecret(teamID, clientID, keyID string, key *ecdsa.PrivateKey) (func(ctx context.Context) (string, error), error)

AppleClientSecret returns a ClientSecretFunc that signs Apple's client-secret assertion: an ES256 JWT with the team id as issuer, the client id as subject and Apple as audience, valid appleSecretLifetime and renewed before it expires. teamID and keyID come from the Apple developer account; key is ParseAppleKey of the .p8 file.

func ApplePostedUser

func ApplePostedUser(raw string, p *Profile)

ApplePostedUser reads Apple's user field ({"name":{"firstName","lastName"}, "email"}) into the profile: the name joined with a space when the profile has none, and the email only when the ID token carried none. The field comes from the browser, not from Apple's signed token, so it never overrides what the token said and never marks an address verified. A field that is not that JSON is ignored.

func AppleScopes

func AppleScopes() []string

AppleScopes are the scopes Sign in with Apple offers: the email (in the ID token) and the name (in the user form field, on first consent only). A function, so no caller can change another's list.

func AtlassianProfile

func AtlassianProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

AtlassianProfile reads Atlassian's /me answer from u.

func CallbackEndpoint

func CallbackEndpoint(provider string) auth.EndpointID

CallbackEndpoint names a provider's callback operation: "oauth.<provider>-callback".

func ClientAssertion

func ClientAssertion(clientID, kid string, key crypto.Signer) (func(ctx context.Context, tokenURL string) (string, error), error)

ClientAssertion returns an OAuth2.ClientAssertion that signs the RFC 7523 client assertion for TokenAuth PrivateKeyJWT: a JWT with the client id as issuer and subject, the token endpoint as audience, a fresh jti and a lifetime of assertionLifetime, signed ES256 with a P-256 key or RS256 with an RSA key. kid is the key id registered at the provider, written in the header so the provider picks the right public key. Entra ID (workload identity federation, certificate credentials) and any issuer that refuses shared secrets take this form.

func CloudflareProfile

func CloudflareProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

CloudflareProfile reads Cloudflare's /client/v4/user answer from u.

func DiscordProfile

func DiscordProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

DiscordProfile reads Discord's /users/@me answer from u.

func DropboxProfile

func DropboxProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

DropboxProfile posts to Dropbox's get_current_account at u.

func DubProfile

func DubProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

DubProfile reads Dub's /oauth/userinfo answer from u: id, name, image.

func FacebookProfile

func FacebookProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

FacebookProfile reads Facebook's /me answer from u.

func FigmaProfile

func FigmaProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

FigmaProfile reads Figma's /v1/me answer from u.

func FormPostEndpoint

func FormPostEndpoint(provider string) auth.EndpointID

FormPostEndpoint names a provider's form-post relay operation: "oauth.<provider>-form-post", served as POST at the callback route. It exists only for a FormPoster.

func GitHubProfile

func GitHubProfile(base string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

GitHubProfile reads the GitHub user and primary email from the API at base.

func GoogleProfile

func GoogleProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

GoogleProfile reads an OpenID Connect userinfo response from u.

func GoogleProfileFor

func GoogleProfileFor(u, hostedDomain string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

GoogleProfileFor is GoogleProfile restricted to one Google Workspace domain: a userinfo whose hd claim is not hostedDomain is refused. The hd parameter on the authorization URL is only a hint to the account picker; this check on the returned claim is the enforcement. An empty hostedDomain accepts every account.

func GumroadProfile

func GumroadProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

GumroadProfile reads Gumroad's /v2/user answer from u.

func HubSpotProfile

func HubSpotProfile(base string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

HubSpotProfile reads HubSpot's access-token description at base + token.

func IDTokenEndpoint

func IDTokenEndpoint(provider string) auth.EndpointID

IDTokenEndpoint names a provider's id-token operation: "oauth.<provider>-id-token". It exists only for an IDTokenVerifier.

func InstagramProfile

func InstagramProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

InstagramProfile reads the Graph API /me answer from u: the app-scoped id as subject (better-auth-instagram's), the name or else the username, and the profile picture.

func KakaoProfile

func KakaoProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

KakaoProfile reads Kakao's /v2/user/me answer from u.

func KickProfile

func KickProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

KickProfile reads Kick's /public/v1/users answer from u.

func LinearProfile

func LinearProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

LinearProfile posts the viewer query to Linear's GraphQL endpoint at u.

func LogoutEndpoint

func LogoutEndpoint(provider string) auth.EndpointID

LogoutEndpoint names a provider's logout operation: "oauth.<provider>-logout". It exists only for a Logouter.

func MicrosoftPhoto

func MicrosoftPhoto(fetch func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error), size MicrosoftPhotoSize) (func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error), error)

MicrosoftPhoto wraps a Microsoft FetchProfile so the profile's image is the person's Graph photo at size, as a data URL (better-auth's profilePhotoSize; leaving it unwrapped is disableProfilePhoto, our default). A photo that is missing (404), unreadable, or longer than MaxPhotoDataURL as a data URL leaves the image as the profile had it: a sign-in never fails over a photo. size must be one of MicrosoftPhotoSizes.

m := oauth.Microsoft(tenant, id, secret)
m.FetchProfile, err = oauth.MicrosoftPhoto(m.FetchProfile, oauth.MicrosoftPhoto48)
func NaverProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

NaverProfile reads Naver's /v1/nid/me answer from u.

func NotionProfile

func NotionProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

NotionProfile reads Notion's /v1/users/me answer from u: a bot whose owner.user is the person who authorized it.

func PKCEChallenge

func PKCEChallenge(verifier string) string

PKCEChallenge is the RFC 7636 S256 code challenge of verifier: the unpadded base64url SHA-256. Exported for a flow that runs a Provider without this plugin (authgate): *OAuth2 with IDToken.ProfileFromToken also sends this value as the OpenID Connect nonce and recomputes it in Exchange, so a caller that builds the authorization URL must derive the challenge by exactly this rule or the ID token's nonce check fails.

func ParseAppleKey

func ParseAppleKey(p8 []byte) (*ecdsa.PrivateKey, error)

ParseAppleKey reads the .p8 file Apple issues for Sign in with Apple.

func PatreonProfile

func PatreonProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

PatreonProfile reads Patreon's identity answer from u.

func RedditProfile

func RedditProfile(u, userAgent string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

RedditProfile reads Reddit's /api/v1/me answer from u, sending userAgent.

func SnapchatProfile

func SnapchatProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

SnapchatProfile reads Snap Kit's /v1/me at u with the GraphQL query for externalId, displayName and the Bitmoji avatar. A GraphQL error, or an answer with no externalId (the user.external_id scope not granted), is a failed read.

func SpotifyProfile

func SpotifyProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

SpotifyProfile reads Spotify's /v1/me answer from u.

func StartEndpoint

func StartEndpoint(provider string) auth.EndpointID

StartEndpoint names a provider's start operation: "oauth.<provider>-start".

func TikTokProfile

func TikTokProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

TikTokProfile reads TikTok's /v2/user/info/ answer from u: data.user with open_id, display_name and avatar_large_url. An answer whose error.code is not "ok" is a failed read.

func UserInfoProfile

func UserInfoProfile(u, subjectClaim string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

UserInfoProfile reads an OpenID Connect userinfo answer from u and takes subjectClaim as the subject instead of sub, for an issuer whose stable identifier is another claim (Entra ID's oid). An empty subjectClaim is sub. The claim must be a string or a number; anything else is a profile with no subject, which the exchange refuses.

func VKProfile

func VKProfile(u, clientID string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

VKProfile reads VK ID's user_info answer from u, posting the access token and clientID as a form, which is how VK ID takes them.

func ValidateProvider

func ValidateProvider(prov Provider) error

ValidateProvider is the configuration check New's Validate runs on every provider: a route-safe name, and for the built-in shapes the fields a sign-in cannot work without (endpoints, client id, a profile source, a coherent token-endpoint authentication, a usable ID-token configuration). Exported so a flow that runs a Provider without this plugin (authgate) refuses the same broken configurations at startup. A Provider of another type passes on its name alone: its own constructor owns its checks.

func WeChatProfile

func WeChatProfile(u string) func(ctx context.Context, hc *http.Client, ts TokenSet) (Profile, error)

WeChatProfile reads WeChat's /sns/userinfo from u for the openid the token answer carried (TokenSet.Raw).

func WithCallbackValues

func WithCallbackValues(ctx context.Context, v CallbackValues) context.Context

WithCallbackValues returns ctx carrying v, for the exchange the callback runs; a test or a flow outside this plugin (authgate) sets it the same way.

func XProfile

func XProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

XProfile reads X's /2/users/me answer from u.

func YandexProfile

func YandexProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

YandexProfile reads Yandex's info answer from u.

func ZoomProfile

func ZoomProfile(u string) func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)

ZoomProfile reads Zoom's /v2/users/me answer from u.

Types

type AccessTokenRequest

type AccessTokenRequest struct {
	Provider string `json:"provider"`
}

AccessTokenRequest names the provider whose token the signed-in user wants.

type AccessTokenResult

type AccessTokenResult struct {
	// AccessToken is revealed on purpose: delivering it is this route's
	// job (ExposeAccessTokens).
	AccessToken string `json:"accessToken"`
	// ExpiresAt is when AccessToken expires; nil when the provider did not
	// say.
	ExpiresAt *time.Time `json:"accessTokenExpiresAt"`
	// Scopes are the scopes the provider said it granted.
	Scopes []string `json:"scopes"`
	// IDToken is the OpenID Connect ID token of the last exchange, "" when
	// the provider issued none.
	IDToken string `json:"idToken,omitempty"`
}

AccessTokenResult is the signed-in user's provider token, without the refresh token, which stays on the server.

type Account

type Account struct {
	ID        string    `json:"id"`
	UserID    string    `json:"-"`
	Provider  string    `json:"provider"`
	Subject   string    `json:"-"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"createdAt"`
	// Scopes are the scopes the provider said it granted at the last
	// exchange or refresh (better-auth's listAccounts scopes), so a page can
	// tell whether to ask for more; empty when tokens are not stored
	// (StoreTokens) or the provider did not say. Scope names are not
	// secrets; the tokens themselves never appear here.
	Scopes []string `json:"scopes"`
	// contains filtered or unexported fields
}

Account is one linked provider account. UserID and Subject are hidden from JSON: a client never needs the provider's id for the person.

type AccountAction

type AccountAction string

AccountAction is the value of AnnotationAction.

const (
	// AccountSignIn: a linked provider account signed its user in.
	AccountSignIn AccountAction = "sign_in"
	// AccountSignUp: a user was created for the provider account.
	AccountSignUp AccountAction = "sign_up"
	// AccountLinked: a signed-in user linked the provider account (a link
	// flow, or an id-token call from a session).
	AccountLinked AccountAction = "link"
	// AccountLinkedByEmail: the provider account was linked to an existing
	// user because both sides' verified addresses matched.
	AccountLinkedByEmail AccountAction = "link_email"
	// AccountReauthenticated: a signed-in user confirmed who they are with a
	// linked provider account (EndpointReauthenticate, or
	// EndpointReauthenticateIDToken); nothing was signed in, created or
	// linked.
	AccountReauthenticated AccountAction = "reauthenticate"
)

What an OAuth flow did. Linking is split by how it happened, because an audit log reads them differently: a link the signed-in user asked for, and a link the plugin made because the verified addresses matched.

type AccountsResult

type AccountsResult struct {
	Accounts []Account `json:"accounts"`
}

AccountsResult is the output of accounts: the signed-in user's linked providers, oldest first.

type CallbackParam

type CallbackParam string

CallbackParam names a callback query parameter a provider's token request carries as well (OAuth2.CallbackParams). A closed set: the plugin forwards only values it decoded and checked itself.

const (
	// CallbackParamState forwards the callback's state, which VK ID's token
	// request requires. The state is spent by then; sending it back to the
	// provider that issued it reveals nothing.
	CallbackParamState CallbackParam = "state"
	// CallbackParamDeviceID forwards VK ID's device_id.
	CallbackParamDeviceID CallbackParam = "device_id"
)

type CallbackRequest

type CallbackRequest struct {
	Code  string `json:"code,omitempty"`
	State string `json:"state"`
	Error string `json:"error,omitempty"`
	// Providers add fields such as error_description and scope; they are
	// accepted and ignored.
	ErrorDescription string `json:"error_description,omitempty"`
	Scope            string `json:"scope,omitempty"`
	AuthUser         string `json:"authuser,omitempty"`
	Prompt           string `json:"prompt,omitempty"`
	HD               string `json:"hd,omitempty"`
	// Iss is the RFC 9207 issuer identifier, checked against the provider's
	// AuthorizationIssuer when it has one.
	Iss string `json:"iss,omitempty"`
	// User is the user field of a form-post callback (Apple's first-consent
	// name and email), relayed from the POST and read by the provider's
	// PostedProfile after the exchange; ignored by any other provider.
	User string `json:"user,omitempty"`
	// DeviceID is VK ID's device_id, which its token request requires; a
	// provider forwards it only when its CallbackParams name it.
	DeviceID string `json:"device_id,omitempty"`
}

CallbackRequest is what the provider sends back.

type CallbackResult

type CallbackResult struct {
	User auth.UserView `json:"user"`
	// Created is true when the callback created the user.
	Created bool `json:"created"`
	// Linked is true when the callback linked the provider to the signed-in
	// user.
	Linked     bool   `json:"linked"`
	RedirectTo string `json:"redirectTo"`
	// SecondFactorRequired is true when the session is pending a second
	// factor; the app page at RedirectTo asks for it.
	SecondFactorRequired bool `json:"secondFactorRequired"`
	// EmailVerificationRequired is true when VerifyEmailBeforeSignIn held
	// the sign-in: the user exists, no session was issued, and RedirectTo
	// carries `?error=oauth.email_not_verified`.
	EmailVerificationRequired bool       `json:"emailVerificationRequired,omitempty"`
	Token                     auth.Token `json:"-"`
	// Data is what the start request carried in StartRequest.Data; empty
	// for an id-token sign-in.
	Data string `json:"data"`
	// ServerContext is what the WithServerContext functions returned at
	// the start (better-auth's serverContext): server-set values, never
	// sent to the client (json "-"), read by an After hook on the callback.
	ServerContext map[string]string `json:"-"`
	// Reauthentication is the outcome of a re-authentication callback
	// (EndpointReauthenticate), nil for every other flow: the session is
	// fresh, or the second factor is due with a ticket. No session is
	// issued and Token stays empty; RedirectTo carries ParamReauth.
	Reauthentication *auth.Reauthentication `json:"reauthentication,omitempty"`
}

CallbackResult is the outcome of a callback.

type CallbackValues

type CallbackValues struct {
	State    string
	DeviceID string
}

CallbackValues are the callback's values a token request may need beyond the code: what VK ID's exchange requires. The callback puts them in the exchange's context; a Provider of the app's own reads them with CallbackValuesOf (better-auth hands deviceId to validateAuthorizationCode).

func CallbackValuesOf

func CallbackValuesOf(ctx context.Context) CallbackValues

CallbackValuesOf returns the callback values in ctx, zero outside a callback's exchange.

type Discoverer

type Discoverer interface {
	Discover(ctx context.Context, hc *http.Client) error
}

Discoverer is a provider that must look up its endpoints before its first flow. The plugin calls Discover at every start; an implementation caches.

type Endpoints

type Endpoints struct {
	AuthURL     string
	TokenURL    string
	UserInfoURL string
	// JWKSURL is the key set, for ID tokens; empty accepts none while the
	// fallback is in use.
	JWKSURL       string
	EndSessionURL string
	RevokeURL     string
	// IssParameterSupported is the discovery flag of RFC 9207: set it when
	// the issuer sends iss on every authorization response, so the callback
	// keeps checking it while the fallback is in use.
	IssParameterSupported bool
}

Endpoints are an OpenID Connect issuer's endpoints written down by hand, for OIDCProvider.Fallback. AuthURL, TokenURL and UserInfoURL are required once any field is set (ErrFallbackIncomplete); the others are optional, as in a discovery document.

func (Endpoints) Enabled

func (e Endpoints) Enabled() bool

Enabled reports whether any endpoint is set.

type FormPostRequest

type FormPostRequest struct {
	Code  string `json:"code"`
	State string `json:"state"`
	Error string `json:"error"`
	// User is Apple's first-consent user field (name and email as JSON).
	User string `json:"user"`
}

FormPostRequest is what a response_mode=form_post provider posts to the callback route: the same fields as CallbackRequest, as a form.

type FormPostResult

type FormPostResult struct {
	RedirectTo string `json:"redirectTo"`
}

FormPostResult is the GET callback URL the relay redirects to.

type FormPoster

type FormPoster interface {
	Provider
	PostsBack() bool
}

FormPoster is a Provider that answers by response_mode=form_post. For each one the plugin also serves POST at the callback route: a relay that turns the cross-site form into the GET callback the flow cookie travels with. *OAuth2 implements it through FormPost.

type IDTokenClaim

type IDTokenClaim struct {
	// Key identifies the token's issuance: the hex SHA-256 of its signed
	// part (the JWS header and payload, without the signature), or of the
	// whole token when it is not a compact JWS. Not the raw token, because
	// an ES256 signature has a second valid form (s and n-s), so one
	// issuance has two spellings; and not issuer, subject, audience and jti,
	// because jti is optional (Apple's tokens carry none) and two tokens
	// issued in one second would share the rest. Every claim the provider
	// signed is in the key, so two different issuances never share it.
	Key string
	// Provider is the provider that verified the token.
	Provider string
	// Until is when the claim may be forgotten: the token's exp plus the
	// verifier's clock leeway, after which the token is refused as expired
	// anyway. A verifier whose profile carries no exp claim gets
	// idTokenClaimFallback from now.
	Until time.Time
}

IDTokenClaim is one accepted ID token, as the guard is asked to claim it.

type IDTokenConfig

type IDTokenConfig struct {
	// JWKSURL is where the provider publishes its signing keys. Required.
	JWKSURL string
	// Issuers are the iss values accepted; Google uses two spellings. At
	// least one is required.
	Issuers []string
	// Audiences are accepted aud values besides ClientID: an iOS bundle id,
	// an Android client id. The web ClientID is always accepted.
	Audiences []string
	// ProfileFromToken makes Exchange read the profile from the id_token in
	// the token response instead of calling FetchProfile, for a provider that
	// has no userinfo endpoint. The token's nonce is the flow's PKCE
	// challenge, which AuthCodeURL sends as the nonce parameter.
	ProfileFromToken bool
	// RequireNonce refuses an ID token that carries no nonce claim
	// (ErrNonceRequired), for an app whose clients always send one: a token
	// replayed from another app's sign-in then has no way in.
	RequireNonce bool
	// HostedDomain refuses a token whose hd claim is not this Google
	// Workspace domain (ErrIDTokenInvalid), the native counterpart of
	// GoogleProfileFor; empty accepts every account. Only Google sets hd.
	HostedDomain string
}

IDTokenConfig makes a provider accept ID tokens: tokens a native SDK (Google One Tap, Sign in with Apple, the Android and iOS libraries) hands the app, verified here against the provider's published keys, and tokens the provider returns from its token endpoint, for a provider that has no userinfo endpoint (Apple). The zero value accepts none.

func GoogleIDTokens

func GoogleIDTokens(audiences ...string) IDTokenConfig

GoogleIDTokens makes oauth.Google accept ID tokens on its id-token route: Google One Tap in the browser, and the Android and iOS sign-in libraries. audiences are the app's other client ids (the iOS and Android ones); the web ClientID is always accepted. Set it on the provider:

g := oauth.Google(id, secret)
g.IDToken = oauth.GoogleIDTokens("ios-client-id")

hostedDomain, when given through GoogleIDTokensFor, refuses tokens whose hd claim is another Workspace domain, the native counterpart of GoogleProfileFor.

func GoogleIDTokensFor

func GoogleIDTokensFor(hostedDomain string, audiences ...string) IDTokenConfig

GoogleIDTokensFor is GoogleIDTokens restricted to one Google Workspace domain: a token whose hd claim is not hostedDomain is ErrIDTokenInvalid. An empty hostedDomain accepts every account.

func (IDTokenConfig) Enabled

func (c IDTokenConfig) Enabled() bool

Enabled reports whether the provider accepts ID tokens.

type IDTokenReplayGuard

type IDTokenReplayGuard interface {
	Claim(ctx context.Context, claim IDTokenClaim) error
}

IDTokenReplayGuard decides whether an ID token may be accepted: Claim records claim.Key as used until claim.Until and returns nil the first time, and ErrIDTokenReplayed (or an error wrapping it) for a key already claimed and not yet past its Until. Of concurrent claims of one key exactly one may succeed. Any other error is a failure of the guard, and the operation fails with it rather than accept an unchecked token.

type IDTokenReplayGuardFunc

type IDTokenReplayGuardFunc func(ctx context.Context, claim IDTokenClaim) error

IDTokenReplayGuardFunc adapts a function to IDTokenReplayGuard.

func (IDTokenReplayGuardFunc) Claim

Claim calls f.

type IDTokenRequest

type IDTokenRequest struct {
	// IDToken is the provider's ID token, a compact JWS.
	IDToken string `json:"idToken"`
	// Nonce is the value the app sent to the provider, when it sent one;
	// the token's nonce claim must be it or its SHA-256 hex.
	Nonce string `json:"nonce,omitempty"`
	// SignUp says the person meant to sign up, which ExplicitSignUp
	// requires before a user is created.
	SignUp bool `json:"signUp,omitempty"`
	// AccessToken is the access token the native SDK received with the ID
	// token, better-auth's idToken.accessToken. It is taken only when the ID
	// token binds it: an at_hash claim (OpenID Connect Core 3.1.3.6) equal
	// to the hash of this value, so a client cannot attach a stolen or
	// unrelated token to a verified identity. A token without at_hash, or
	// with another hash, is ErrIDTokenInvalid. Once taken, it is the
	// account's stored access token under StoreTokens, like a callback's.
	AccessToken string `json:"accessToken,omitempty"`
}

IDTokenRequest is the input of an id-token operation: a token the app received from the provider's native SDK.

type IDTokenVerifier

type IDTokenVerifier interface {
	Provider
	// VerifyIDToken checks the token's signature, issuer, audience and
	// lifetime, and its nonce against nonce when the token carries one. A
	// token that must not be trusted is ErrIDTokenInvalid; a provider not
	// configured for ID tokens is ErrIDTokenUnsupported.
	VerifyIDToken(ctx context.Context, hc *http.Client, token, nonce string) (Profile, error)
}

IDTokenVerifier is a Provider that can verify a provider-issued ID token and turn its claims into a Profile. *OAuth2 and *OIDCProvider implement it; the id-token operation exists only for providers that do.

type ImportAccount

type ImportAccount struct {
	// UserID is the user here the account belongs to; it must exist.
	UserID string
	// Provider is the configured provider's name (ErrProviderUnknown otherwise).
	Provider string
	// ProviderAccountID is the provider's stable id for the person, stored as
	// the subject; "Sign in with <provider>" then lands on UserID.
	ProviderAccountID string
	// Email is the address the provider reported there, kept as information.
	Email string
}

ImportAccount is one linked account from another system, for ImportAccount.

type InitiatedExchanger

type InitiatedExchanger interface {
	Provider
	// ExchangeInitiated trades code for the person's profile without a
	// verifier. The token response must carry an ID token whose signature,
	// issuer, audience and lifetime verify and which carries no nonce, and
	// the profile's subject must be the token's.
	ExchangeInitiated(ctx context.Context, hc *http.Client, code, redirectURI string) (Profile, error)
}

InitiatedExchanger is a Provider that can complete a sign-in the provider started (AllowProviderInitiated): a code that arrives with no state and no PKCE verifier, trusted only through the verified ID token of its token response. *OAuth2 (with an IDToken key set) and *OIDCProvider implement it.

type IssuerChecker

type IssuerChecker interface {
	Provider
	// ExpectedIssuer is the iss every authorization response must carry, or
	// "" when the provider sends none.
	ExpectedIssuer() string
}

IssuerChecker is a Provider whose authorization responses carry the RFC 9207 iss parameter. *OAuth2 (AuthorizationIssuer) and *OIDCProvider (from discovery) implement it; the callback refuses a response whose iss is missing or another when AuthorizationIssuer is not "".

type Labeler

type Labeler interface {
	Provider
	// Label is the display name; "" falls back to Name.
	Label() string
}

Labeler is a Provider with a name for people, shown on a sign-in button ("Google", "Okta at Acme"). *OAuth2 and *OIDCProvider implement it through their DisplayName; a Provider that does not is listed under its Name.

type Loader

type Loader interface {
	Provider
	// Load fetches what the provider would otherwise fetch at its first
	// flow, and reports why it cannot be used.
	Load(ctx context.Context, hc *http.Client) error
}

Loader is a Provider that reads part of its configuration lazily (a discovery document, a key set). SkipBrokenProviders calls Load on every one at auth.New, so a provider that cannot be loaded is found and left out then rather than failing its first flow. *OAuth2 and *OIDCProvider implement it; a custom Provider that is not a Loader is checked on its name alone.

type LogoutRequest

type LogoutRequest struct {
	// Provider names the provider to sign out of on the general logout
	// route (POST /logout), which needs it (ErrLogoutProvider). On a
	// provider's own route (/<provider>/logout) it may be left empty; any
	// other value than that provider is ErrLogoutProvider.
	Provider string `json:"provider,omitempty"`
	// ReturnTo is where the provider sends the browser after its logout, as
	// post_logout_redirect_uri, instead of PostLogoutURL. It passes the same
	// TrustsReturnURL check as a sign-in's returnTo, so it is a path on this
	// site, a URL on a trusted origin or a trusted deep-link scheme;
	// anything else is ErrBadReturnTo and the session is not ended. A path
	// is made absolute against the base URL, because the parameter must be
	// an absolute URI. It must still be registered at the provider, which
	// refuses an unregistered one on its own page.
	ReturnTo string `json:"returnTo,omitempty"`
}

LogoutRequest is the input of a provider logout. Every field is optional: an empty request sends the configured PostLogoutURL.

type LogoutResult

type LogoutResult struct {
	URL string `json:"url"`
}

LogoutResult is the provider URL that ends the session there. HTTP transports redirect to it; a Go or API caller navigates itself.

type Logouter

type Logouter interface {
	Provider
	// EndSession returns the provider's end_session_endpoint, or "" when the
	// provider has none.
	EndSession(ctx context.Context, hc *http.Client) (string, error)
}

Logouter is a Provider with an RP-initiated logout endpoint (OpenID Connect RP-Initiated Logout 1.0). *OAuth2 (EndSessionURL) and *OIDCProvider (the discovery document's end_session_endpoint) implement it; the logout operation exists only for providers that do.

type MicrosoftPhotoSize

type MicrosoftPhotoSize int

MicrosoftPhotoSize is a size Microsoft Graph serves a profile photo at (better-auth's profilePhotoSize). A closed set: Graph answers 404 for any other.

const (
	MicrosoftPhoto48  MicrosoftPhotoSize = 48
	MicrosoftPhoto64  MicrosoftPhotoSize = 64
	MicrosoftPhoto96  MicrosoftPhotoSize = 96
	MicrosoftPhoto120 MicrosoftPhotoSize = 120
	MicrosoftPhoto240 MicrosoftPhotoSize = 240
	MicrosoftPhoto360 MicrosoftPhotoSize = 360
	MicrosoftPhoto432 MicrosoftPhotoSize = 432
	MicrosoftPhoto504 MicrosoftPhotoSize = 504
	MicrosoftPhoto648 MicrosoftPhotoSize = 648
)

The sizes Graph serves, in pixels per side.

type OAuth2

type OAuth2 struct {
	ID           string
	ClientID     string
	ClientSecret string
	AuthURL      string
	TokenURL     string
	Scopes       []string
	// FetchProfile reads the profile with the access token.
	FetchProfile func(ctx context.Context, hc *http.Client, accessToken string) (Profile, error)
	// FetchProfileFromTokens reads the profile from the whole token set
	// (TokenSet.Raw included), for a provider whose profile call needs more
	// than the access token. One profile source is required: this,
	// FetchProfile, or IDToken.ProfileFromToken; the first set wins in that
	// order reversed (ProfileFromToken, then this, then FetchProfile).
	FetchProfileFromTokens func(ctx context.Context, hc *http.Client, tokens TokenSet) (Profile, error)
	// TokenAuth is how the token endpoint is authenticated to; zero is ClientSecretPost.
	TokenAuth TokenAuth
	// AuthParams are added to every authorization URL (prompt, access_type,
	// hd, …); the protocol's own keys (reservedAuthParams) are ignored, so a
	// parameter here can never weaken the flow.
	AuthParams url.Values
	// TokenParams are added to every token and refresh request; the grant's
	// own keys (reservedTokenParams) are ignored.
	TokenParams url.Values
	// RefreshParams are added to refresh requests only, on top of
	// TokenParams, for a provider whose refresh grant takes a parameter the
	// code exchange does not; the grant's own keys are ignored here too.
	RefreshParams url.Values
	// AuthorizationIssuer is the iss value the callback must carry (RFC
	// 9207), for a provider that sends one; a callback without it, or with
	// another, is refused as ErrStateInvalid, the mix-up attack defence.
	// Empty means the provider sends none and nothing is checked. An
	// OIDCProvider fills it from the discovery document when the issuer
	// publishes authorization_response_iss_parameter_supported.
	AuthorizationIssuer string
	// RevokeURL is the provider's token revocation endpoint (RFC 7009); when
	// set and StoreTokens kept tokens, unlink revokes them there, best
	// effort. Empty revokes nothing.
	RevokeURL string
	// ClientAssertion signs the RFC 7523 client assertion for TokenAuth
	// PrivateKeyJWT; see the ClientAssertion constructor. Validate requires
	// it with PrivateKeyJWT and refuses it with any other TokenAuth.
	ClientAssertion func(ctx context.Context, tokenURL string) (string, error)
	// OptionalScopes are the scopes a start request may add to Scopes for
	// incremental authorization; any other is refused with ErrBadScope.
	OptionalScopes []string
	// OptionalParams are the authorization parameters a start request may
	// set (StartRequest.Params): better-auth's per-request additionalParams,
	// such as display, ui_locales or acr_values. Only names listed here are
	// forwarded, any other is refused with ErrBadParam, and the protocol's
	// own (reservedAuthParams) are never forwarded, so a client can never
	// choose a parameter the app did not name.
	OptionalParams []string
	// TokenLifetime is the access token's lifetime when the token response
	// has no expires_in; zero keeps the expiry unknown, which disables refresh.
	TokenLifetime time.Duration
	// IDToken makes the provider accept ID tokens (the id-token operation)
	// and, with ProfileFromToken, read the profile from the token response's
	// id_token instead of FetchProfile. Zero accepts none.
	IDToken IDTokenConfig
	// EndSessionURL is the provider's RP-initiated logout endpoint; empty
	// means none, and the logout operation answers ErrLogoutUnsupported.
	EndSessionURL string
	// ClientSecretFunc computes the client secret per token request, for a
	// secret that is a signed assertion (Apple; see AppleClientSecret).
	ClientSecretFunc func(ctx context.Context) (string, error)
	// Headers are added to every token, refresh and revocation request, for
	// a provider that requires a User-Agent naming the app (Reddit) or an
	// API version header. Authorization and Content-Type (reservedHeaders)
	// are ignored: the credentials and the encoding are the grant's own.
	Headers http.Header
	// FormPost asks the provider to answer with response_mode=form_post
	// (OAuth 2.0 Form Post Response Mode): the callback arrives as a
	// cross-site POST, which the plugin relays into the GET callback. Apple
	// requires it whenever a scope is requested; see AppleWithEmail.
	FormPost bool
	// PostedUser reads the user field a form-post callback may carry into
	// the profile after the exchange: Apple sends the person's name there,
	// once, on first consent, and nowhere else. Nil ignores the field.
	PostedUser func(raw string, p *Profile)
	// GetToken, when set, replaces the built-in code exchange: it receives
	// the callback's code, the flow's PKCE verifier ("" on a
	// provider-initiated sign-in, which has none) and the redirect URI, and
	// returns the provider's tokens. For a provider whose token endpoint is
	// not RFC 6749 (a GET, a JSON body, another parameter name). Everything
	// else stays the plugin's: the state, PKCE, the profile read from the
	// returned tokens (FetchProfile, FetchProfileFromTokens or the ID token),
	// linking and the session. An error it returns comes out of Exchange
	// unchanged; the callback logs it and answers ErrProviderFailed, as for
	// every failed exchange. A set without an AccessToken is a failed
	// exchange, and TokenLifetime fills a zero ExpiresAt. With it, TokenURL
	// is needed only for Refresh.
	GetToken func(ctx context.Context, code, verifier, redirectURI string) (TokenSet, error)
	// DisplayName is the provider's name for people ("Google", "Okta at
	// Acme"), listed by the providers route for a sign-in page's buttons.
	// The presets set it; empty falls back to ID.
	DisplayName string
	// NoPKCE leaves PKCE out of this provider's flow: no code_challenge on
	// the authorization URL and no code_verifier at the exchange. better-auth's
	// pkce: false, for a provider whose web flow has no PKCE and might refuse
	// or misread the parameters (Notion, TikTok's web Login Kit, WeChat). The
	// state is still single use and bound to the browser by the flow cookie,
	// and the client secret still guards the exchange; what is lost is RFC
	// 7636's defence against an authorization code stolen in transit and
	// injected into another browser's callback, the accepted risk of setting
	// it. Validate refuses it with PublicClient, where PKCE is the only thing
	// that ties the code to this client. The verifier is still generated, so
	// an IDToken.ProfileFromToken nonce keeps its binding to the flow.
	NoPKCE bool
	// ClientIDParam is the name the client id travels under on the
	// authorization URL and in the token, refresh and revocation forms;
	// empty is client_id (RFC 6749). TikTok calls it client_key (better-auth's
	// clientKey option), WeChat appid.
	ClientIDParam string
	// ClientSecretParam is the name of the client secret in the token,
	// refresh and revocation forms under ClientSecretPost; empty is
	// client_secret. WeChat calls it secret.
	ClientSecretParam string
	// ScopeSeparator joins the scopes in the scope parameter; empty is a
	// space (RFC 6749 section 3.3). TikTok and WeChat take a comma.
	ScopeSeparator string
	// AuthURLFragment is appended to the authorization URL as #fragment, for
	// a provider that requires one (WeChat's #wechat_redirect). Empty adds
	// none.
	AuthURLFragment string

	// CallbackParams are callback query parameters the token request
	// carries as well, for a provider whose exchange needs a value its
	// redirect returned: VK ID requires the device_id and the state it sent
	// back. Each must be one of CallbackParamValues; Validate refuses any
	// other. Empty forwards none, the RFC 6749 exchange.
	CallbackParams []CallbackParam
	// RefreshCallbackParams are callback values the refresh grant needs
	// again, long after the callback: VK ID's device_id. The exchange puts
	// them in TokenSet.RefreshParams, StoreTokens keeps them sealed with the
	// tokens, and RefreshWithParams sends them. Each must be one of
	// CallbackParamValues.
	RefreshCallbackParams []CallbackParam
	// contains filtered or unexported fields
}

OAuth2 is a provider that speaks plain OAuth 2.0 with PKCE and reads the profile with the access token. The URL fields are exported so a test can point them at a local server.

func Apple

func Apple(clientID string, secret func(ctx context.Context) (string, error), audiences ...string) *OAuth2

Apple is Sign in with Apple. The profile comes from the ID token Apple's token endpoint returns (Apple has no userinfo endpoint), verified against Apple's keys, and the same configuration accepts ID tokens from the iOS SDK through the id-token operation: audiences lists the native app's bundle ids. secret is AppleClientSecret, since Apple's client secret is a signed assertion, not a string.

Limits: no scopes are requested, so the web flow yields the subject without an address or a name, and nothing arrives by POST; the native flow's ID token carries the email when the user shared it. AppleWithEmail is the same provider with the email and name scopes, which Apple answers by form post.

func AppleWithEmail

func AppleWithEmail(clientID string, secret func(ctx context.Context) (string, error), audiences ...string) *OAuth2

AppleWithEmail is Apple with the email and name scopes. Apple answers such a flow with response_mode=form_post, so the provider sets FormPost: the callback arrives as a cross-site POST at the callback URL, which the plugin relays into its GET callback; register the same callback URL at Apple as for Apple. The email comes from the ID token (a private relay address when the person hid theirs), the name from the user field Apple sends once, on first consent, read by ApplePostedUser; both reach the profile and the hooks. The native flow is unchanged.

func Atlassian

func Atlassian(clientID, clientSecret string) *OAuth2

Atlassian is Atlassian account sign-in (Jira, Confluence): scopes read:me, read:account and offline_access, profile from /me, email verified when Atlassian says so.

func Cloudflare

func Cloudflare(clientID, clientSecret string) *OAuth2

Cloudflare is Cloudflare sign-in: scope user-details.read with a Basic-authenticated token request, profile from /client/v4/user. The answer has no verified flag, so the email is unverified.

func Discord

func Discord(clientID, clientSecret string) *OAuth2

Discord is Discord sign-in: scopes identify and email, profile from /users/@me; the email is verified when Discord says so.

func Dropbox

func Dropbox(clientID, clientSecret string) *OAuth2

Dropbox is Dropbox sign-in: scope account_info.read, profile from get_current_account (a POST), email verified when Dropbox says so.

func Dub

func Dub(clientID, clientSecret string) *OAuth2

Dub is Sign in with Dub, better-auth's dub plugin oauth option: PKCE (Dub enables it by default) and the user.read scope. The profile is /oauth/userinfo: id, name and image, no email, so a Dub sign-up creates a user with no address; the workspace the user authorized comes with it in Profile.Claims ("workspace"). Add Dub's API scopes (links.read, …) with Scopes or OptionalScopes and keep the tokens with StoreTokens to call the Dub API for the user.

func Facebook

func Facebook(clientID, clientSecret string) *OAuth2

Facebook is Facebook Login: scopes email and public_profile, profile from /me. The Graph API has no verified flag for the email, so it is unverified; TrustedProviders("facebook") trusts it.

func Figma

func Figma(clientID, clientSecret string) *OAuth2

Figma is Figma sign-in: scope current_user:read with a Basic-authenticated token request, profile from /v1/me; Figma has no verified flag, so the email is unverified.

func GitHub

func GitHub(clientID, clientSecret string) *OAuth2

GitHub is GitHub sign-in. The address comes from /user/emails, where GitHub says which address is primary and verified; the public profile email is neither.

func Google

func Google(clientID, clientSecret string) *OAuth2

Google is Google sign-in through its OpenID Connect userinfo endpoint.

func Gumroad

func Gumroad(clientID, clientSecret string) *OAuth2

Gumroad is Gumroad sign-in: scope view_profile, profile from /v2/user; Gumroad has no verified flag, so the email is unverified.

func HubSpot

func HubSpot(clientID, clientSecret string) *OAuth2

HubSpot is HubSpot sign-in: scope oauth, identity from the access-token introspection endpoint (user id and email; HubSpot has no name or picture there). The email is unverified: HubSpot does not say.

func Instagram

func Instagram(appID, appSecret string) *OAuth2

Instagram is sign-in with an Instagram professional (business or creator) account through the Instagram API with Instagram Login, as better-auth-instagram configures it: scopes comma-joined (instagram_business_basic), no PKCE (NoPKCE; Instagram documents none), and a token answer that is not RFC 6749: Instagram documents it wrapped in a data array ({"data":[{"access_token","user_id","permissions"}]}) and has answered it flat, so the exchange reads either. The token is the one-hour short-lived token (InstagramTokenLifetime) and cannot be refreshed: the long-lived exchange is a separate Graph API call the app makes with the stored token when it needs one. Instagram sends no email, so a sign-up creates a user with no address; better-auth-instagram invents "<id>@instagram.com" instead, which this preset does not, because an address nobody receives mail at would be verified-looking data that is false. "force_reauth" is an allowed per-request parameter (StartRequest.Params).

func Kakao

func Kakao(clientID, clientSecret string) *OAuth2

Kakao is Kakao Login: scopes account_email, profile_image and profile_nickname, profile from /v2/user/me, email verified when Kakao says so.

func Kick

func Kick(clientID, clientSecret string) *OAuth2

Kick is Kick sign-in: scope user:read, profile from /public/v1/users; Kick has no verified flag, so the email is unverified.

func Linear

func Linear(clientID, clientSecret string) *OAuth2

Linear is Linear sign-in: scope read, profile through the GraphQL viewer query. Linear accounts are email-verified by Linear, but its API has no flag, so the email is unverified here; TrustedProviders("linear") trusts it.

func Microsoft

func Microsoft(tenant, clientID, clientSecret string) *OAuth2

Microsoft is Microsoft Entra ID (work, school and personal accounts). The endpoints are static rather than discovered because the multi-tenant aliases publish a templated issuer the discovery check would refuse. The subject is the oid claim. Graph's userinfo has no email_verified, so the email is unverified: name the provider in TrustedProviders to link by it. With a single tenant the preset also accepts ID tokens (MSAL on mobile).

func Naver(clientID, clientSecret string) *OAuth2

Naver is Naver Login: the profile fields are chosen in the Naver console rather than by scope, and come from /v1/nid/me. Naver has no verified flag, so the email is unverified.

func Notion

func Notion(clientID, clientSecret string) *OAuth2

Notion is Sign in with Notion for a public integration: owner=user on the authorization URL and no scopes; the token request is HTTP Basic with a JSON body, as Notion documents it; the profile is the token's owner read from /v1/users/me (bot.owner.user), its email unverified. PKCE is off (NoPKCE): Notion documents none, and better-auth sends none.

func Patreon

func Patreon(clientID, clientSecret string) *OAuth2

Patreon is Patreon sign-in: scopes identity and identity[email], profile from the v2 identity endpoint, email verified when Patreon says so.

func PayPal

func PayPal(clientID, clientSecret string, sandbox bool) *OAuth2

PayPal is Log in with PayPal: scopes openid, profile and email with a Basic-authenticated token request, profile from the OpenID userinfo endpoint (subject user_id). sandbox switches the three endpoints to the sandbox environment.

func Polar

func Polar(clientID, clientSecret string) *OAuth2

Polar is Polar sign-in: scopes openid, profile and email, profile from the OpenID-shaped userinfo endpoint.

func Reddit

func Reddit(clientID, clientSecret, userAgent string) *OAuth2

Reddit is Reddit sign-in: scope identity with a Basic-authenticated token request, profile from /api/v1/me. Reddit's API refuses the default Go user agent, so userAgent names the app ("web:com.acme.app:v1.0 (by /u/acme)" is Reddit's recommended form); it is sent on the profile call. Reddit returns no email address.

func Snapchat

func Snapchat(clientID, clientSecret string) *OAuth2

Snapchat is Login with Snapchat (Login Kit), as better-auth-snapchat configures it: the scopes user.external_id, user.display_name and user.bitmoji.avatar as Snapchat's full scope URLs (SnapchatScopePrefix), the token endpoint with HTTP Basic as Snapchat documents it, PKCE S256 on, revocation of the refresh token at unlink, and the profile from Snap Kit's /v1/me: the externalId as subject, the display name and the Bitmoji avatar. Snapchat sends no email, so a sign-up creates a user with no address; better-auth-snapchat invents "<id>@snapchat.com", which this preset does not, for the reason Instagram gives.

func Spotify

func Spotify(clientID, clientSecret string) *OAuth2

Spotify is Spotify sign-in: scope user-read-email, profile from /v1/me; Spotify has no verified flag, so the email is unverified.

func TikTok

func TikTok(clientKey, clientSecret string) *OAuth2

TikTok is Login Kit for Web. clientKey is the app's client key, TikTok's name for the client id (better-auth's clientKey), sent as client_key on the authorization URL and in the token, refresh and revocation forms. Scope user.info.basic, comma-joined; the subject is open_id; TikTok has no email, so the user is created with no identity. PKCE is off (NoPKCE): the web flow documents none, and better-auth sends no challenge either. Unlink revokes the token at TikTok's revoke endpoint.

func VK

func VK(clientID, clientSecret string) *OAuth2

VK is Sign in with VK ID: PKCE S256, scopes email and phone, and a token request that carries the device_id and the state the callback returned (CallbackParams), as VK ID requires. The profile is read by posting the access token and client id to /oauth2/user_info; the email is unverified. VK ID's refresh grant also requires the device_id: it is kept, sealed, with the stored tokens (RefreshCallbackParams, under StoreTokens) and sent with every refresh.

func Vercel

func Vercel(clientID, clientSecret string) *OAuth2

Vercel is Sign in with Vercel: scopes openid, profile and email, profile from the OpenID-shaped userinfo endpoint.

func WeChat

func WeChat(appID, appSecret, lang string) *OAuth2

WeChat is WeChat Login for a website app (QR code): appid instead of client_id, scope snsapi_login, #wechat_redirect on the authorization URL, lang on the QR page (WeChatLangCN, better-auth's default, or WeChatLangEN). The token and refresh endpoints are GETs that take appid and secret in the query, as WeChat documents them; the secret therefore travels in a URL, which this provider cannot avoid. The subject is the unionid when WeChat sends one, else the openid; WeChat has no email. PKCE is off (NoPKCE): WeChat has none.

func X

func X(clientID, clientSecret string) *OAuth2

X is Sign in with X (Twitter): PKCE with a Basic-authenticated token request, scopes users.read, tweet.read, offline.access and users.email, profile from /2/users/me. The email is the account's confirmed address, so it is verified when present.

func Yandex

func Yandex(clientID, clientSecret string) *OAuth2

Yandex is Yandex ID sign-in: scopes login:info, login:email and login:avatar, profile from login.yandex.ru/info with the OAuth authorization scheme. Yandex has no verified flag, so the email is unverified.

func Zoom

func Zoom(clientID, clientSecret string) *OAuth2

Zoom is Zoom sign-in: a Basic-authenticated token request and the profile from /v2/users/me; scopes are set on the app in the Zoom marketplace.

func (*OAuth2) ApplyPosted

func (o *OAuth2) ApplyPosted(raw string, p *Profile)

ApplyPosted runs PostedUser on p when the provider has one and the callback carried a user field.

func (*OAuth2) AuthCodeURL

func (o *OAuth2) AuthCodeURL(state, challenge, redirectURI string) string

AuthCodeURL builds the authorization URL. An AuthURL that already carries a query, as some tenants' endpoints do, keeps it.

func (*OAuth2) AuthCodeURLWithParams

func (o *OAuth2) AuthCodeURLWithParams(state, challenge, redirectURI string, rp RequestParams) string

AuthCodeURLWithParams builds the authorization URL with the provider's AuthParams and the request's parameters. Protocol parameters win over both, so neither can replace the state or the challenge.

func (*OAuth2) EndSession

func (o *OAuth2) EndSession(context.Context, *http.Client) (string, error)

EndSession returns EndSessionURL.

func (*OAuth2) Exchange

func (o *OAuth2) Exchange(ctx context.Context, hc *http.Client, code, verifier, redirectURI string) (Profile, error)

Exchange trades the code for an access token and reads the profile.

func (*OAuth2) ExchangeInitiated

func (o *OAuth2) ExchangeInitiated(ctx context.Context, hc *http.Client, code, redirectURI string) (Profile, error)

ExchangeInitiated implements InitiatedExchanger: the code is exchanged with no verifier, the ID token verified with no nonce allowed, and the profile read as Exchange reads it, whose subject must be the token's.

func (*OAuth2) ExpectedIssuer

func (o *OAuth2) ExpectedIssuer() string

ExpectedIssuer returns the AuthorizationIssuer field.

func (*OAuth2) FetchProfileWith

func (o *OAuth2) FetchProfileWith(ctx context.Context, hc *http.Client, ts TokenSet) (Profile, error)

FetchProfileWith runs FetchProfileFromTokens, else FetchProfile with the access token.

func (*OAuth2) Label

func (o *OAuth2) Label() string

Label returns DisplayName, or ID when it is empty.

func (*OAuth2) Load

func (o *OAuth2) Load(ctx context.Context, hc *http.Client) error

Load fetches the ID-token key set when the provider accepts ID tokens; a provider without one has nothing to load.

func (*OAuth2) Name

func (o *OAuth2) Name() string

Name returns ID, the route segment and stored provider name.

func (*OAuth2) OptionalParamList

func (o *OAuth2) OptionalParamList() []string

OptionalParamList returns OptionalParams.

func (*OAuth2) OptionalScopeList

func (o *OAuth2) OptionalScopeList() []string

OptionalScopeList returns OptionalScopes.

func (*OAuth2) PostsBack

func (o *OAuth2) PostsBack() bool

PostsBack reports FormPost: the authorization response arrives as a POST.

func (*OAuth2) Refresh

func (o *OAuth2) Refresh(ctx context.Context, hc *http.Client, refreshToken auth.Token) (TokenSet, error)

Refresh runs the refresh_token grant. A provider that does not rotate refresh tokens answers without one; the caller keeps the old one then.

func (*OAuth2) RefreshWithParams

func (o *OAuth2) RefreshWithParams(ctx context.Context, hc *http.Client, refreshToken auth.Token, params url.Values) (TokenSet, error)

RefreshWithParams is Refresh that also sends params, the account's stored TokenSet.RefreshParams (VK ID's device_id); the protocol's own keys in it are ignored. The parameters stay with the new token set.

func (*OAuth2) Revoke

func (o *OAuth2) Revoke(ctx context.Context, hc *http.Client, ts TokenSet) error

Revoke posts the token to RevokeURL with the client credentials, per RFC 7009 section 2.1. The RFC makes an unknown token a success (200), so only a transport failure or another status is an error.

func (*OAuth2) VerifyIDToken

func (o *OAuth2) VerifyIDToken(ctx context.Context, hc *http.Client, token, nonce string) (Profile, error)

VerifyIDToken implements IDTokenVerifier for a plain OAuth 2.0 provider with an IDTokenConfig.

type OIDCProvider

type OIDCProvider struct {
	ID           string
	Issuer       string
	ClientID     string
	ClientSecret string
	Scopes       []string
	// TokenAuth, AuthParams, TokenParams, RefreshParams, OptionalScopes,
	// OptionalParams, TokenLifetime and ClientAssertion mean what they mean on OAuth2; they
	// are copied onto the discovered endpoints.
	TokenAuth       TokenAuth
	AuthParams      url.Values
	TokenParams     url.Values
	RefreshParams   url.Values
	OptionalScopes  []string
	OptionalParams  []string
	TokenLifetime   time.Duration
	ClientAssertion func(ctx context.Context, tokenURL string) (string, error)
	// SubjectClaim is the userinfo claim taken as the account's subject
	// instead of sub, for an issuer whose stable identifier is another claim
	// (Entra ID's oid, which survives a tenant's app re-registration where
	// sub does not). Empty is sub.
	SubjectClaim string
	// RevokeURL overrides the discovery document's revocation_endpoint;
	// empty takes the discovered one, and unlink revokes stored tokens there.
	RevokeURL string
	// IDToken configures ID-token sign-in. JWKSURL and Issuers are filled
	// from the discovery document (jwks_uri and the configured issuer) when
	// left empty, so an OIDC provider accepts ID tokens with no extra field;
	// Audiences, ProfileFromToken and RequireNonce are copied as given.
	IDToken IDTokenConfig
	// EndSessionURL overrides the discovery document's end_session_endpoint;
	// empty takes the discovered one.
	EndSessionURL string
	// Headers means what it means on OAuth2: added to every token, refresh
	// and revocation request at the discovered endpoints.
	Headers http.Header
	// DiscoveryHeaders are added to the discovery request, for an issuer
	// behind a gateway that wants a header on it; Authorization is ignored,
	// since a discovery document is public and takes no credential.
	DiscoveryHeaders http.Header
	// GetToken means what it means on OAuth2: it replaces the code exchange
	// at the discovered token endpoint.
	GetToken func(ctx context.Context, code, verifier, redirectURI string) (TokenSet, error)
	// DisplayName is the provider's name for people, as on OAuth2; the
	// presets set it, and empty falls back to ID.
	DisplayName string
	// Fallback are static endpoints used ONLY while the discovery document
	// cannot be fetched (a transport error, a status other than 2xx, a body
	// that is not JSON), so an issuer whose discovery is down still signs
	// people in. A document that arrives but names another issuer or lacks
	// an endpoint is still refused: the issuer answered, and what it said is
	// wrong. Each use is logged at Warn on Logger, and discovery is tried
	// again after discoveryRetryInterval. The zero value has no fallback: a
	// failed discovery fails that flow with ErrProviderFailed, as before.
	Fallback Endpoints
	// Logger receives the fallback warning; nil is slog.Default(). The oauth
	// plugin sets it to the core's logger when it is nil.
	Logger *slog.Logger
	// contains filtered or unexported fields
}

OIDCProvider is any OpenID Connect issuer, configured from its discovery document: Okta, Auth0, Keycloak, Entra ID, Zitadel. The profile comes from the userinfo endpoint with the access token, which the specification makes authoritative, so no ID-token signature check is needed on this path.

It discovers lazily at the first flow and caches the result on the value; a failed discovery is retried at the next flow rather than kept.

func Auth0

func Auth0(domain, clientID, clientSecret string) *OIDCProvider

Auth0 is an Auth0 tenant through OpenID Connect; domain is the tenant domain such as "acme.us.auth0.com", with or without the scheme.

func Cognito

func Cognito(region, userPoolID, clientID, clientSecret string) *OIDCProvider

Cognito is an Amazon Cognito user pool through OpenID Connect: the pool's discovery document publishes its hosted UI as the authorization endpoint.

func GitLab

func GitLab(issuer, clientID, clientSecret string) *OIDCProvider

GitLab is GitLab sign-in through OpenID Connect; issuer is GitLabIssuer or a self-hosted instance.

func HuggingFace

func HuggingFace(clientID, clientSecret string) *OIDCProvider

HuggingFace is Hugging Face sign-in through OpenID Connect.

func Keycloak

func Keycloak(issuer, clientID, clientSecret string) *OIDCProvider

Keycloak is a Keycloak realm through OpenID Connect; issuer is "https://<host>/realms/<realm>".

func LINE

func LINE(clientID, clientSecret string) *OIDCProvider

LINE is LINE Login through OpenID Connect; the email scope needs LINE's approval for the channel.

func LinkedIn

func LinkedIn(clientID, clientSecret string) *OIDCProvider

LinkedIn is Sign in with LinkedIn through OpenID Connect; the product "Sign In with LinkedIn using OpenID Connect" must be enabled on the app.

func OIDC

func OIDC(name, issuer, clientID, clientSecret string) *OIDCProvider

OIDC builds a provider for issuer. name is the route segment and the stored provider name, such as "okta".

func Okta

func Okta(issuer, clientID, clientSecret string) *OIDCProvider

Okta is an Okta org or authorization server through OpenID Connect; issuer is "https://<org>.okta.com" or "https://<org>.okta.com/oauth2/<server>".

func Paybin

func Paybin(clientID, clientSecret string) *OIDCProvider

Paybin is Sign in with Paybin through OpenID Connect.

func Railway

func Railway(clientID, clientSecret string) *OIDCProvider

Railway is Railway sign-in through OpenID Connect; the userinfo endpoint is its /oauth/me.

func Roblox

func Roblox(clientID, clientSecret string) *OIDCProvider

Roblox is Roblox sign-in through OpenID Connect. Roblox has no email scope, so the profile carries the subject, name and picture only.

func Salesforce

func Salesforce(issuer, clientID, clientSecret string) *OIDCProvider

Salesforce is Salesforce sign-in through OpenID Connect; issuer is SalesforceIssuer, SalesforceSandboxIssuer or a My Domain.

func Slack

func Slack(clientID, clientSecret string) *OIDCProvider

Slack is Sign in with Slack through OpenID Connect.

func Twitch

func Twitch(clientID, clientSecret string) *OIDCProvider

Twitch is Twitch sign-in through OpenID Connect, with the claims request Twitch needs before its userinfo endpoint sends the email, picture and username.

func Zitadel

func Zitadel(issuer, clientID, clientSecret string) *OIDCProvider

Zitadel is a Zitadel instance through OpenID Connect; issuer is the instance's URL.

func Zoho

func Zoho(region ZohoRegion, clientID, clientSecret string) *OIDCProvider

Zoho is Sign in with Zoho through OpenID Connect at region's accounts server, as better-auth-zoho configures it (discovery, scopes openid, email and profile). Use the data centre the app was registered in.

func (*OIDCProvider) AuthCodeURL

func (o *OIDCProvider) AuthCodeURL(state, challenge, redirectURI string) string

AuthCodeURL builds the authorization URL from the discovered endpoint. It returns "" before discovery; the plugin always discovers first.

func (*OIDCProvider) AuthCodeURLWithParams

func (o *OIDCProvider) AuthCodeURLWithParams(state, challenge, redirectURI string, rp RequestParams) string

AuthCodeURLWithParams is AuthCodeURL with the request's parameters.

func (*OIDCProvider) Discover

func (o *OIDCProvider) Discover(ctx context.Context, hc *http.Client) error

Discover reads the discovery document once. It refuses a document whose issuer is not the configured one, the check the specification requires so a redirected discovery URL cannot hand the flow to another issuer.

func (*OIDCProvider) EndSession

func (o *OIDCProvider) EndSession(ctx context.Context, hc *http.Client) (string, error)

EndSession returns the issuer's end_session_endpoint, discovering first; "" when the issuer publishes none.

func (*OIDCProvider) Exchange

func (o *OIDCProvider) Exchange(ctx context.Context, hc *http.Client, code, verifier, redirectURI string) (Profile, error)

Exchange trades the code for the profile through the discovered endpoints.

func (*OIDCProvider) ExchangeInitiated

func (o *OIDCProvider) ExchangeInitiated(ctx context.Context, hc *http.Client, code, redirectURI string) (Profile, error)

ExchangeInitiated discovers the issuer and exchanges through its endpoints; an issuer whose document has no jwks_uri cannot verify the ID token and fails.

func (*OIDCProvider) ExpectedIssuer

func (o *OIDCProvider) ExpectedIssuer() string

ExpectedIssuer returns the iss the callback must carry: the configured issuer when the discovery document says it is sent, "" otherwise or before discovery (the plugin discovers at start, before any callback).

func (*OIDCProvider) FetchProfileWith

func (o *OIDCProvider) FetchProfileWith(ctx context.Context, hc *http.Client, ts TokenSet) (Profile, error)

FetchProfileWith reads the userinfo endpoint with the tokens' access token.

func (*OIDCProvider) Label

func (o *OIDCProvider) Label() string

Label returns DisplayName, or ID when it is empty.

func (*OIDCProvider) Load

func (o *OIDCProvider) Load(ctx context.Context, hc *http.Client) error

Load discovers the issuer (a Fallback in use counts) and fetches the key set it names.

func (*OIDCProvider) Name

func (o *OIDCProvider) Name() string

Name returns ID, the route segment and stored provider name.

func (*OIDCProvider) OptionalParamList

func (o *OIDCProvider) OptionalParamList() []string

OptionalParamList returns OptionalParams.

func (*OIDCProvider) OptionalScopeList

func (o *OIDCProvider) OptionalScopeList() []string

OptionalScopeList returns OptionalScopes.

func (*OIDCProvider) Refresh

func (o *OIDCProvider) Refresh(ctx context.Context, hc *http.Client, refreshToken auth.Token) (TokenSet, error)

Refresh runs the refresh grant at the discovered token endpoint.

func (*OIDCProvider) Revoke

func (o *OIDCProvider) Revoke(ctx context.Context, hc *http.Client, ts TokenSet) error

Revoke revokes the tokens at the discovered revocation_endpoint; nothing when the issuer publishes none.

func (*OIDCProvider) VerifyIDToken

func (o *OIDCProvider) VerifyIDToken(ctx context.Context, hc *http.Client, token, nonce string) (Profile, error)

VerifyIDToken implements IDTokenVerifier for an OpenID Connect issuer: the key set is the discovery document's jwks_uri and the issuer the configured one, so no field needs filling by hand.

type Option

type Option func(*Plugin)

Option configures the plugin; pass options to New.

func AllowIDTokenReplay

func AllowIDTokenReplay() Option

AllowIDTokenReplay turns ID-token replay protection off: a captured token then signs in, or re-authenticates its user's session, as often as its holder likes until it expires (minutes to an hour at the providers). For an app whose client legitimately posts one token twice and cannot ask the SDK for a new one. auth.New logs WarnIDTokenReplayAllowed when it is set.

func AllowProviderInitiated

func AllowProviderInitiated(provider string, more ...string) Option

AllowProviderInitiated lets the named providers start a sign-in themselves: a tile on the company's identity dashboard (Okta, Entra ID, Google Workspace) that sends the browser to the callback with a code and no state. Off by default for every provider, and only for providers that verify ID tokens (*OIDCProvider, or *OAuth2 with an IDToken key set; auth.New refuses any other with ErrProviderInitiatedConfig).

On that path the code is exchanged without PKCE, and the token response must carry an ID token whose signature, issuer, audience and lifetime verify; it must carry no nonce (one would mean it was issued for another flow), and its subject must be the profile's. The person is signed in only as the user already linked to that provider subject, or as a new user; an address that matches an existing account is ErrAccountExists, never a link, and a signed-in browser is signed out first, never linked. It lands on the site root.

The risk, which this switch accepts: without a state bound to the browser, anyone who holds a code for their own account at the provider can send someone else's browser to the callback with it and sign that browser in as themselves (login forgery). The victim then works inside the attacker's account. Enable it only for a provider whose dashboard people actually use, and keep it off for consumer providers. RestartProviderInitiated is the mode without that risk, at the price of a second trip to the provider.

func AllowUnlinkingAll

func AllowUnlinkingAll() Option

AllowUnlinkingAll lets a user unlink the last provider account even when nothing else signs them in, better-auth's accountLinking.allowUnlinkingAll. Off by default: unlink refuses to remove the last sign-in method, because a user who does it cannot sign in again and only an account recovery (a password reset to a verified address, an admin) brings them back. For apps where signing in again is a fresh sign-up by design.

func CallbackURL

func CallbackURL(fn func(provider string) string) Option

CallbackURL sets the redirect URI registered at each provider. The default is BaseURL + DefaultMountPrefix + "/oauth/<provider>/callback".

func ErrorURL

func ErrorURL(path string) Option

ErrorURL sends the browser to path, a page on this site, when a callback fails, with the error code in the query (`?error=oauth.denied`), instead of answering the error as JSON. For apps whose sign-in page shows the message; a request may name another page with StartRequest.ErrorTo. Without it, callbacks answer errors as JSON, as every operation does.

func ExplicitSignUp

func ExplicitSignUp(providers ...string) Option

ExplicitSignUp makes a callback create a user only when the request asked for it (StartRequest.SignUp, IDTokenRequest.SignUp); a sign-in for a provider account nobody holds is ErrSignUpNotRequested. For apps with separate "sign in" and "sign up" buttons, where a person who meant to sign in should not end up with a second account. Linked accounts, verified-email matches and link flows are unaffected. With provider names, only those providers need the flag; with none, all do. NoSignUp wins where both apply.

func ExposeAccessTokens

func ExposeAccessTokens() Option

ExposeAccessTokens adds POST /access-token {"provider"}: the signed-in user's own access token at that provider, refreshed when it is within RefreshMargin of expiring, with its expiry, granted scopes and ID token. better-auth's getAccessToken from the client, for a browser or mobile app that calls the provider's API itself (Google Drive from the page). Off by default, because it moves a bearer credential for another service into the browser: any script that runs on the app's origin (one XSS) can then read it and call the provider as the user until it expires. The refresh token is never sent; the route needs a signed-in session and the origin check, and requires StoreTokens (ErrExposeNeedsStoreTokens).

func LinkUnverifiedLocalEmail

func LinkUnverifiedLocalEmail() Option

LinkUnverifiedLocalEmail is better-auth's accountLinking.requireLocalEmailVerified: false. The implicit link by email then also links a provider account with a verified address to a local account holding that address unverified, instead of refusing with ErrAccountExists. The provider side must still be verified (or trusted). Off by default; Init logs WarnLinkUnverifiedLocal at every start while it is on.

The attack it opens is account pre-hijacking: anyone can register the victim's address with a password of their own and leave it unverified; when the victim later signs in with their provider, the provider account is linked to that local account, and the attacker's password still signs in to it, now holding the victim's data. Use it only where nobody can create an account with an address they do not control (no self sign-up, or every sign-up verified before any credential is set).

func NoImplicitLinking

func NoImplicitLinking() Option

NoImplicitLinking turns off the automatic link by verified email: a provider account whose address matches an existing user is refused with ErrAccountExists even when both sides are verified, and the person links from settings after signing in the usual way. For apps that want every link to be a deliberate act.

func NoSignUp

func NoSignUp(providers ...string) Option

NoSignUp stops callbacks from creating users: a provider account that is already linked signs in, a verified address that matches a verified identity links and signs in, a signed-in user may link, and anything else is ErrSignUpDisabled. For apps where accounts are invited, not opened. With provider names, only those providers are closed to sign-up; with none, all are.

func OnResolved

func OnResolved(fn ResolvedHook) Option

OnResolved adds a ResolvedHook; several run in order.

func PostLogoutURL

func PostLogoutURL(fn func(provider string) string) Option

PostLogoutURL sets the post_logout_redirect_uri sent with a provider logout, per provider; it must be registered at the provider. The default is the base URL. A logout request's own ReturnTo, once it passes TrustsReturnURL, replaces it for that request.

func RequireEmail

func RequireEmail() Option

RequireEmail refuses a sign-in or sign-up whose provider profile carries no email at all (ErrEmailNotFound), verified or not, so every user this plugin creates can be reached and can recover the account. A link flow by a signed-in user is not affected.

func RequireSameEmailOnLink() Option

RequireSameEmailOnLink refuses a link flow whose provider account carries an address that is not one of the signed-in user's identities (ErrEmailMismatch), so a settings page cannot attach a stranger's account by mistake. Off by default: linking from a signed-in session links whatever the provider returns.

func RequireVerifiedEmail

func RequireVerifiedEmail() Option

RequireVerifiedEmail refuses a sign-in or sign-up whose provider profile has no email or an unverified one (ErrEmailUnverified), so no session is ever issued on an address the provider does not vouch for. A link flow by a signed-in user is not affected: the user is already proven.

func RestartProviderInitiated

func RestartProviderInitiated(provider string, more ...string) Option

RestartProviderInitiated is the other answer to a provider-initiated sign-in, better-auth's allowIdpInitiated: a callback with a code and no state, for the named providers, discards the code and starts an ordinary flow in its place (a fresh state bound to the browser, PKCE, the nonce), so the browser goes back to the provider, which signs the person straight through when they are signed in there, and the callback that follows is checked like any other. The forged code is never exchanged, so this mode cannot sign a browser in as someone else: at worst it signs a person in as themselves. It works for every provider, and lands on the site root. It costs one more round trip than AllowProviderInitiated, and a provider that shows a consent or account-picker page shows it again. A provider may not be named in both (ErrProviderInitiatedConfig).

func SkipBrokenProviders

func SkipBrokenProviders() Option

SkipBrokenProviders leaves out, with an Error log, a provider whose configuration fails its check or whose discovery document or key set cannot be loaded, so the other providers keep working: it disappears from the routes, the providers list and every lookup by name, and comes back when the app restarts with it fixed. With it, auth.New also loads every discovery document and key set at once (in parallel, each bounded by the HTTP client's timeout) instead of at the first flow, because that is the only moment a provider can be left out; a Fallback in use counts as loaded. auth.New still fails when every provider is broken, since there is nothing left to keep working. Without it (the default) nothing is skipped: a bad configuration fails auth.New, and an unreachable issuer fails the flow that needed it with ErrProviderFailed.

func SkipStateCookieCheck

func SkipStateCookieCheck() Option

SkipStateCookieCheck accepts a callback whose state does not match the flow cookie, or that arrives with no flow cookie at all: better-auth's account.skipStateCookieCheck. The state is still required, still single use and still checked against its stored row, provider and expiry. Off by default; Init logs WarnStateCookieCheckOff at every start while it is on.

The attack it opens is login CSRF. The flow cookie is the only thing that ties a callback to the browser that started the flow; without it an attacker starts a flow, signs in at the provider as themself, stops before the callback and sends the victim that callback URL, whose state is valid in the database. The victim is then signed in to the attacker's account, and whatever they save there (a card, a document, a linked provider) the attacker reads. Its one honest use is a callback that lands where the flow cookie cannot reach (another domain, an embedded browser that drops cookies); prefer authhttp's CookieDomain or the desktop hand-off there, which keep the binding.

func StateInCookie

func StateInCookie() Option

StateInCookie is better-auth's account.storeStateStrategy "cookie": a flow's state and metadata (return address, PKCE verifier, flow data) are sealed into the __Host-auth_flow cookie instead of a verification row, so a sign-in writes nothing before the callback and needs no shared store between instances. The state is still random per flow, the cookie is sealed with a key derived from auth.WithSecret (auth.KeyLabelOAuthState, which Validate requires: ErrStateCookieKey), bound to the provider, and expires with WithStateTTL; the callback clears it, so a state is used once by the browser that holds it. It cannot be combined with SkipStateCookieCheck, which drops the very cookie that holds the state.

func StoreTokens

func StoreTokens(key ...[]byte) Option

StoreTokens keeps each provider's access and refresh token sealed (AES-256-GCM), for apps that call the provider's API later; read them back with Tokens. Without it, provider tokens are never stored. With a key (32 bytes) that key seals; with none, the keys derived from auth.WithSecret under auth.KeyLabelOAuthTokens do, current secret first, so a rotated secret still opens tokens sealed under the previous one. One of the two is required (ErrBadTokenKey otherwise), and an explicit key wins.

func StoreTokensWith

func StoreTokensWith(s auth.Sealer) Option

StoreTokensWith is StoreTokens with the tokens sealed by s instead of a key held in memory: a KMS or HSM behind the auth.Sealer interface (Vault transit, AWS KMS). It wins over auth.WithSealer and loses to a StoreTokens key. Each token is sealed with the account id as additional data, so s must refuse a value opened under another id. A seal error fails the sign-in that would have stored the tokens; an open error is reported by Tokens, never an empty token.

func TrustedProviders

func TrustedProviders(providers ...string) Option

TrustedProviders names providers whose addresses are taken as verified even when the profile does not say so, so they may link to an existing verified identity by email. Use it only for a provider you know verifies addresses before returning them: a trusted provider that does not is an account takeover for anyone who can register that address there. The local identity must still be verified.

func UpdateProfileOnSignIn

func UpdateProfileOnSignIn() Option

UpdateProfileOnSignIn copies the provider's name and image onto the user at every sign-in and link, not only when the user is created, for apps whose users expect a new avatar at the provider to show up here.

func ValidateProfile

func ValidateProfile(fn ProfileHook) Option

ValidateProfile adds a ProfileHook; several run in order and the first error wins.

func VerifyEmailBeforeSignIn

func VerifyEmailBeforeSignIn() Option

VerifyEmailBeforeSignIn is better-auth's requireEmailVerification for social sign-in: the callback (and an id-token or provider-initiated sign-in) creates or resolves the user as usual, but issues no session while the user has no verified email address. The browser lands on the error page (StartRequest.ErrorTo, ErrorURL or auth.WithErrorURL; the landing page when none is set) with `?error=oauth.email_not_verified`, and the result says EmailVerificationRequired. The person verifies the address the provider gave (plugin/email's resend and verify routes, which need no session) and signs in again. A profile with no email is refused with ErrEmailNotFound before anything is written, since such a user could never be released.

RequireVerifiedEmail is the stricter alternative: it refuses an unverified provider address outright and creates nothing.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient replaces the client used for provider calls, which by default times out after DefaultHTTPTimeout. A client whose CheckRedirect is nil is used as a copy that refuses redirects (ErrRedirectRefused), like the default one; an app that means its provider calls to follow redirects sets a CheckRedirect of its own.

func WithIDTokenReplayGuard

func WithIDTokenReplayGuard(g IDTokenReplayGuard) Option

WithIDTokenReplayGuard replaces the default guard (a claim row in the core's verification store) with g: a shared cache, a dedicated table, a service. A nil g is refused (auth.ErrNilOption); turning the check off is AllowIDTokenReplay, never a nil.

func WithProviders

func WithProviders(ps ...Provider) Option

WithProviders adds providers. The constructors Google and GitHub are the usual values.

func WithServerContext

func WithServerContext(fn ServerContextFunc) Option

WithServerContext adds a ServerContextFunc; several run in order, and a later one's key replaces an earlier one's. Unlike StartRequest.Data, which the client chooses and the app must validate, these values come from the server and need no check at the callback.

func WithStateTTL

func WithStateTTL(d time.Duration) Option

WithStateTTL overrides DefaultStateTTL, the time a user may spend at the provider before the callback is refused as expired.

type ParamRefresher

type ParamRefresher interface {
	Refresher
	RefreshWithParams(ctx context.Context, hc *http.Client, refreshToken auth.Token, params url.Values) (TokenSet, error)
}

ParamRefresher is a Refresher whose refresh grant takes the account's own stored parameters (TokenSet.RefreshParams). *OAuth2 implements it; the plugin calls it instead of Refresh when an account has stored parameters.

type ParamsAllowLister

type ParamsAllowLister interface {
	ParamsProvider
	// OptionalParamList is the parameter names a request may set.
	OptionalParamList() []string
}

ParamsAllowLister is a ParamsProvider that names the extra parameters a start request may set (OptionalParams). *OAuth2 and *OIDCProvider implement it; a start with StartRequest.Params at a provider that does not is refused with ErrRequestParams.

type ParamsProvider

type ParamsProvider interface {
	Provider
	// AuthCodeURLWithParams is AuthCodeURL plus the request's parameters.
	AuthCodeURLWithParams(state, codeChallenge, redirectURI string, rp RequestParams) string
	// OptionalScopeList is the scopes a request may add.
	OptionalScopeList() []string
}

ParamsProvider is a Provider whose authorization URL takes per-request parameters. *OAuth2 and *OIDCProvider implement it; a Provider that does not is refused a start that carries any (ErrRequestParams), rather than silently dropping what the caller asked for.

type Plugin

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

Plugin is the OAuth sign-in method: a start and a callback operation per provider, plus accounts and unlink. Build it with New.

func New

func New(opts ...Option) *Plugin

New builds the plugin and defines each provider's operations. Providers must be passed here: operations are defined once, at construction.

func (*Plugin) Accounts

func (p *Plugin) Accounts(ctx context.Context, userID string) ([]Account, error)

Accounts lists a user's linked accounts, oldest first; accounts linked in the same instant come in id order, so every engine returns one order. It takes the user id, for an app's admin pages; the accounts operation is the signed-in user's own view.

func (*Plugin) Callback

func (p *Plugin) Callback(ctx context.Context, provider string, in CallbackRequest) (CallbackResult, error)

Callback finishes a flow from Go code, the typed form of GET <prefix>/oauth/<provider>/callback: in is what the provider sent back. The state must match the flow cookie of the request ctx carries, as it must over HTTP, so a callback received by the app's own handler passes that handler's request context. The session the callback issues reaches that request's response; the result carries it too (Token) for a caller with no response to write to. ErrProviderUnknown as for Start.

func (*Plugin) ImportAccount

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

ImportAccount links a provider account from another system to a user, from Go only: how an import writes auth_account without touching the table by hand, so a person who signed in with Google before keeps landing on the same user. The provider must be configured (ErrProviderUnknown), the user must exist (auth.ErrUserNotFound), and the provider account must not be linked already (ErrAccountLinkedElsewhere). No tokens are stored and no hook runs: the person did not sign in. It joins the transaction in ctx when there is one.

func (*Plugin) Init

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

Init captures the core, fills the default callback URL, hands OIDC providers the core's logger, and under SkipBrokenProviders loads every provider and leaves the broken ones out.

func (*Plugin) Name

func (p *Plugin) Name() string

Name is PluginName; it never changes.

func (*Plugin) Operations

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

Operations lists every start, callback, form-post, id-token and logout operation of the providers in use (SkipBrokenProviders may have left some out), then unlink, accounts, the providers list, re-authentication (and its ID-token form when a provider verifies ID tokens) and the general logout, for the transports to mount.

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) Profile

func (p *Plugin) Profile(ctx context.Context, userID, provider string) (Profile, error)

Profile reads userID's current profile at provider with the stored tokens (refreshing them as Tokens does), for an app that wants the provider's latest name, picture or claims without a new sign-in. The provider must be a ProfileFetcher (ErrProfileUnsupported otherwise); a failed call is ErrProviderFailed with the reason logged. Nothing is written to the user: what to copy is the app's decision.

func (*Plugin) Providers

func (p *Plugin) Providers() []ProviderInfo

Providers lists the providers in use, in configuration order, with their display names: what a sign-in page needs to draw one button per provider (each linking to /oauth/<id>/start). A provider SkipBrokenProviders left out is not listed.

func (*Plugin) RateLimits

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

RateLimits bounds flow starts, each of which writes a state row (the re-authentication start per user too), id-token sign-ins, each of which verifies a signature, and the callbacks of providers open to provider-initiated sign-in, where a request with no state costs a token exchange.

func (*Plugin) Reauthenticate

func (p *Plugin) Reauthenticate(ctx context.Context, in ReauthenticateRequest) (StartResult, error)

Reauthenticate starts a re-authentication with in.Provider for the signed-in user and returns the provider URL the browser must navigate to.

func (*Plugin) ReauthenticateIDToken

func (p *Plugin) ReauthenticateIDToken(ctx context.Context, in ReauthenticateIDTokenRequest) (auth.Reauthentication, error)

ReauthenticateIDToken re-authenticates the signed-in user with a provider ID token, for a Go caller holding the request context. Without a provider that verifies ID tokens it is ErrIDTokenUnsupported.

func (*Plugin) Refresh

func (p *Plugin) Refresh(ctx context.Context, userID, provider string) (TokenSet, error)

Refresh renews userID's tokens at provider now, whatever their expiry, and stores the result: for an app that was told the token was revoked, or that wants a token good for a long job. ErrNoTokens without stored tokens, ErrCannotRefresh without a refresh token or at a provider that is not a Refresher, ErrRefreshFailed when the provider refuses.

func (*Plugin) SignInCredentials

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

SignInCredentials counts linked accounts, for the core's last-way-in check.

func (*Plugin) SignInMethods

func (p *Plugin) SignInMethods() []authhttp.SignInMethod

SignInMethods offers one button per provider in use, in configuration order, to pages that render sign-in methods (authui's sign-in and sign-up pages among them): ID "oauth.<provider>", Kind UIMethodKind, Label "Continue with <display name>", Route the provider's GET start operation. A provider SkipBrokenProviders left out, or whose start operation auth.WithDisabledOperations switched off, gets no button. Before auth.New wires the plugin there are no routes, so the list is empty.

func (*Plugin) SignsIn

func (p *Plugin) SignsIn() bool

SignsIn reports that providers are a way in, so an app with only OAuth passes auth.New's "no sign-in method" check.

func (*Plugin) Start

func (p *Plugin) Start(ctx context.Context, provider string, in StartRequest) (StartResult, error)

Start begins a sign-in, sign-up or link flow at provider from Go code, the typed form of GET <prefix>/oauth/<provider>/start: a GraphQL resolver or a handler of the app's own that wants the authorization URL. The flow's state is bound to the browser with the flow cookie on the response of the request ctx carries, so ctx must carry one (the authhttp middleware, or a contrib adapter); without it the binding is dropped and the callback answers ErrStateInvalid. The result's URL is where to send the browser; over HTTP the operation also redirects there. ErrProviderUnknown for a provider that is not configured or that SkipBrokenProviders left out.

func (*Plugin) Tables

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

Tables declares auth_account and its alias, so auth.New can refuse a second owner of either.

func (*Plugin) Tokens

func (p *Plugin) Tokens(ctx context.Context, userID, provider string) (TokenSet, error)

Tokens returns the stored provider tokens of userID's account at provider.

An access token that expires within RefreshMargin is refreshed first when the account has a refresh token and the provider is a Refresher; the new tokens are stored. Two callers refreshing at once both succeed with a provider that keeps refresh tokens valid; with one that rotates them, the slower caller may get ErrRefreshFailed and should call again.

func (*Plugin) TypedMigrations

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

TypedMigrations creates auth_account for engine. (provider, subject) is UNIQUE: one provider account belongs to one user.

func (p *Plugin) Unlink(ctx context.Context, provider string) error

Unlink removes a linked provider from the signed-in user. It needs a fresh sign-in, and refuses to remove the last way into the account.

func (*Plugin) UnlinkAccount

func (p *Plugin) UnlinkAccount(ctx context.Context, provider, accountID string) error

UnlinkAccount is Unlink for one of several accounts of a provider.

func (*Plugin) UserTables

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

UserTables declares that auth_account rows belong to the user in user_id, so DeleteUser removes them (no foreign key does, ADR-0002).

func (*Plugin) Validate

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

Validate checks the providers, the callback URL and the providers named by AllowProviderInitiated. Under SkipBrokenProviders a provider that fails its own check is recorded as skipped (Init logs it) instead of failing auth.New.

type PostedProfile

type PostedProfile interface {
	Provider
	// ApplyPosted reads raw, the callback's user field, into p; "" is a
	// callback that carried none.
	ApplyPosted(raw string, p *Profile)
}

PostedProfile is a Provider whose form-post callback carries a user field beside the code (Apple's first-consent name): the plugin hands the field to it after the exchange. *OAuth2 implements it through PostedUser.

type Profile

type Profile struct {
	// Subject is the provider's stable id for the person. Never the email,
	// which the person can change at the provider.
	Subject string
	Email   string
	// EmailVerified is the provider's claim that the person controls Email.
	// Only a verified claim can link to an existing account.
	EmailVerified bool
	Name          string
	Image         string
	// Tokens are the provider's tokens, kept only with StoreTokens.
	Tokens TokenSet
	// Claims are the raw claims the profile was read from: the userinfo or
	// API answer of a built-in FetchProfile, or the ID token's payload. A
	// ValidateProfile or OnResolved hook reads provider-specific fields here
	// (a Google hd, a GitHub login). Nil for a custom FetchProfile that does
	// not fill it.
	Claims map[string]json.RawMessage
}

Profile is what a provider says about the person who signed in.

type ProfileAction

type ProfileAction string

ProfileAction is what a callback is about to do with a provider profile.

const (
	// ActionSignIn: the provider account is linked; its user signs in.
	ActionSignIn ProfileAction = "sign_in"
	// ActionSignUp: nobody holds the provider account; a user is created.
	ActionSignUp ProfileAction = "sign_up"
	// ActionLink: the provider account is linked to an existing user, by a
	// signed-in link flow or by a verified-email match.
	ActionLink ProfileAction = "link"
)

The three outcomes a profile can have.

type ProfileEvent

type ProfileEvent struct {
	Provider string
	Profile  Profile
	Action   ProfileAction
	// UserID is the user the flow resolves to; "" for ActionSignUp, where
	// the user does not exist yet.
	UserID string
	// Data is what the start request carried (StartRequest.Data); "" for an
	// id-token sign-in.
	Data string
	// ProviderInitiated is true for a sign-in the provider started
	// (AllowProviderInitiated), which no request of this app began: a hook
	// may hold such a sign-in to a stricter rule.
	ProviderInitiated bool
	// ServerContext is what the WithServerContext functions returned at
	// the start of this flow, or at the id-token call: trusted, because no
	// client can set it. Nil when none ran or none returned anything.
	ServerContext map[string]string
	// Reauthentication is true when a signed-in user is confirming who they
	// are with a linked account (EndpointReauthenticate, or
	// EndpointReauthenticateIDToken). Action is then
	// ActionSignIn, so a hook that holds sign-ins to a rule holds this to
	// it too; refusing it leaves the session as it was.
	Reauthentication bool
}

ProfileEvent is what a ValidateProfile hook sees: the provider, its profile (Claims included), what is about to happen, and whom to.

type ProfileFetcher

type ProfileFetcher interface {
	Provider
	// FetchProfileWith reads the profile with tokens. A provider whose
	// profile exists only in the ID token of the exchange (ProfileFromToken
	// with no other source) answers ErrProfileUnsupported.
	FetchProfileWith(ctx context.Context, hc *http.Client, tokens TokenSet) (Profile, error)
}

ProfileFetcher is a Provider that can read a profile from a token set it did not just issue: the stored tokens of a linked account. *OAuth2 and *OIDCProvider implement it; Plugin.Profile needs it.

type ProfileHook

type ProfileHook func(ctx context.Context, c *auth.Call, ev ProfileEvent) error

ProfileHook runs before a callback or id-token sign-in writes anything, inside its transaction. Returning an error refuses the flow: an *auth.Error is answered as it is, any other error is logged and answered as ErrProfileRefused. For allow-listing a Workspace domain, refusing a provider email, or requiring a claim.

type Provider

type Provider interface {
	// Name is the route segment and the stored provider name, such as
	// "google". It must match auth.KindNamePattern and never change.
	Name() string
	// AuthCodeURL is where the browser goes to sign in. PKCE is not
	// optional: every URL carries the S256 challenge.
	AuthCodeURL(state, codeChallenge, redirectURI string) string
	// Exchange turns the callback's code into the person's profile.
	Exchange(ctx context.Context, hc *http.Client, code, codeVerifier, redirectURI string) (Profile, error)
}

Provider is one identity provider. The library ships Google and GitHub as *OAuth2 values; an app adds any other by implementing this interface.

type ProviderInfo

type ProviderInfo struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

ProviderInfo is one provider in the providers list: its id, the route segment of its start link, and its display name. Nothing secret.

type ProvidersResult

type ProvidersResult struct {
	Providers []ProviderInfo `json:"providers"`
}

ProvidersResult is the output of the providers route.

type ReauthenticateIDTokenRequest

type ReauthenticateIDTokenRequest struct {
	// Provider names the provider that issued IDToken; it must verify ID
	// tokens (an IDTokenVerifier) and the user must have an account linked
	// there.
	Provider string `json:"provider"`
	// IDToken is the provider's ID token, a compact JWS.
	IDToken string `json:"idToken"`
	// Nonce is the value the app sent to the provider, when it sent one,
	// under the same rule as IDTokenRequest.Nonce.
	Nonce string `json:"nonce,omitempty"`
	// AccessToken is the access token the SDK received with the ID token,
	// taken only when the ID token's at_hash binds it, and then stored under
	// StoreTokens, as IDTokenRequest.AccessToken is.
	AccessToken string `json:"accessToken,omitempty"`
}

ReauthenticateIDTokenRequest is the input of EndpointReauthenticateIDToken: IDTokenRequest's fields, minus SignUp (nothing is created here), plus the provider, since the operation is one route for every provider.

type ReauthenticateRequest

type ReauthenticateRequest struct {
	// Provider is the provider to confirm with; the user must have an
	// account linked there.
	Provider string `json:"provider"`
	// ReturnTo is where the callback lands, with ParamReauth added: the page
	// that asked, so it can rerun the refused action. Same rules as
	// StartRequest.ReturnTo; empty is the site root.
	ReturnTo string `json:"returnTo,omitempty"`
	// ErrorTo is where a refused callback lands with `?error=<code>`; empty
	// takes ReturnTo, so the user comes back to the page that asked.
	ErrorTo string `json:"errorTo,omitempty"`
	// Prompt is forwarded to a ParamsProvider ("login" asks the provider to
	// sign the person in again rather than answer from its own session).
	Prompt string `json:"prompt,omitempty"`
	// LoginHint pre-fills the account at a ParamsProvider. Empty takes the
	// address of the user's linked account there when there is exactly one.
	LoginHint string `json:"loginHint,omitempty"`
}

ReauthenticateRequest is the input of EndpointReauthenticate.

type Refresher

type Refresher interface {
	Refresh(ctx context.Context, hc *http.Client, refreshToken auth.Token) (TokenSet, error)
}

Refresher is a provider that can trade a refresh token for new tokens.

type RequestAuthURLer

type RequestAuthURLer interface {
	Provider
	AuthCodeURLForRequest(ctx context.Context, state, codeChallenge, redirectURI string, rp RequestParams) string
}

RequestAuthURLer is a Provider whose authorization URL depends on the request that starts the flow: start calls AuthCodeURLForRequest with the request's context (auth.RequestHeader and auth.RequestHost read it) in place of AuthCodeURL or AuthCodeURLWithParams. rp is empty unless the request carried parameters, which start accepts only for a ParamsProvider. plugin/oauthproxy uses it for its per-request skip header.

type RequestParams

type RequestParams struct {
	// Prompt is the OpenID Connect prompt value: none, login, consent,
	// select_account.
	Prompt string
	// LoginHint pre-fills the account at the provider.
	LoginHint string
	// Scopes are extra scopes, already checked against OptionalScopes.
	Scopes []string
	// Extra are further authorization parameters, already checked against
	// the provider's OptionalParams; the protocol's own keys are never in
	// it.
	Extra url.Values
}

RequestParams are what one start request adds to the authorization URL, on top of the provider's static configuration.

type ResolvedEvent

type ResolvedEvent struct {
	ProfileEvent
	Created bool
	Linked  bool
}

ResolvedEvent is what an OnResolved hook sees: the ProfileEvent with the user the flow resolved to, and whether it created or linked.

type ResolvedHook

type ResolvedHook func(ctx context.Context, c *auth.Call, ev ResolvedEvent) error

ResolvedHook runs after a callback or id-token sign-in resolved its user, inside the same transaction and before any session is issued, with UserID set. An error fails the flow and rolls the user back. For writing provider claims into the app's own user columns (auth.SetUserColumn) or its own tables, which no DataHooks hook can do because none sees the profile.

type Revoker

type Revoker interface {
	Provider
	// Revoke invalidates the tokens at the provider: the refresh token when
	// there is one (which revokes the access tokens it issued), else the
	// access token. A provider with no revocation endpoint returns nil and
	// revokes nothing.
	Revoke(ctx context.Context, hc *http.Client, tokens TokenSet) error
}

Revoker is a Provider that can revoke tokens it issued (RFC 7009). *OAuth2 with a RevokeURL implements it; unlink calls it for the stored tokens.

type ServerContextFunc

type ServerContextFunc func(ctx context.Context, c *auth.Call, provider string) (map[string]string, error)

ServerContextFunc returns server-set values to carry across one OAuth flow, better-auth's addOAuthServerContext: a tenant id the request resolved, a plan the server chose. It runs at every start and id-token sign-in, with the provider's name; what it returns is stored with the state, where no client can set or change it, and handed back in CallbackResult.ServerContext and ProfileEvent.ServerContext. An error refuses the start: an *auth.Error is answered as it is, any other error as a server error.

type StartRequest

type StartRequest struct {
	// ReturnTo is a path on this site to land on after sign-in.
	ReturnTo string `json:"returnTo,omitempty"`
	// Link, when LinkFlag, links the provider to the signed-in user instead
	// of signing in. A string because it arrives in a URL query.
	Link string `json:"link,omitempty"`
	// Prompt is forwarded to a ParamsProvider as the prompt parameter
	// (select_account, consent, login, none).
	Prompt string `json:"prompt,omitempty"`
	// LoginHint is forwarded as login_hint, to pre-fill the account.
	LoginHint string `json:"loginHint,omitempty"`
	// Scopes are extra scopes, space-separated, each listed in the
	// provider's OptionalScopes; for incremental authorization.
	Scopes string `json:"scopes,omitempty"`
	// Params are extra authorization parameters as a query string
	// ("display=popup&ui_locales=de"), each named in the provider's
	// OptionalParams (ErrBadParam otherwise): better-auth's per-request
	// additionalParams. A string because it arrives in a URL query; at most
	// maxFlowDataLength characters.
	Params string `json:"params,omitempty"`
	// Data is carried through the flow untouched and handed back in
	// CallbackResult.Data: a referral code, an analytics source. At most
	// maxFlowDataLength characters. It comes from the client, so the app
	// treats it as untrusted input.
	Data string `json:"data,omitempty"`
	// ErrorTo is a path on this site a failed callback redirects to, with
	// the error code in the query; empty takes the plugin's ErrorURL. Without
	// either, a failed callback answers JSON.
	ErrorTo string `json:"errorTo,omitempty"`
	// NewUserTo is where a callback that created the user lands (an
	// onboarding page) instead of ReturnTo; same rules as ReturnTo.
	NewUserTo string `json:"newUserTo,omitempty"`
	// SignUp, when SignUpFlag, says the person meant to sign up, which
	// ExplicitSignUp requires before a user is created. A string because it
	// arrives in a URL query.
	SignUp string `json:"signUp,omitempty"`
}

StartRequest is the input of a start operation. It arrives in a URL query, since the browser reaches start by a link.

type StartResult

type StartResult struct {
	URL string `json:"url"`
}

StartResult carries the provider URL. HTTP transports redirect to it.

type TokenAuth

type TokenAuth string

TokenAuth is how the client proves itself at the token endpoint (RFC 6749 section 2.3.1). Providers differ: most accept the secret in the form body, some (Reddit, PayPal, Zoom, X, Cloudflare) only as HTTP Basic, and a native or browser client has no secret at all and relies on PKCE.

const (
	// ClientSecretPost sends client_id and client_secret in the form body.
	// The default, and what the built-in providers use.
	ClientSecretPost TokenAuth = "client_secret_post"
	// ClientSecretBasic sends them in an Authorization: Basic header, each
	// form-encoded first as the RFC requires.
	ClientSecretBasic TokenAuth = "client_secret_basic"
	// PublicClient sends client_id only; the code is protected by PKCE alone.
	// Validate refuses a ClientSecret together with it, since a secret that
	// is never sent is a configuration mistake.
	PublicClient TokenAuth = "none"
	// PrivateKeyJWT sends a signed assertion (RFC 7523 section 2.2) instead
	// of a secret: client_assertion_type and client_assertion from
	// OAuth2.ClientAssertion. For Entra ID workload identity and any issuer
	// that refuses shared secrets. Validate refuses a ClientSecret with it.
	PrivateKeyJWT TokenAuth = "private_key_jwt"
)

type TokenSet

type TokenSet struct {
	AccessToken  auth.Token
	RefreshToken auth.Token
	// ExpiresAt is when AccessToken expires; zero when the provider did not
	// say.
	ExpiresAt time.Time
	// RefreshParams are parameters every refresh grant of this account
	// must repeat, kept (sealed) with the tokens under StoreTokens: VK ID's
	// device_id, taken from the callback (OAuth2.RefreshCallbackParams).
	// Nil for every other provider.
	RefreshParams url.Values
	// IDToken is the OpenID Connect ID token the token endpoint returned,
	// empty when it returned none. Stored (sealed) only with StoreTokens, for
	// the id_token_hint of a provider logout.
	IDToken auth.Token
	// Scope is the scope the provider says it granted (RFC 6749 section 5.1),
	// "" when it did not say; stored with StoreTokens so an app knows which
	// scopes an incremental authorization added.
	Scope string
	// RefreshExpiresAt is when RefreshToken expires, for the providers that
	// say (refresh_token_expires_in, refresh_expires_in); zero when unknown.
	RefreshExpiresAt time.Time
	// Raw is the token endpoint's whole JSON answer, for a
	// FetchProfileFromTokens that needs a provider-specific field (WeChat's
	// openid, Slack's authed_user). Never stored.
	Raw json.RawMessage
}

TokenSet is what a provider's token endpoint issued. The tokens are auth.Token, so they print as "[redacted]" everywhere.

type UnlinkRequest

type UnlinkRequest struct {
	Provider string `json:"provider"`
	// AccountID picks one of several accounts of Provider (a person may link
	// two GitHub accounts); it must be one of the caller's own, from the
	// accounts operation. Empty unlinks the oldest.
	AccountID string `json:"accountId,omitempty"`
}

UnlinkRequest names a provider to unlink from the signed-in user.

type ZohoRegion

type ZohoRegion string

ZohoRegion is a Zoho data centre's accounts server, which is also its OpenID Connect issuer: a Zoho app is registered in one data centre and signs in that centre's users. Each value was read from the centre's discovery document on 2026-10-08 (S256, client_secret_basic and _post, /oauth/v2/userinfo on every one).

const (
	//ds:def id=zohoregions-p3z4kw8n owner=@khanakia stability=stable
	ZohoUS        ZohoRegion = "https://accounts.zoho.com"
	ZohoEU        ZohoRegion = "https://accounts.zoho.eu"
	ZohoIndia     ZohoRegion = "https://accounts.zoho.in"
	ZohoAustralia ZohoRegion = "https://accounts.zoho.com.au"
	ZohoJapan     ZohoRegion = "https://accounts.zoho.jp"
	ZohoUK        ZohoRegion = "https://accounts.zoho.uk"
	ZohoCanada    ZohoRegion = "https://accounts.zohocloud.ca"
	ZohoSaudi     ZohoRegion = "https://accounts.zoho.sa"
	ZohoChina     ZohoRegion = "https://accounts.zoho.com.cn"
)

The Zoho data centres, better-auth-zoho's location option (with China, which it leaves out).

Directories

Path Synopsis
Package oauthtest checks an oauth.Provider against the contract the oauth plugin relies on, by driving it against a fake authorization server it starts.
Package oauthtest checks an oauth.Provider against the contract the oauth plugin relies on, by driving it against a fake authorization server it starts.

Jump to

Keyboard shortcuts

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