Skip to content

Add id_token_hint support#5814

Open
tung2744 wants to merge 17 commits into
authgear:mainfrom
tung2744:dev-3722-rp-init-logout
Open

Add id_token_hint support#5814
tung2744 wants to merge 17 commits into
authgear:mainfrom
tung2744:dev-3722-rp-init-logout

Conversation

@tung2744

@tung2744 tung2744 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

requires

ref DEV-3722

How to test:

  1. Clone this repo: https://github.com/tung2744/test-oidc
  2. Fill client id and client secret (You can test public client by leaving it empty)
  3. Whitelist http://localhost:15001/callback as redirect uri, http://localhost:15001/ as post_logout_redirect_uri.
  4. Try login and logout
  5. Change OIDC_END_SESSION_METHOD to POST to test for POST method

Points to note:

  • In this PR, I changed the spec a bit: Now id_token_hint do not only match the exact same session of sid, but the session in the same sso group
  • For any offline grant (refresh tokens) that created from an idp session (By continue screen), now they will be treated as the same SSO group as the parent idp session. I guess this practically did not change anything: Offline grant can only be created with idp_session_id set is by the continue screen, and user only see continue screen if sso is enabled. But we can discuss on this if you see any concern.

@carmenlau carmenlau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase.

tung2744 and others added 17 commits July 24, 2026 16:39
The end_session_endpoint silently falls back to the Settings page on
an invalid post_logout_redirect_uri instead of returning an error.
Document this as intentional API debt kept to avoid a breaking change,
not the ideal behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plans the missing id_token_hint handling for the RP-initiated logout
endpoint: a stateless POST stash to keep the hint out of the URL, and
resolving the hint's session by SSO group membership rather than sid
equality.
A POST to end_session can't reliably see the Lax session cookie, so
the request needs to be stashed and replayed as a same-origin GET.
Sealing it with a random per-request AES-256-GCM key carried in a
short-lived cookie avoids exposing id_token_hint in the redirect URL
without needing a server-side store.
The confirmation page redirect needs to forward the rest of the
request without re-exposing id_token_hint in the new URL.
Adds the spec's id_token_hint fast path: if the hint's sid resolves to
a session or offline grant in the same SSO group as the current IdP
session, and the client is first-party, log out directly instead of
showing the confirmation page. SSO group membership is used rather
than sid equality, since a first-party client's normal offline_access
grant is bound to its own refresh-token session, not the IDP session
cookie directly.

A POST is stashed and redirected to itself as a GET first, so the Lax
session cookie is visible before the decision is made. An invalid or
expired stash (e.g. a stale link from browser history) is logged and
treated as a parameterless request rather than surfacing as an error.

Once id_token_hint is given at all, its resolution is the sole
authority on the direct-logout decision; the pre-existing SameSiteStrict
fast path is only consulted when no hint is present, so it can no
longer silently override a hint that failed to match.
Adds what's needed to exercise id_token_hint end-to-end: exposing the
raw ID token from an exchange_code result, options to drive a second
OAuth client and enable SSO session cookies during setup, an action to
approve the OAuth consent screen, and an action to clear specific
cookies (the test client's jar doesn't model SameSite, so a
same-site-only cookie set during login otherwise leaks into later
requests). Also adds a shared third-party client fixture.

All additions are backward compatible: existing callers keep their
current defaults.
Covers: first-party client with a matching hint (GET and POST/stash
round trip), third-party client (confirmation page despite a matching
hint), SSO-group mismatch, a malformed hint, no session at all, and an
invalid/expired POST stash. All cases use the normal offline_access +
exchange_code flow to obtain id_token_hint, matching how a real client
would.
…2e tests

Every case only asserted the response's own status/redirect target,
never that a session or offline grant was actually deleted from (or
preserved in) storage. Add a refresh_token field to the exchange_code
result and, after each logout or no-op assertion, present it to an
independent code path (the refresh_token grant, which looks the grant
up from storage) expecting invalid_grant after a real logout or 200
after a confirmed no-op.

This also surfaced that logging in as a second user while the client's
jar still carried the first user's session cookie causes the server to
delete that session and its offline grant outright, unrelated to
end_session, which had been letting the sid-mismatch case pass for the
wrong reason (the hint failing to resolve at all, not resolving to a
different SSO group). Fixed by resetting the cookie jar between logins.
Documents the refresh_token-based revocation checks and the "one login
per browser" behavior discovered while adding them, per §13.1 points
9-10.
…mation

Case titles and comments still described the pre-redesign sid-equality
check even though the production comparison moved to SSO-group
membership; renamed to match. Also add a case simulating a human
actually confirming logout on the /logout page: no case exercised
that handler before, only that the page was reached.
…onsumed

Both properties already existed (a 5-minute MaxAge, and an
unconditional clear on every resumed request via resumeFromStash's
defer) but were only implicitly covered by response-code assertions.
Make them explicit so a regression here would fail loudly.
The cookie's actual on-wire name didn't read as related to
x_end_session_ref, the query param it pairs with. Renamed so the two
are recognizable together, and to better reflect that the cookie holds
the key decrypting the query's sealed value, not "the stash" itself.
…Enabled

An ordinary OIDC client that never sends x_sso_enabled (the common
case, since that's an Authgear-specific extension) still authenticates
through a real IDP session, but its offline grant's SSOEnabled
defaults to false, so IsSameSSOGroup always denied it the silent-logout
fast path even though IDPSessionID demonstrably named that exact
session. OfflineGrant.IsSameSSOGroup no longer gates its own (the
grant's) side of the comparison on SSOEnabled; only raw IDPSessionID
equality is checked. The argument's side is untouched, so grant-to-grant
comparisons still correctly require both sides to have opted in.

session.Manager.invalidate reuses this same method, so revoking the IDP
session now also revokes this grant automatically, with no special
casing needed in end_session's own handler. This also affects
sessionlisting.go's "current device" flag for the same grant, which is
the same fix applied consistently, not a regression: updated its test
to match.

Adds SetupOAuthOptions.SSOEnabledOmitted to the e2e harness (the
existing option always sent x_sso_enabled explicitly, true or false,
so there was no way to construct a request that omits it entirely) and
a test reproducing the exact scenario end-to-end, verified against a
live server.
…up fix

RP-Initiated Logout's id_token_hint section previously said "its sid
matches the current logged in IdP session" without defining what
"matches" means; spell out the two cases (the session itself, or a
refresh token grant whose idp_session_id names it, regardless of
x_sso_enabled). Updates oidc-sso-browser.md's Logout and Session
listing sections to reflect that revoking an IdP session now sweeps
every refresh token grant created from it regardless of that grant's
own sso_enabled.
Documents the live HAR-trace debugging session, the two alternative
designs considered and rejected (a separate IsCreatedFromSession check
with a Handle-level double-Logout call; deriving SSOEnabled from
IDPSessionID at token-issuance time), and the final IsSameSSOGroup fix,
including its sessionlisting.go consequence.
@tung2744
tung2744 force-pushed the dev-3722-rp-init-logout branch from ffc3c56 to 1ebc85c Compare July 24, 2026 08:39
@tung2744

Copy link
Copy Markdown
Contributor Author

Rebased, thanks!

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.

2 participants