fix(server): introspect session-bound refresh tokens against their session - #4963
fix(server): introspect session-bound refresh tokens against their session#4963SashaMIT wants to merge 3 commits into
Conversation
5d4c792 to
fb60e14
Compare
…ssion 533d177 added introspection's sessionAlive check and deliberately excluded refresh tokens, because they outlive the session that issued them. 155557b then let a client tie its refresh tokens to the session (refreshTokenLifetime: session), and rewrote sessionAlive to gate on exactly that flag. introspectAccessToken consults it; introspectRefreshToken never did, so a session-bound refresh token introspects active after its session has ended. Explicit logout is covered: it eagerly deletes bound clients' tokens, so the lookup fails. A session that ends by idle or absolute timeout revokes nothing eagerly, and the token introspects active until its own expiry, even though the refresh grant would refuse to redeem it. Read the token's sid from its offline-session reference, the same source the refresh grant uses, and apply the same sessionAlive check the access-token path applies: a session-bound client whose session has ended introspects inactive. A bound token whose offline session cannot be read is reported inactive, matching the grant's refusal. Standalone clients are unchanged. Signed-off-by: Sasha Mitchell <sash@ela.city>
fb60e14 to
76cb50b
Compare
- Gate sessionAlive on Sessions.Enabled() so disabling sessions cannot flip bound tokens inactive while the refresh grant still redeems them. - Skip GetOfflineSessions for standalone clients (early return). - Report deleted clients as inactive instead of 500. - Share sid resolution with the refresh grant via tokens.RefreshReferenceSessionID; drop unused subject param. - Cover unreadable offline session and empty-sid reference in tests. Signed-off-by: Sasha Mitchell <sash.t.mitchell@gmail.com>
|
Thanks for the PR, I looked through it. The diagnosis holds: introspection and the refresh grant answered differently about the same token, and reading the sid from the offline-session reference is the right source, since that is what the grant reads. Some suggestions.
|
|
Addressed review feedback:
|
|
Thanks, that covers all of it. One leftover of the same kind as the first point: with sessions off and a client marked session-bound, the offline-session read still runs before |
Hoist Sessions.Enabled() above GetOfflineSessions for session-bound refresh introspection so a storage error cannot report inactive while the refresh grant would still redeem the token. Signed-off-by: Sasha Mitchell <sash.t.mitchell@gmail.com>
|
Thanks @nabokihms. Hoisted the
|
Problem
533d177 added
Handler.sessionAliveto token introspection and deliberately excluded refresh tokens, on the grounds that they outlive the session that issued them. 155557b then introduced per-client session binding: a client configured withrefreshTokenLifetime: session(storage.Client.RefreshBoundToSession) has refresh tokens that explicitly do NOT outlive the session, andsessionAlivewas rewritten to gate on exactly that flag.The access-token path got the check; the refresh-token path never did.
introspectAccessTokenconsultssessionAlive, whileintrospectRefreshTokenreturnsactive: truefor any token that passesLookupRefreshToken, without consulting the session.Reachability
Explicit logout is already covered: logout eagerly deletes bound clients' refresh tokens, so the lookup itself fails. But a session that ends by idle or absolute timeout revokes nothing eagerly. Between session end and the token's own expiry, a session-bound refresh token still introspects
active: true, even though the refresh grant would refuse to redeem it (grants/refresh.gosessionID()reads the same flag and the same session, and refuses a dead one). Introspection and the grant disagree about the token's state, and RFC 7662 section 4 requires a revoked token to be reported inactive.Fix
In
introspectRefreshToken, after the lookup succeeds:sessionAlivecheck the access-token path uses. A dead session meansactive: false.A bound token whose offline session cannot be read is reported inactive, matching the grant's refusal in the same situation. For standalone clients the response is unchanged; the path now additionally reads the client and the offline-session reference. Also updates the
IntrospectionExtra.SessionIDdoc comment, which still claimed nothing here consults session storage.Tests
New
TestHandleIntrospectRefreshTokenSessionBinding(server/server_introspection_test.go): a session-bound client whose session ended by timeout (row still stored, both expiries in the past) introspects its refresh token asactive: false; a standalone client with the same dead session staysactive: true. Verified the test fails without the fix.go build ./...go test ./server/ -run 'Introspect|Session' -count=1go test ./server/... -count=1All pass.
Made with Cursor