Skip to content

fix(auth): logout revokes the session an id_token_hint points at - #163

Open
romanetar wants to merge 1 commit into
mainfrom
fix/logout-revokes-id-token-hint-session
Open

romanetar wants to merge 1 commit into
mainfrom
fix/logout-revokes-id-token-hint-session

Conversation

@romanetar

@romanetar romanetar commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

ref: https://app.clickup.com/t/86bc57f8f

Problem

After a user logs out of the IDP, an RP still holding their id_token could present it as id_token_hint on /oauth2/auth and reattach to the old, still-authenticated session for the rest of the token lifetime (3600 s by default).

Two independent causes in AuthService:

  1. invalidateSession() wrote its "void" marker under Crypt::encrypt(session id), while reloadSession() looked it up under the ciphertext generateJTI() stored earlier. Laravel's Encrypter uses a random IV per call, so the keys never match and the "session was marked as void" branch could never fire.
  2. logout() used Session::flush() + Session::regenerate() (destroy=false), so the old session's server-side data, including the auth key, stayed in the store until it expired.

The sub-based fallback added in #158 also had no revocation signal to consult.

Changes

  • Revocation marker keyed by sha256(session id). reloadSession() derives the same key from the session id the jti maps to. The jti mapping format is unchanged, so tokens already issued keep working.
  • A marked session is always rejected, even when the caller is already authenticated (the old check only rejected when !Auth::check()), and never falls back to the sub.
  • logout() now calls Session::invalidate() so the old session id's data is removed from the store.
  • logout() records a per-user "logged out at" timestamp. The sub-based fallback rejects a hint whose auth_time is at or before it. auth_time is the hint's auth_time claim, else iat, so this covers any token minted before the logout. A user who logs in again gets hints with a later auth_time, which stay valid.
  • Both markers have a 30 day TTL: AuthService has no access to OAuth2.IdToken.Lifetime (admin configurable), so this is a fixed cap that outlives any live token.

Behaviour to review

  • The "logged out at" marker is global per user and is also written by the internal logout() calls (account switch in processUserHint, OpenID handler, end-session). Logging out revokes hints minted for the user's other devices too.

Tests

  • Unit (AuthServiceReloadSessionTest): marker written by invalidateSession() is found by reloadSession(); a revoked session is neither resumed nor falls back to the sub; the sub fallback rejects a hint from before the logout (cache miss and failed resume) and accepts one from after.
  • Unit (AuthServiceLogoutTest): adapted to Session::invalidate(), plus a test for the marker keys and TTLs.
  • E2E (OIDCColdSessionReloadTest::testLogoutRevokesIdTokenHintSessionReload): mint an id_token by back channel, log out, assert the old session id resolves no data in the store, then present the hint from a cold session and assert a 302 to /auth/login and Auth::check() === false. It fails against the previous AuthService and passes with this change. The logout goes through IAuthService::logout() rather than the HTTP route: an action() request carries no session cookie, so StartSession would run the controller on a fresh session.
  • Not covered by E2E: the sub-fallback path with the jti evicted. The seeded client signs with HS512 (its own secret), so its hints never get sub-fallback semantics. That path is covered by the unit tests only.

Full suite: 266 tests, 9 failures. All 9 fail identically on main without this change: OAuth2EndSessionTest (1, redirects to https because of URL::forceScheme), PasswordChangeRevokeTokenTest (3, 302 instead of 201/204) and TurnstileProtectedControllersTest (5). OIDCColdSessionReloadTest (4 tests) and the logout/reload unit tests pass.

Summary by CodeRabbit

  • Bug Fixes
    • Logging out now revokes the current session, preventing it from being restored through a stale sign-in hint.
    • Sign-in hints issued before a user’s most recent logout are rejected, while hints issued afterward can still be used to sign in.
    • Revoked sessions are not resumed or used to authenticate through an alternate fallback.

invalidateSession() keyed its "void" marker by Crypt::encrypt(session id),
but reloadSession() looked it up with the ciphertext generateJTI() stored
earlier. Laravel's Encrypter uses a random IV per call, so the keys never
matched and the "session was marked as void" branch could not fire. On top
of that logout() used Session::flush() + regenerate() (destroy=false), so the
old session's data stayed in the store until it expired.

After logging out, any RP still holding the user's id_token could present it
as id_token_hint and reattach to the old, still-authenticated session (or,
for IDP-signed hints, log the user in through the sub-based fallback).

- Key the session revocation marker by sha256(session id); reloadSession()
  derives the same key from the session id the jti maps to. The jti mapping
  format is unchanged.
- A marked session is always rejected, even if the caller is already
  authenticated, and never falls back to the sub.
- logout() now calls Session::invalidate() so the old session id's data is
  removed from the store.
- logout() records a per-user "logged out at" timestamp; the sub-based
  fallback rejects a hint whose auth_time is at or before it.
- Tests: unit coverage for the marker round trip, revoked sessions and the
  logged-out-at check; adapt the logout tests to Session::invalidate();
  E2E in OIDCColdSessionReloadTest.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: f3aca072-cd6e-4f86-b348-137756ed15a4

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa9925 and 1abf68d.

📒 Files selected for processing (4)
  • app/libs/Auth/AuthService.php
  • tests/AuthServiceLogoutTest.php
  • tests/AuthServiceReloadSessionTest.php
  • tests/OIDCColdSessionReloadTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Logout now records user and session revocation markers. Session reload checks those markers and rejects revoked sessions or reload hints issued at or before the user’s recorded logout time. Tests cover logout, reload, and cold-session OIDC behavior.

Changes

Logout and Session Reload

Layer / File(s) Summary
Record logout markers and invalidate sessions
app/libs/Auth/AuthService.php, tests/AuthServiceLogoutTest.php
Logout writes a user logout timestamp and a session-revocation marker keyed by the session ID’s SHA-256 hash. Both markers have a 30-day TTL. Logout invalidates the session, and tests check the writes and their ordering.
Enforce markers during session reload
app/libs/Auth/AuthService.php, tests/AuthServiceReloadSessionTest.php, tests/OIDCColdSessionReloadTest.php
Session reload rejects revoked sessions before switching sessions or using the subject fallback. Reload-hint authentication rejects hints whose auth_time is at or before the recorded logout timestamp. Tests cover marker detection, timestamp comparisons, and a cold-session OIDC reload after logout.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: smarcet

Merge Risk: ⚪ Minimal · up to 1abf6

The remaining concerns do not warrant a code change before merge; the logout revocation behavior is consistent with the stated policy.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.88% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the primary change: logout now revokes the session referenced by an id_token_hint.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-163/

This page is automatically updated on each push to this PR.

@romanetar
romanetar requested a review from smarcet September 29, 2026 16:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant