OIDC discovery, nonce and logout

OAuth, OIDC & JWT · Advanced · 15 min

Real SSO integrations need correct session handling, not just a successful login.

Discovery: GET {issuer}/.well-known/openid-configuration returns JSON with authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, end_session_endpoint, and supported scopes, response types and algorithms. Configure clients from it rather than hard-coding URLs, and check that its issuer exactly equals the issuer you expect.

ID token validation in full: signature via jwks_uri, iss exact match, aud contains your client id (and azp if several audiences), exp/iat sane, nonce matches the value stored for this login attempt. Use auth_time and the max_age request parameter when you need recent authentication; prompt=login forces re-authentication and prompt=none checks for an existing session silently.

Logout in SSO is about several sessions: your app's session, the provider's session, and other apps' sessions.

  • RP-initiated logout: clear your session, then redirect to end_session_endpoint with id_token_hint and post_logout_redirect_uri.
  • Back-channel logout: the provider POSTs a signed logout token to your registered endpoint when the user logs out elsewhere; validate it and destroy the matching session (by sid or sub).
  • Front-channel logout uses hidden iframes and is increasingly broken by third-party cookie blocking.

Store state, nonce and the PKCE verifier together per login attempt so parallel logins in multiple tabs do not overwrite each other.

Example

// Configure a client from discovery (Node 22+)
const issuer = "https://accounts.google.com";
const config = await (await fetch(`${issuer}/.well-known/openid-configuration`)).json();
if (config.issuer !== issuer) throw new Error("issuer mismatch");

console.log(config.authorization_endpoint); // https://accounts.google.com/o/oauth2/v2/auth
console.log(config.jwks_uri);               // https://www.googleapis.com/oauth2/v3/certs
console.log(config.code_challenge_methods_supported); // [ 'plain', 'S256' ]

// RP-initiated logout URL (when the provider supports end_session_endpoint)
function logoutUrl(cfg, idToken) {
  const u = new URL(cfg.end_session_endpoint);
  u.searchParams.set("id_token_hint", idToken);
  u.searchParams.set("post_logout_redirect_uri", "https://app.example.com/bye");
  return u.toString();
}

Remember

  • Discovery gives endpoints and jwks_uri; verify its issuer
  • Validate nonce, aud/azp, iss and times on every ID token
  • Logout spans app and provider sessions: RP-initiated and back-channel
  • Keep state, nonce and verifier per login attempt

Watch out for

  • Clearing only the local session, so the next 'Log in' silently signs the user back in via the provider session.

Check yourself

  1. After your app clears its own session, clicking Login immediately logs the user back in without a password. Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: openid.net/specs/openid-connect-discovery-1_0.html