Guides / OAuth and OpenID Connect
OAuth and OpenID Connect.
Register a durable client and complete the ComplicatedAuth authorization-code profile with explicit consent, S256 PKCE, pairwise subjects, signed tokens, UserInfo, and revocation.
openid profile email scopes, pairwise subjects, optional exact Resource Server indicators and delegated scopes, and no refresh tokens. No OAuth access token authorizes ComplicatedAuth management APIs.Resource boundaries
An OAuth Application is a Tenant-owned client registration, not a Project and not a Project service account. Its immutable type is public or confidential; its client ID is a public opaque identifier; and its redirect URIs are an exact canonical set. Confidential client secrets are separate expiring resources because reusable credential values must never appear in ordinary application reads.
Resource Servers, delegated scopes, and administrative client grants are separate authorization resources. See Delegated authorization before requesting a non-UserInfo audience.
Deployment origins
OAUTH_ISSUERis the stable public API origin published in discovery and tokeniss. Production requires HTTPS. It is configured, not inferred fromHostor forwarded headers.CONSOLE_ORIGINis the stable first-party browser origin used for authentication, consent, CSRF checks, and console cookies.INTERNAL_API_URLis only the console container’s runtime upstream. It is deployment topology and never appears in the public protocol.
Changing an issuer creates a different security domain and invalidates client discovery assumptions. Treat it like a data migration, not a routine environment rename.
Register an application
- Open OAuth applications in the console and register a type, name, and one or more exact redirects.
- For a confidential client, create an expiring client secret and copy it into a backend secret manager. Public clients never receive a secret.
- Retain the representation
versionor responseETag. Updates and deletion require the current strongIf-Matchvalue. - During rotation, create the replacement, deploy it, confirm
last_used_at, then revoke the old credential. At most two unexpired active secrets are permitted.
Application and secret creation require Idempotency-Key. Exact retries replay the same client ID or one-time secret response for 24 hours; changed inputs conflict. Deletion tombstones the application and permanently reserves its client ID.
Discover the provider
GET {OAUTH_ISSUER}/.well-known/openid-configuration
GET {OAUTH_ISSUER}/oauth/jwksCache discovery and JWKS according to Cache-Control. When a valid token names an unknown kid, refresh JWKS once before rejecting it. Never fetch a JWKS URL supplied by the token itself.
Authorization request
Generate a fresh code verifier, state, and nonce per browser attempt. Store them in the client’s server-side login transaction or a secure same-site session; do not put the verifier in the authorization URL.
const verifier = base64url(randomBytes(32));
const challenge = base64url(sha256(verifier));
const authorize = new URL(discovery.authorization_endpoint);
authorize.search = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: "https://app.example.com/oauth/callback",
scope: "openid profile email",
state,
nonce,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString();ComplicatedAuth validates the active client and exact redirect before any redirect occurs. It then creates a hashed ten-minute request handle and places that handle in the console URL fragment, keeping it out of HTTP request lines. The console safely resumes it through login, shows the client, redirect host, scopes, and expiry, and requires an explicit approve or deny decision.
Callback and code exchange
At the callback, require the original state and the configured issuer response parameter before exchanging the code. Reject missing, duplicated, or unexpected parameters.
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://app.example.com/oauth/callback",
code_verifier: verifier,
});
const response = await fetch(discovery.token_endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: "Basic " + Buffer.from(CLIENT_ID + ":" + CLIENT_SECRET).toString("base64"),
},
body,
});Public clients omit Basic authentication and include client_id in the form. The code is five-minute, one-time, and bound to the client, exact redirect, and verifier. If the successful token response is lost after the code is consumed, start a new authorization flow; retrying cannot safely recreate a standard token response.
Validate the ID token
- Allow only the discovered
RS256algorithm and select the published key bykid. - Verify the signature, exact issuer, client ID audience, expiry, issued-at sanity, and the original nonce.
- Use
subas the external account key. It is stable for one member/application pair and deliberately differs between applications. - Read
nameonly withprofileand email claims only withemail. Do not join accounts solely by email.
Access token, UserInfo, and revocation
Without a resource indicator, the JWT access token is audience-bound to the issuer’s UserInfo endpoint and expires after ten minutes. Call UserInfo with Authorization: Bearer …; the server checks expiry, application and member status, audience, and database revocation before returning scoped claims. Resource-bound tokens are deliberately rejected by UserInfo.
Confidential clients authenticate to /oauth/revoke with Basic; public clients submit client_id. Revocation returns success even for unknown tokens, as required to avoid a token oracle. Tenant Members may also revoke the application grant under My account, which revokes all server-tracked access tokens for that member/application pair.
Failure and retry rules
- Authorization errors redirect only after exact client and redirect validation. An untrusted redirect receives a local HTML error.
- Token, UserInfo, and revocation endpoints use the OAuth
error/error_descriptionshape rather than the management API envelope. - Do not retry invalid grants or client authentication. A code replay is always
invalid_grant. - Consent decisions are idempotent because they can return one-time codes; reuse their original key only for the identical decision.
- Never log codes, request handles, verifiers, client secrets, access tokens, ID tokens, or complete callback URLs.