What is validated, and why
What is validated, and why in the Sign-in with OAuth and OpenID Connect guide.
These checks are enabled by default.
Before the browser leaves
- State (32 random bytes, bound to the session). Without it an attacker can finish their own sign-in in the victim's browser (login CSRF) and make the victim use the attacker's account. The callback is refused when the state is missing, unknown, repeated, expired or belongs to another browser.
- Nonce (32 random bytes in the ID token). Ties the ID token to this sign-in, so a token captured from another sign-in cannot be replayed.
- PKCE with S256 (a 48-byte verifier). A stolen authorization code cannot be exchanged without the verifier, which never leaves the server. A provider that announces it cannot do S256 is refused.
returnTogoes throughcheckRedirectTargetand must stay on your origin;//evil.test,/\evil.test,javascript:and absolute URLs of other sites become/. It is checked again after the callback, so a tampered session store cannot make it an open redirect.- HTTPS only. The issuer, every discovered endpoint and the redirect URI must be
https.http://localhostis accepted only while the handler runs in development (event.dev), so a production server never sends a code or a secret over plain http.
At the callback
- One use, ten minutes. The waiting sign-in is removed the moment a callback with its state arrives, before any other
check, and it expires after
transactionSeconds(600). Replaying a callback URL getsOIDC_STATE_INVALIDand never reaches the token endpoint. - Mix-up defense. An
issparameter on the callback (RFC 9207) must be exactly the issuer, and is required when discovery says the provider sends it (requireIssParameterforces it). A malicious provider cannot get a code meant for another provider accepted. The discovery document must also name the issuer you configured. - Errors never echo. The
erroranderror_descriptionthe provider sends are kept on the error asproviderError(cut to a safe alphabet) andproviderDescription(200 characters, untrusted) and are never put in the message.
The ID token (OpenID Connect Core 3.1.3.7)
- Signature through the JWKS. Keys come only from the configured
jwks_uri;jku,x5uand an embeddedjwkin the header are never followed. A key must fit the algorithm (type, curve,use,key_ops,alg), and RSA keys under 2048 bits are refused. A token with acritorzipheader is refused, as is a malformed or oversized (64 KiB) token. - Algorithm allowlist. Default
RS256,ES256,PS256,EdDSA; the token'salgmust be in it, and in the list the provider announces.nonecan never be allowed.HS256,HS384andHS512are accepted only for a confidential client that lists them, keyed with the client secret, so an attacker cannot sign with the public key as an HMAC secret. - Key rotation. The key set is cached (
jwksSeconds, default 600). A token with an unknownkidmakes one refetch; a token withoutkidis accepted only when exactly one key fits. - Claims.
issequals the issuer;audcontains your client id;azp, when present, is your client id, and is required whenaudhas several entries;exphas not passed andiat,nbfare not in the future, each withclockSkewSeconds(60);subis a non-empty string;noncematches;at_hash, when present, matches the access token. For a multi-tenant providerissuerMatchesreplaces the exactisscomparison; it must accept only issuers you trust. - Userinfo (
fetchUserinfo: true, always for OAuth 2.0): itssubmust equal thesubof the ID token, so a profile of another user is never merged.
Every call to the provider
- A timeout (
timeoutMs, default 10 s) through anAbortControllerthat also stops when the client disconnects; no redirects are followed (an endpoint that redirects is an error, so a compromised endpoint cannot bounce the server to an internal address); no cookies or credentials; the body is cut at 1 MiB while it streams (maxResponseBytes). - The key set is fetched at most three times in a burst and then once per 30 seconds, whatever tokens arrive, so tokens with
invented
kidvalues cannot turn your server into a client that hammers the provider. A failed fetch never falls back to an expired key set. A failed discovery is not retried for five seconds. - Secrets, codes and tokens are compared with a constant-time comparison where they are compared at all (state, nonce,
at_hash), and never appear in an error message.
The session
- A new session id at sign-in (
signIn): an id planted before the sign-in is dead afterward, and the data (an anonymous cart, the other waiting sign-ins) moves to the new id. - The tokens stay on the server in the session store. Nothing is sent to the browser except the signed session cookie. If
your store is shared (Redis, a database), tokens are at rest there: encrypt the store or pass
storeTokens: falseand use only the user. - CSRF is unchanged. The callback is a
GETthat changes state only through a one-usestatebound to the browser's session; logout and refresh arePOSTroutes behind the handler's CSRF check. The session cookie keepsSameSite=Lax, which is what lets it accompany the top-level redirect back from the provider.