Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OpenID Connect

auth.sourceorigin.dev is an OpenID Connect provider. This page lists what it implements, for an app that talks to it directly or through an OIDC library.

Discovery

Discovery is served at both /.well-known/openid-configuration (OIDC) and /.well-known/oauth-authorization-server (RFC 8414, read by OAuth-only and MCP clients); the two are the same document. JWKS is /.well-known/jwks.json. All three are cacheable for an hour and readable from any origin, without credentials. Every authorization response redirected to a client, success or error, carries iss (RFC 9207), so clients that use several issuers can reject mix-ups; discovery advertises authorization_response_iss_parameter_supported.

Pushed authorization requests

A client may push its authorization request (RFC 9126) instead of sending it through the browser: it posts the usual parameters to /oauth/par, authenticated as at the token endpoint, and receives a request_uri valid for 60 seconds. The browser then opens /oauth/authorize?client_id=…&request_uri=…. The request_uri works once, only for that client, and auth ignores every other parameter sent with it. A pushed request is validated exactly like a browser one, but its errors come back as JSON. Discovery advertises pushed_authorization_request_endpoint. Client-hosted request objects (request and request_uri from RFC 9101) are not supported.

The endpoints implement code exchange, RS256 ID/access tokens, userinfo, opt-in refresh tokens, revocation, RP-initiated and back-channel logout. A code is tried once: a redemption by another client, with another redirect_uri or a wrong code_verifier expires it and records code_redemption_failed, so it can’t be tried again. Every step of an authorization checks the client’s registration as it is then, under a share lock when it issues a code or tokens: a client disabled, suspended or changed meanwhile gets nothing it no longer allows. Access tokens are intended for the configured client audience. Only requested claims are released; passkey-only accounts have no email claim. Browser clients should keep tokens in memory and use SSO again after reload. Refresh permission is off unless explicitly enabled and offline_access is allowed/requested. A public client’s refresh token rotates: each refresh answers with a new one, and presenting a spent one revokes the whole grant, so a stolen copy is caught as soon as both are used. A confidential client keeps its token, and a refresh answers with none: without the client’s secret the token is useless, so it needs no rotation (RFC 9700 §4.14), and a refresh writes nothing.

Consent is remembered per user, client and scope; explicitly trusted clients bypass consent. The consent page names the host the app sends the browser back to and the signed-in account, which can switch to another, and labels an app an account registered as unverified, with its owner’s handle. Users see the apps they consented to, or whose refresh grants still work, under Connected apps in settings (/settings/apps), and remove an app’s access there: its refresh grants end, it asks for consent again, and its outstanding codes stop working. Access tokens it already holds expire within five minutes. The settings API serves them too (GET /account/apps, DELETE /account/apps/{client_id}). An administrator removes one the same way from the account’s Connected apps page on the dashboard, and an operator with auth-service account <user> apps remove <client-id>, each with a reason the owner sees in their history.

Authentication claims

ID and access tokens report how the browser session behind the grant authenticated:

ClaimValues
acrphr once a passkey confirmed the session, which a passkey sign-in does: phishing-resistant multi-factor authentication (OpenID Connect EAP ACR Values). urn:sourceorigin:acr:social for a session signed in with GitHub or Google only, the provider’s authentication, whatever it was; and for one signed in with a recovered passkey until its recovery settles
amrEvery method the session used (RFC 8176): pop, mfa and user for a passkey, which proves possession of a key and requires user verification; fed for a provider sign-in. RFC 8176 registers no value for federation, so fed is SourceOrigin’s own. It says how, not how strongly: gate on acr
auth_timeThe latest authentication at the session’s acr: its last passkey confirmation, or else its sign-in

phr satisfies a urn:sourceorigin:acr:social requirement, not the reverse. Both values are listed in discovery’s acr_values_supported. An authorization request may send acr_values in order of preference: the first known value becomes its target, and unknown values are ignored. A session below the target signs in again, on a login page offering only passkeys for phr; with prompt=none the request fails with login_required. If a passkey sign-in for the request still falls short, as a recovered passkey does, the request fails with unmet_authentication_requirements rather than asking again. With no known value, the tokens report what the session reached.

Signing in again (prompt=login, max_age, a target) re-authenticates the browser’s session rather than starting another, so sid stays the same: a passkey confirms it, and a social sign-in renews it only before any passkey confirmation, since it would lower acr. Every re-authentication, including a passkey confirmation for a settings change, replaces the session cookie, so a copied cookie never gains the new authentication. Signing in as another account revokes the previous session. A refresh token keeps reporting the authentication of the code it came from, even after the session re-authenticates.

An API that needs a stronger or more recent sign-in answers with an RFC 9470 step-up challenge, and its client signs the user in again with that challenge’s acr_values and max_age. Step-up authentication explains how to choose a requirement and handle the challenge.