OIDC RP-initiated logout
Cleanly sign users out of your app and Olympus together
When a user clicks "Sign out" in your app, you want them logged out of:
- Your app's session.
- Olympus's session.
- (Optionally) any downstream OAuth2 clients they granted.
OIDC defines a standard for this: RP-Initiated Logout (the RP = your app).
The flow
1. User clicks "Sign out" in your app.
2. Your app clears local session, redirects to:
https://ciam.your-domain/oauth2/sessions/logout
?id_token_hint=...
&post_logout_redirect_uri=https://your-app/goodbye
&state=...
3. Hydra checks id_token_hint matches a session.
4. (Optionally) Hera prompts "Sign out of all apps?"
5. User confirms.
6. Hydra invalidates the session, calls each tracked client's logout URI.
7. Hydra redirects to post_logout_redirect_uri.Configuration
Register post_logout_redirect_uris
hydra update client your-app-client \
--post-logout-redirect-uri https://your-app.com/goodbye \
--post-logout-redirect-uri https://your-app.com/loginYou can list multiple. The one in the request must match exactly.
In your app
async function signOut() {
// Clear app-local state
localStorage.clear();
// Get current id_token
const idToken = sessionStorage.getItem("id_token");
// Redirect to logout
const params = new URLSearchParams({
id_token_hint: idToken,
post_logout_redirect_uri: `${window.location.origin}/goodbye`,
state: crypto.randomUUID(),
});
window.location.href = `${HYDRA_URL}/oauth2/sessions/logout?${params}`;
}Skip confirmation
By default, Hydra shows a "Are you sure?" page via Hera. Skip:
# hydra.yml
oauth2:
session:
# Trust the post_logout_redirect_uri (must be in client config)
encrypt_at_rest: true
urls:
login: https://your-hera/login
logout: https://your-hera/logout # custom logout pageIn your Hera /logout page:
import { hydraAdmin } from "@/lib/hydra";
export default async function Logout({ searchParams }) {
const challenge = searchParams.logout_challenge;
const request = await hydraAdmin.getOAuth2LogoutRequest({ challenge });
// Skip confirmation:
const accept = await hydraAdmin.acceptOAuth2LogoutRequest({ challenge });
return <Redirect to={accept.redirect_to} />;
}User never sees the prompt, direct sign-out.
Confirming with user
If you want the confirmation:
"use client";
export default function LogoutPage() {
return (
<form action="/api/accept-logout" method="POST">
<p>Sign out of [Your App]?</p>
<button type="submit">Yes, sign out</button>
<button type="button" formAction="/api/reject-logout">Cancel</button>
</form>
);
}Front-channel logout
To notify other RPs that the user signed out, configure frontchannel_logout_uri:
hydra update client your-app-client \
--frontchannel-logout-uri https://your-app.com/oidc-logout \
--frontchannel-logout-session-requiredWhen Hydra processes the logout, it loads https://your-app.com/oidc-logout?sid=... in a hidden iframe. Your app sees this and clears its own session.
This is how you get "log out everywhere" across multiple RPs.
Caveats:
- Requires the user to be at Hydra (front-channel relies on browser visiting).
- Some browsers block third-party iframes (cookie isolation). Test.
Back-channel logout
The push variant: Hydra calls backchannel_logout_uri server-to-server with a logout token (JWT). Your app validates and clears its session.
hydra update client your-app-client \
--backchannel-logout-uri https://your-app.com/oidc-logout \
--backchannel-logout-session-requiredYour app's handler:
app.post("/oidc-logout", async (req, res) => {
const logoutToken = req.body.logout_token;
const { payload } = await jwtVerify(logoutToken, hydraJwks);
if (payload.events?.["http://schemas.openid.net/event/backchannel-logout"]) {
await invalidateSession(payload.sub);
return res.status(200).send();
}
res.status(400).send();
});Cleaner than front-channel, works even if user never visits your app again.
Kratos session logout
The flows above kill the OAuth2 (Hydra) session. To also kill the Kratos session (so user is logged out of Hera too):
await kratos.updateLogoutFlow({ token: kratosLogoutToken });Hera does this automatically when handling the logout flow.
Idempotence
Hitting logout twice should be a no-op, not an error. Hydra handles this, if there's no session, it just redirects.
What can go wrong
post_logout_redirect_uri mismatch
Same as login redirect. Must exact-match registered URIs.
id_token_hint expired
If the ID token is more than ~1h old, Hydra may reject. Tell users to sign out promptly; or include state instead.
Cookies cleared but session lives
The user clears browser cookies but Hydra's DB still has the session. Eventually expires (default 1d). Not a leak, just no longer usable.