AnnouncementsIntroducing Arkoren

    Menu

    OIDC Tokens

    Verify provider-issued JWTs for an API and resolve the exact request identity your handlers need.

    What's on this page
    1. Configure API audiences and discovery before accepting provider-issued JWTs.
    2. Understand signature, issuer, key retrieval, clock, and token-purpose verification boundaries.
    3. Require the exact external or local identity type and preserve safe failure handling.
    On this page

    An API can accept an existing JWT issued by a trusted OpenID Connect provider without browser routes or a session. oauthauth.Client.Verify validates the token and returns an external identity; auth.Bearer connects that result to request authentication.

    The provider or a separate client flow acquires and refreshes tokens. This adapter does not implement a token endpoint, authorization server, opaque-token introspection, or provider revocation checks. Plain OAuth2 adapters, including GitHub, cannot use Verify. For browser sign-in, use OAuth Login.

    Prepare Verification

    Register an audience intended for this API with your provider. An audience names a token’s intended recipient. Prefer separate API and browser-client audiences; do not accept a browser client ID merely because it is convenient. Use a trusted issuer from application configuration, never from unverified request claims.

    Install arkoren.dev/driver/oauth as shown in OAuth client setup. With an existing cipher *crypto.Encrypter from Encryption and your registered clientID, configure the verifier:

    import oauth "arkoren.dev/driver/oauth/oauthauth"
    
    client, err := oauth.New(
    	oauth.OIDC("https://id.example.com/realms/invoices"),
    	cipher,
    	clientID,
    	func(verification *oauth.ClientBuilder) {
    		verification.Audiences("https://api.example.com/invoices")
    	},
    )

    Handle the constructor error before use. The API-only path still requires a non-nil encrypter and nonempty client ID because they are constructor inputs; it needs no redirect URL or client secret. Audiences is required by Verify. Without it, verification returns ErrNoAudiences rather than defaulting to the browser client ID.

    Construction performs no network I/O. Register client.Start in your existing application composer, importing arkoren.dev/core/application:

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

    Startup discovers endpoints and prepares the verifier. Until it succeeds, verification returns ErrNotStarted. Key retrieval can occur later during verification, so successful discovery does not prove every request can reach the signing-key endpoint. Keep the returned client pointer; it owns no stop hook and does not stop its shared encrypter.

    Set Verification Boundaries

    Verify checks the JWT signature, expiry, trusted issuer, and whether at least one token audience matches a configured API audience. It requires a nonempty final subject after any provider-specific mapping. Google and Microsoft apply their preset identity checks; generic OIDC does not infer tenant restrictions or email-verification policy.

    The discovered provider supplies signing keys through its JWKS endpoint, a published set of verification keys. The underlying OIDC verifier caches keys and can fetch them during signature verification. Accepted algorithms come from supported discovery metadata, with an RS256 fallback when none are set; the token header alone does not define the trust policy. Arkoren’s builder does not expose a custom algorithm allowlist or native-verifier configuration. Use a different application verifier when your policy needs controls this adapter does not expose.

    Time checks use the service’s wall clock. The current native verifier rejects expired tokens and allows five minutes of tolerance for a future nbf (not-before) claim; that is not a general five-minute expiry grace period. There is no clock or skew setting on ClientBuilder. Keep service clocks synchronized, and do not assume signature/expiry checks enforce a maximum token age or every provider’s additional time policy.

    Audience and signature checks do not prove a JWT is an API access token. Verify rejects any payload nonce claim and a payload typ equal to ID case-insensitively, but an unmarked JWT with an accepted audience can pass. Enforce your provider’s access-token purpose policy before granting API access.

    These marker checks are heuristics, not a universal access-token profile; the adapter does not require every provider to use at+jwt. If additional signed claims must be checked, wrap client.Verify with your application checks or use a provider-specific verifier. A successful verification still needs authorization.

    Local JWT verification is not an online check that the provider still accepts the token or account. A token can remain acceptable until expiry even after provider-side revocation. A current local-account lookup can enforce your own disabled-account policy, but cannot substitute for provider introspection.

    Require Identity

    When handlers work directly with the provider identity, use its exact type throughout middleware and lookup. Import auth and request from their arkoren.dev/core/http/ packages. With your existing API router group api:

    api.Use(auth.Middleware(auth.Bearer(client.Verify)))
    api.Use(auth.Require[oauth.Identity]())

    This assumes the audience and purpose policy above is sufficient for your provider. Otherwise pass your stricter application verifier instead of client.Verify. Authentication middleware alone permits missing credentials; Require rejects that anonymous path. Invalid bearer credentials produce 401 with WWW-Authenticate: Bearer error="invalid_token".

    In a handler, read the same identity type:

    external, err := request.User[oauth.Identity](r)
    if err != nil {
    	return err
    }

    external.Issuer and external.Subject identify the external account. *oauth.Identity and your local User are different context keys, not alternate views of this value. Claims, Email, and Name remain provider-controlled data; do not log arbitrary claims or use email to link local accounts.

    Load Local Accounts

    If handlers need current local permissions, map the verified issuer/subject pair to an existing account. Keep token scopes request-local instead of mutating a shared cached account. For example:

    type User struct {
    	ID     int
    	Scopes []string
    }

    With standard-library context and slices, accounts.ByIdentity returning your application’s Account, and a client satisfying your provider’s purpose policy, use:

    verifyUser := func(ctx context.Context, token string) (User, error) {
    	external, err := client.Verify(ctx, token)
    	if err != nil {
    		return User{}, err
    	}
    	account, err := accounts.ByIdentity(ctx, external.Issuer, external.Subject)
    	if err != nil {
    		return User{}, err
    	}
    	if account.Disabled {
    		return User{}, auth.ErrUnauthenticated
    	}
    
    	return User{ID: account.ID, Scopes: slices.Clone(external.Scopes)}, nil
    }

    The repository must reject unknown links with an error other than auth.ErrNoCredentials. That sentinel would permit credential fallback, not reject the presented token. A zero user with nil error would count as a success. Do not create or merge accounts automatically from the email claim; see local-account linkage.

    Wire the mapped type, not the external type:

    api.Use(auth.Middleware(auth.Bearer(verifyUser)))
    api.Use(auth.Require[User]())

    Scope checks and resource permissions belong in Authorization Policies. oauth.HasScopes works with oauth.Identity, not this local User; use a User-typed rule for the mapped identity. Token scopes limit the presented credential’s authority rather than replacing the account’s resource permissions.

    Handle Verification

    Direct Verify calls preserve their error identity and native causes. After unsupported-provider and missing-audience checks, an ended context is rejected with context.Canceled or context.DeadlineExceeded, including when cached or local keys could otherwise succeed. A final context check also prevents identity publication after cancellation during verification. These checks do not promise that every underlying key-fetch operation stops with the request.

    Through auth.Bearer, verifier errors become an invalid-token HTTP response, without exposing the error message to the client. The attached cause can still reach diagnostics. Native errors can contain provider-controlled claims, so inspect deliberately instead of logging entire chains. Optional token-rejection events contain a bounded opaque error instead; they do not retain the native cause.

    One started OIDC client can handle browser callbacks and APIs, but each uses its own audience rule: callbacks require the positional client ID, while Verify uses configured API audiences. For MCP, apply the same bearer and required-user middleware outside the mount and follow MCP endpoint security for challenges, admission, and tool/resource authorization. The CLI sends existing credentials; it does not acquire or refresh them.

    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