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
- User clicks "Sign out"
- App builds logout URL (with
id_token_hint) - App clears local tokens
- App navigates to IdP's end-session endpoint
- IdP clears its session, redirects to
post_logout_redirect_uri - 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-outpage, ensure it's outside protected routes -
post_logout_redirect_uriis registered with the IdP