NextAuth Authentik Session Termination OIDC Flow Fix

Summary

A critical session mismatch occurred where the application-level session (NextAuth) was terminated, but the Identity Provider (IdP) session (Authentik) remained active. This resulted in a “Zombie Session” state: users appeared logged out of the application, but any attempt to re-authenticate immediately re-established the session without user interaction. This creates a severe security risk in shared terminal environments.

Root Cause

The failure stems from a misunderstanding of Distributed Session Management in OIDC flows.

  • Local vs. Remote Sessions: signOut() only destroys the local application cookie. It has no authority over the cookies set by auth.example.com.
  • OIDC End-Session Failure: The implementation attempted an OIDC Logout flow, but the IdP (Authentik) failed to respect the post_logout_redirect_uri parameter.
  • IdP Configuration Mismatch: Authentik’s “Default Invalidation Flow” is being triggered because the Redirect URI provided in the logout request was not explicitly whitelisted in the Authentik Application settings, or the id_token_hint was missing/invalid, causing the IdP to treat the request as an unauthenticated logout attempt.

Why This Happens in Real Systems

In modern microservices and SSO architectures, authentication is a two-step handshake:

  1. The App Session: A short-lived cookie used by your Next.js server to identify the user.
  2. The IdP Session: A long-lived cookie held by the Identity Provider (Authentik, Okta, Auth0) that allows for Single Sign-On (SSO).

When you only clear the local session, you are essentially closing the door to your house while leaving the master key in the lock. The next time the user “knocks” (tries to log in), the IdP sees their valid master key and immediately lets them back in without asking for credentials.

Real-World Impact

  • Account Takeover Risk: In public or shared computing environments (libraries, kiosks), the next user gains immediate access to the previous user’s account.
  • Broken Security UX: Users believe they are secure because they “logged out,” leading to a false sense of privacy.
  • Compliance Violations: Failure to implement complete session termination can violate SOC2 or GDPR requirements regarding session management and access control.

Example or Code (if necessary and relevant)

// The INCORRECT way: Only clears local session
await signOut({ redirect: false });
router.push('/login');

// The CORRECT way: Orchestrating both local and remote termination
export async function handleGlobalLogout(idToken: string) {
  // 1. Clear the local NextAuth session
  await signOut({ redirect: false });

  // 2. Construct the OIDC End Session URL
  const idpLogoutUrl = new URL("https://auth.example.com/application/o/my-app/end-session/");

  // id_token_hint is MANDATORY for many IdPs to identify which session to kill
  idpLogoutUrl.searchParams.set("id_token_hint", idToken);

  // This MUST be pre-registered in the Authentik 'Redirect URIs' list
  idpLogoutUrl.searchParams.set("post_logout_redirect_uri", "https://myapp.example.com/login");

  // 3. Perform the hard redirect to the IdP
  window.location.href = idpLogoutUrl.toString();
}

How Senior Engineers Fix It

  1. Verify IdP Whitelisting: Ensure that the post_logout_redirect_uri is explicitly added to the Allowed Redirect URIs in the Authentik provider configuration. If it isn’t an exact match, the IdP will intercept the flow with an error/confirmation screen.
  2. Implement id_token_hint: Never attempt an OIDC logout without passing the id_token. Without it, the IdP doesn’t know which specific user session is being terminated and defaults to a generic, interactive logout UI.
  3. Session Synchronization: Ensure the id_token is being persisted in the NextAuth jwt or session callback so it is available at the moment of logout.
  4. End-to-End Testing: Validate the flow by logging in, closing the browser tab, opening a new tab, and ensuring the user is actually prompted for credentials.

Why Juniors Miss It

  • Local-First Mindset: Juniors often assume that signOut() is a global command that handles everything. They focus on the Application Code and ignore the Infrastructure/IdP Configuration.
  • Ignoring the Protocol: They treat OIDC as a “black box” rather than a strict protocol with specific requirements like id_token_hint and strict redirect URI matching.
  • Testing Shallowly: They test logout by checking if their local useSession() returns null, which passes even if the IdP session is still perfectly intact.

Leave a Comment