Skip to content

fix(oauth): stop leaking access-token format across requests (#139)#157

Merged
windischb merged 1 commit into
developfrom
fix/reference-token-shared-options-leak
Jul 19, 2026
Merged

fix(oauth): stop leaking access-token format across requests (#139)#157
windischb merged 1 commit into
developfrom
fix/reference-token-shared-options-leak

Conversation

@windischb

Copy link
Copy Markdown
Contributor

Problem

AccessTokenTypeHandler switched JWT clients onto self-contained tokens by
setting context.Options.UseReferenceAccessTokens = false on a
ProcessSignInContext handler. But context.Options is the process-wide
OpenIddictServerOptions singleton — OpenIddictServerFactory assigns
Options = IOptionsMonitor.CurrentValue, the same cached instance every request
(verified by decompiling OpenIddict 7.5). The handler mutated that singleton and
never restored it (early-return on != Jwt).

Effect: the first JWT client to authenticate flipped the global reference
default off process-wide, so every AccessTokenType.Reference client that
authenticated afterwards silently received a self-contained JWT instead of an
opaque reference token — losing instant revocation. The shared write also
races concurrent requests. It stayed invisible because /connect/userinfo and
/connect/introspect work identically for JWT and reference tokens.

This is the root cause behind the ordering-dependent token-format observation
flagged while building #139 (reference token opaque in a solo run, JWT-shaped in
the full class run).

Fix

Move AccessTokenTypeHandler from ProcessSignInContext to
GenerateTokenContext (a per-request context) and set the per-request
IsReferenceToken / PersistTokenPayload switches for JWT clients on the
access token only. context.Options is never mutated. Refresh tokens are
untouched. Ordered just ahead of CreateTokenEntry so every downstream handler
(notably AttachTokenPayload) sees the override.

Test

Adds ReferenceClient_TokenFormat_IsNotLeaked_By_A_Prior_JwtClient: drives a JWT
client first (which flipped the shared flag under the old code) then a reference
client, asserting the reference token stays opaque (an OpenIddict reference
access token is Base64Url(256 random bytes) — never a dot; a dot ⟹ JWT).
Fails pre-fix (JWT at pos ~110), passes post-fix.

Docs

Updates docs/integrate/oauth.md, whose AccessTokenTypeHandler snippet showed
the old ProcessSignInContext + context.Options mutation — now the
per-request GenerateTokenContext form, with a note on why Options (the shared
singleton) must not be mutated.

Verification

  • Unit 1395/1395 green
  • Integration 517/517 green (incl. the new reproducer)

🤖 Generated with Claude Code

AccessTokenTypeHandler switched JWT clients onto self-contained tokens by
setting context.Options.UseReferenceAccessTokens = false on a
ProcessSignInContext handler. But context.Options is the process-wide
OpenIddictServerOptions singleton (OpenIddictServerFactory assigns
IOptionsMonitor.CurrentValue, the same cached instance every request), and the
handler never restored it. The first JWT client to authenticate flipped the
global reference default off process-wide, so every Reference-configured client
that authenticated afterwards silently received a self-contained JWT instead of
an opaque reference token — losing instant revocation — and the shared write
races concurrent requests.

Move the handler to GenerateTokenContext (a per-request context) and set the
per-request IsReferenceToken + PersistTokenPayload switches for JWT clients on
the access token only. context.Options is never mutated. Refresh tokens are
untouched.

Adds ReferenceClient_TokenFormat_IsNotLeaked_By_A_Prior_JwtClient: drives a JWT
client first (which flipped the shared flag under the old code) then a reference
client, asserting the reference token stays opaque. Fails pre-fix (JWT at
pos ~110), passes post-fix. Unit 1395/1395, integration 517/517 green.

Also updates docs/integrate/oauth.md, whose AccessTokenTypeHandler snippet
showed the old ProcessSignInContext + context.Options mutation — now the
per-request GenerateTokenContext form, with a note on why Options must not be
mutated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@windischb
windischb merged commit 2c66efc into develop Jul 19, 2026
8 checks passed
@windischb
windischb deleted the fix/reference-token-shared-options-leak branch July 19, 2026 22:05
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