[]previous and next page

What is validated, and why

What is validated, and why in the Sign-in with OAuth and OpenID Connect guide.

For agentsUpdated

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.
  • returnTo goes through checkRedirectTarget and 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://localhost is 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 gets OIDC_STATE_INVALID and never reaches the token endpoint.
  • Mix-up defense. An iss parameter on the callback (RFC 9207) must be exactly the issuer, and is required when discovery says the provider sends it (requireIssParameter forces 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 error and error_description the provider sends are kept on the error as providerError (cut to a safe alphabet) and providerDescription (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, x5u and an embedded jwk in 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 a crit or zip header is refused, as is a malformed or oversized (64 KiB) token.
  • Algorithm allowlist. Default RS256, ES256, PS256, EdDSA; the token's alg must be in it, and in the list the provider announces. none can never be allowed. HS256, HS384 and HS512 are 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 unknown kid makes one refetch; a token without kid is accepted only when exactly one key fits.
  • Claims. iss equals the issuer; aud contains your client id; azp, when present, is your client id, and is required when aud has several entries; exp has not passed and iat, nbf are not in the future, each with clockSkewSeconds (60); sub is a non-empty string; nonce matches; at_hash, when present, matches the access token. For a multi-tenant provider issuerMatches replaces the exact iss comparison; it must accept only issuers you trust.
  • Userinfo (fetchUserinfo: true, always for OAuth 2.0): its sub must equal the sub of the ID token, so a profile of another user is never merged.

Every call to the provider

  • A timeout (timeoutMs, default 10 s) through an AbortController that 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 kid values 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: false and use only the user.
  • CSRF is unchanged. The callback is a GET that changes state only through a one-use state bound to the browser's session; logout and refresh are POST routes behind the handler's CSRF check. The session cookie keeps SameSite=Lax, which is what lets it accompany the top-level redirect back from the provider.