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_endpointwithid_token_hintandpost_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
sidorsub). - 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.