Knowledge Base

OAuth Logout in Protected Apps

Implementing logout correctly in OAuth apps requires careful attention to the order of operations. This guide covers common pitfalls and best practices.

How Logout Should Work

  1. User clicks "Sign out"
  2. App builds logout URL (with id_token_hint)
  3. App clears local tokens
  4. App navigates to IdP's end-session endpoint
  5. IdP clears its session, redirects to post_logout_redirect_uri
  6. User lands on app, sees login form (since IdP session is cleared)

Common Implementation Bugs

Bug 1: Clearing Tokens Before Building Logout URL

The logout URL should include id_token_hint so the IdP knows which session to terminate. If you clear tokens first, the hint is lost:

// WRONG - id_token cleared before URL is built
const logout = (redirectUri) => {
  tokenStore.clearTokens();  // Clears id_token!
  window.location.href = client.buildLogoutUrl(redirectUri);  // No id_token_hint
};
 
// CORRECT - build URL first while id_token is still available
const logout = (redirectUri) => {
  const logoutUrl = client.buildLogoutUrl(redirectUri);  // Has id_token_hint
  tokenStore.clearTokens();
  window.location.href = logoutUrl;
};

Bug 2: React State Updates Before Navigation

If your logout function updates React state (e.g., setUser(null)), this triggers a re-render. If your component is inside an AuthGuard, the guard sees no user and calls login() - before the navigation to end-session executes.

This creates an apparent "infinite loop" where logout seems to immediately re-authenticate the user:

// WRONG - setUser triggers re-render, AuthGuard calls login() before navigation
const logout = (redirectUri) => {
  const logoutUrl = client.buildLogoutUrl(redirectUri);
  setUser(null);  // Triggers re-render! AuthGuard calls login()!
  tokenStore.clearTokens();
  window.location.href = logoutUrl;  // Never executes
};
 
// CORRECT - don't update React state, just navigate away
const logout = (redirectUri) => {
  const logoutUrl = client.buildLogoutUrl(redirectUri);
  tokenStore.clearTokens();
  // Skip setUser() - page is navigating away, React state doesn't matter
  window.location.href = logoutUrl;
};

Post-Logout Redirect Options

After end-session clears the IdP session, the user is redirected to post_logout_redirect_uri. You have two options:

Option 1: Redirect to / (Simpler)

The user lands on your protected route, AuthGuard redirects to IdP login, and since the IdP session was cleared, they see the login form.

logout();  // Uses default redirect (usually app origin)

Pros: No extra page needed Cons: User immediately sees login form without confirmation they logged out

Option 2: Dedicated /logged-out Page (Better UX)

Create a page outside your protected routes that confirms logout:

// app/logged-out/page.tsx (outside protected route group)
"use client";
import { useOAuth } from "@hikari/citizen/oauth/react";
 
export default function LoggedOutPage() {
  const { login } = useOAuth();
  return (
    <main>
      <h1>Signed Out</h1>
      <p>You have been successfully signed out.</p>
      <button onClick={() => login()}>Sign in again</button>
    </main>
  );
}
// In your navigation
logout(`${window.location.origin}/logged-out`);

Pros: Clear feedback that logout succeeded Cons: Extra page to maintain

Directory Structure for Option 2

app/
├── (protected)/           # AuthGuard wraps this group
│   ├── layout.tsx
│   └── page.tsx
├── auth/callback/         # OAuth callback (outside protected)
└── logged-out/            # Logout landing (outside protected)

Checklist

  • Logout function builds URL before clearing tokens
  • Logout function doesn't update React state before navigation
  • If using /logged-out page, ensure it's outside protected routes
  • post_logout_redirect_uri is registered with the IdP