Olympus Docs
CookbookTokens & OAuth2

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:

  1. Your app's session.
  2. Olympus's session.
  3. (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/login

You 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 page

In 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-required

When 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-required

Your 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.

On this page