AnnouncementsIntroducing Arkoren

    Menu

    OAuth Login

    Complete external browser sign-in and map the provider identity to a local account safely.

    What's on this page
    1. Configure an external provider and complete browser login through a shared session identity.
    2. Link local accounts by issuer and subject while preserving state, nonce, and redirect checks.
    3. Apply each provider's callback and identity requirements and handle bounded outcome events.
    On this page

    Let a provider check a person’s credentials, then create a local browser login for the matching account. OpenID Connect (OIDC) adds verified ID tokens to the OAuth 2 authorization flow; plain OAuth2 providers instead supply identity through an authenticated user API. oauthauth supports both paths.

    You still own local accounts and external-account linkage. The adapter completes the provider flow, calls your mapper, and stores its returned account identifier with Session Login. It is not an authorization server and does not acquire API tokens for arbitrary clients or refresh tokens for your service. To accept an existing JWT at an API, use OIDC Tokens.

    Choose Providers

    Use a preset when it matches your provider; it includes identity checks that a generic adapter does not infer. The matrix describes shipped adapter behavior, not every feature offered by the provider. PKCE is the authorization-code challenge sent by default, not a claim about provider-side enforcement.

    Compatibility Matrix

    Provider Browser login OIDC discovery PKCE default Verified-email filter JWT verification
    Google Yes Yes Yes Yes
    Microsoft Yes Yes
    GitHub Yes No Yes No
    Apple Yes No No
    Generic OIDC Yes Yes Yes No
    Generic OAuth2 Yes No Yes No

    All clients require an encrypter. JWT verification requires OIDC, explicit API audiences, and your provider’s token-purpose policy; it is not interchangeable with browser ID-token verification. A filtered email is still not an account key.

    Configure Clients

    Register an application and exact callback URL with your provider. Obtain its client ID and required credentials, then install the adapter in your module:

    go get arkoren.dev/driver/oauth

    Import it as oauth. The example uses an existing cipher *crypto.Encrypter from Encryption, plus clientID and clientSecret loaded from your configuration/secret provider:

    import oauth "arkoren.dev/driver/oauth/oauthauth"
    
    client, err := oauth.New(oauth.Google(), cipher, clientID,
    	func(login *oauth.ClientBuilder) {
    		login.ClientSecret(clientSecret)
    		login.RedirectURL("https://app.example.com/oauth/callback")
    	},
    )

    Handle the constructor error before using client. Construction validates inputs but performs no network I/O. Use a stable encryption key shared by all instances that can receive callbacks; key provisioning belongs in Encryption Keys, not in each request handler.

    For OIDC, the positional clientID is the required browser ID-token audience, its intended recipient. ClientBuilder.Audiences configures only API verification, not this callback check. PKCE starts enabled; disable it only for a provider that cannot support it. Apple forces it off independently of this setting.

    Use a distinct ClientBuilder.Name for each provider client. It prefixes state cookies and defaults to arkoren.oauth; invalid cookie names fail construction. Configuration callbacks run once in order, and nil callback entries panic. The published client copies its settings and scope/audience slices; retained drafts cannot change it, but services and captured application state remain shared.

    Start Discovery

    OIDC clients must discover their endpoints before serving requests. In your existing application composer, register startup with application from arkoren.dev/core/application:

    compose := func(composer *application.Composer) {
    	composer.Startup(client.Start)
    }

    Before successful startup, OIDC handlers return ErrNotStarted (503). A subsequent Start refreshes discovered data. Plain OAuth2 clients prepare their configured endpoints at construction; their Start is a no-op. Signing keys can still be retrieved during OIDC token verification.

    Keep the returned *Client; do not copy its atomic state. It has no Stop method and does not stop the shared encrypter or event bus. Your application owns those services and any supplied native HTTP client; keep encryption available until handlers have finished.

    Connect Browser Routes

    Use an existing session-enabled, CSRF-protected browser group web, as in Enable Login. Import auth from arkoren.dev/core/http/auth. Your loadUser loads a current local User by integer ID; mapIdentity below returns that same ID type:

    identity := auth.NewSessionIdentity(loadUser)
    web.Use(auth.Middleware(identity.Authenticator()))
    web.Get("/login", client.Login())
    web.Get("/oauth/callback", client.Callback(identity, mapIdentity))
    web.Post("/logout", client.Logout(identity))

    Login redirects to the provider. A successful callback verifies the provider identity, maps it, rotates the selected local session ID, and redirects to the saved local destination. Subsequent requests use the normal session loader. Logout removes the local identity and redirects to /; it does not sign the person out at the provider. Keep logout CSRF-protected.

    Check identity.Err() during setup. An invalid definition or nil mapper is a configuration error, not a rejected credential. Callback and logout select exactly the identity’s session slot. An absent session returns 401 before state consumption or provider exchange, without default-slot fallback. If you use custom session selection, share its handle with the session middleware. Middleware still owns response-time persistence; successful callback mutation does not prove storage or old-ID retirement succeeded.

    Identify an external account by the pair Issuer and Subject, not by email or display name. Require an existing link for ordinary sign-in; create accounts only under your explicit registration policy. To link another provider to an existing account, require proof of control of that local account as a separate, authorized action.

    With standard-library context, this application mapper looks up an existing link using accounts.ByIdentity(ctx, issuer, subject), which returns the application’s Account or an error:

    mapIdentity := func(ctx context.Context, external oauth.Identity) (int, error) {
    	if external.Issuer == "" || external.Subject == "" {
    		return 0, auth.ErrUnauthenticated
    	}
    	account, err := accounts.ByIdentity(ctx, external.Issuer, external.Subject)
    	if err != nil {
    		return 0, err
    	}
    	if account.Disabled {
    		return 0, auth.ErrUnauthenticated
    	}
    
    	return account.ID, nil
    }

    The repository owns uniqueness of the issuer/subject link and rejection of unknown accounts. A mapper returning zero and nil error still stores zero as the login identifier; Arkoren cannot infer whether it denotes a real account. Mapper errors matching auth.ErrNoCredentials become ErrInvalidCallback (400); other application errors pass through unchanged.

    Never match or merge accounts by email alone. Some providers pass through unverified email claims, and even verified addresses can change. An automatic email match can give an attacker another person’s existing local account.

    Identity.Email is optional profile data. Google filters using boolean email_verified, Microsoft using boolean xms_edov, and GitHub using its primary verified email record. Generic OIDC and Apple pass email through without that filter; generic OAuth2 uses whatever your user-info function returns. If an action needs a verified address, enforce that policy separately. Treat Name as untrusted display text and escape it when rendering.

    For browser callbacks, Identity.Scopes comes from the token response. If the response omits scopes, the client supplies an independent copy of the requested scopes. These are provider grants, not your local account’s resource permissions.

    Protect Login State

    To return a person to an invoice after login, start the flow with a local path:

    /login?return_to=/invoices/42

    The client validates this target before redirecting to the provider, seals it with the login state, and uses a safe redirect after the callback. Unsafe targets return ErrInvalidLoginRequest (400). Do not bypass this path by redirecting to another unvalidated request parameter.

    Each login gets random state, an OIDC nonce when applicable, and a PKCE verifier when enabled. State binds the callback to the browser login; nonce binds the ID token to that attempt. These values live in an encrypted, HttpOnly cookie for ten minutes, not in the application session. The client keeps room for at most five live login cookies, allowing parallel tabs.

    Callbacks reject missing, tampered, expired, or mismatched state cookies. For OIDC they also check the ID token’s signature, expiry, issuer, positional client-ID audience, nonempty final subject, and saved nonce before mapping. Plain OAuth2 relies on the authenticated user API instead of an ID token. Cookie deletion is browser cleanup, not a server-side replay registry: a copied cookie can survive until expiry. The provider must enforce one-time authorization codes.

    Cookies use Secure and the __Host- prefix by default. Use HTTPS for login and registered callbacks. ClientBuilder.AllowInsecure(true) removes both protections for plain-HTTP development only; never enable it in production. Apple’s cross-site cookie requires HTTPS even during development.

    Google

    Choose Google for Google account sign-in. Register a web client and the callback URL in Google, then use oauth.Google() in the shared constructor with its client ID and secret. Keep the startup hook for discovery and the ordinary GET callback.

    If only one Workspace domain may sign in, replace the provider argument with:

    provider := oauth.Google(func(google *oauth.GoogleBuilder) {
    	google.HostedDomain("example.com")
    })

    Pass provider to oauth.New. This checks the verified hd claim as well as sending the account-chooser hint; the default accepts any domain. Default scopes are openid, email, and profile. The preset normalizes the issuer to https://accounts.google.com and leaves email empty without boolean email_verified: true. Keep issuer/subject linkage even with this filter.

    See the Google API.

    Microsoft

    Choose Microsoft for Entra ID or Microsoft-account sign-in. Register the callback URL and obtain the application’s client ID and secret. Replace the shared provider argument with a deliberately chosen tenant policy:

    provider := oauth.Microsoft("organizations")

    Pass it to oauth.New, keep Start for discovery, and use the shared browser routes and mapper. organizations rejects the reserved personal-account tenant; consumers accepts only that tenant (9188040d-6c67-4c5b-b112-36a304b66dad). common accepts both classes. For single-tenant use, pass the tenant ID, matched case-insensitively, not a domain name such as contoso.onmicrosoft.com.

    Both callback and bearer checks enforce the verified tid policy and require oid. The issuer must be https://login.microsoftonline.com/<tid>/v2.0, and the final subject is oid. The check uses tenant context, not the home idp: a personal account invited into an organization can pass organizations in that organization’s context. Add a narrower tenant allowlist in your mapper if the selected preset is broader than your policy.

    Default scopes are openid, profile, and email. Email remains empty unless the optional xms_edov claim is boolean true; configure that claim in the app registration if needed. It is still not a local-account key.

    See the Microsoft API.

    GitHub

    Choose GitHub for plain OAuth2 sign-in. Register a GitHub OAuth application and the callback URL, then pass its ID/secret to the shared constructor with:

    provider := oauth.GitHub()

    Use the shared GET callback and local mapper. Start is a no-op because this adapter has configured endpoints, not discovery. Default scopes are read:user and user:email; PKCE remains enabled.

    After code exchange, the preset calls the user and email APIs. Its issuer is https://github.com, and subject is the positive numeric user ID, not the mutable login name. Missing, zero, or negative IDs and user/email API failures return ErrProviderUnavailable (502) before mapping. Email is the primary verified address or empty. This preset cannot verify OIDC bearer tokens.

    See the GitHub API.

    Apple

    Choose Apple for Sign in with Apple. Register an HTTPS callback and a Services ID, obtain your team ID/key ID, and load the Apple-issued .p8 signing key with key.LoadSigning from arkoren.dev/foundation/key. Do not generate an unrelated local replacement for a key registered with Apple.

    appleSigning, err := key.LoadSigning("AuthKey_ABC123DEFG.p8")

    Handle that error, then construct the provider:

    provider := oauth.Apple(teamID, keyID, appleSigning)

    Use the Services ID as oauth.New’s client ID, omit ClientSecret, and configure RedirectURL("https://app.example.com/oauth/apple/callback"). The constructor rejects an invalid key. The provider signs a short-lived client secret per code exchange, requires discovery, requests openid, name, and email, and forces PKCE off.

    Apple sends a cross-site POST callback. Mount only that callback outside CSRF protection, while retaining session middleware; keep CSRF on logout and all other state-changing browser routes. With r your session-enabled router, the callback is:

    r.Post("/oauth/apple/callback", client.Callback(identity, mapIdentity))

    Use an Apple-configured client, not the Google baseline. Its encrypted state cookie protects this callback and uses SameSite=None; HTTPS is required. Apple sends the name only on first authorization in an unsigned form field, so store it as optional display text, not an authorization claim. Email can be a private relay address and is not filtered for verification by this preset.

    See the Apple API.

    Generic OIDC

    Choose this adapter for another OIDC service. Register a browser client and callback with the service, then configure its trusted issuer URL:

    provider := oauth.OIDC("https://id.example.com/realms/invoices")

    Pass the provider and registered credentials to the shared constructor; keep discovery startup and the shared GET callback. Default scopes contain only openid. If supported and needed, use ClientBuilder.Scopes("openid", "profile", "email"). PKCE is enabled by default. No provider-specific tenant or email policy is inferred: implement those checks before linking the local account.

    See the OIDC API.

    Generic OAuth2

    Choose this adapter only when the provider has OAuth2 but no OIDC. Register your application/callback and obtain its trusted authorization and token URLs. Supply your own authenticated user-info function:

    provider := oauth.OAuth2(oauth.Endpoint{
    	AuthURL:  "https://id.example.com/authorize",
    	TokenURL: "https://id.example.com/token",
    }, loadIdentity)

    loadIdentity is oauth.UserInfoFunc: it receives context.Context and an authenticated *http.Client and returns oauth.Identity. It must call the provider’s user API with that context, check status, close the response body, and validate a nonempty stable issuer/subject and any email policy. The generic adapter does not impose a provider-specific subject schema.

    Pass the provider and registered credentials to the shared constructor and use its GET callback. There are no default scopes; configure Scopes for the provider’s user API. Endpoints are prepared without network I/O, and Start is a no-op. PKCE starts enabled; disable it only if unsupported. User-info failures become 502, and Verify is unsupported because there is no OIDC verifier.

    See the OAuth2 and UserInfoFunc API.

    Handle Failures

    Invalid state, failed ID-token checks, provider callback errors, and a token endpoint’s native invalid_grant rejection return ErrInvalidCallback (400). Start a fresh login rather than replaying an invalid, expired, or consumed code. Other exchange failures return ErrProviderUnavailable (502). Returned errors retain native causes for explicit inspection; provider response text and claims can be sensitive, so do not dump those chains into logs.

    Discovery errors retain native causes and context cancellation/deadline identity when applicable. The bearer-only verification path has additional context checks; do not assume every browser callback or local policy denial behaves the same way.

    Events

    If you need outcome events, configure ClientBuilder.Events with your existing event bus. Nil publishes nothing. Successful callbacks publish issuer and subject, which can identify a person. Logout has no payload and reports local mutation, not provider logout or durable session persistence.

    LoginFailedEvent.Err contains ErrInvalidCallback, ErrProviderUnavailable, or an opaque local-login classification, without the original cause. ProviderError is an untrusted, allowlisted RFC 6749 callback hint; unknown values and arbitrary descriptions are omitted. Configuration, not-started, and missing-session exits before cookie consumption do not publish callback outcomes. TokenRejectedEvent.Err is likewise a fixed opaque classification; unsupported providers and missing audiences return before publication. Do not classify opaque errors by matching their text. Returned errors retain their separate diagnostic chains, and event context values are not sanitized by these payload limits.

    Ordinary fmt formatting redacts owned client/draft configuration; Config formatting redacts a nonempty client secret. This is not a general serialization or logging guarantee: arbitrary claims, native objects, callback captures, and explicitly unwrapped errors remain outside that boundary.

    For all functions and options, see the oauthauth API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive