Skip to content

fix: park third-party OAuth2 JWTs until a scoped route admits them - #139

Merged
juicycleff merged 1 commit into
mainfrom
fix/oauth2-jwt-scope-parking-main
Oct 20, 2026
Merged

juicycleff merged 1 commit into
mainfrom
fix/oauth2-jwt-scope-parking-main

Conversation

@juicycleff

Copy link
Copy Markdown
Contributor

This lands the scope-parking fix from fix/oauth2-jwt-scope-parking on top of current main.

In JWT token mode, an OAuth2 access token issued to a third-party client skipped parkOAuthSession, so it could reach routes its granted scopes never covered. Opaque tokens were already parked. JWT access tokens now carry the RFC 9068 client_id claim, the auth middleware reads it, and a third-party JWT stays parked until a route its scopes admit. oauth2_jwt_scope_e2e_test.go drives the whole flow.

Only that one commit comes across. The four docs commits on the old branch are already on main, which is why it showed conflicts. You can delete fix/oauth2-jwt-scope-parking once this merges.

Build, vet, go test -race ./... and golangci-lint pass locally with GOWORK=off.

On an app configured for JWT access tokens, a token the OAuth2 provider
issues to a client is now held back by the auth middleware until a route
tagged with RequireScope admits it, the same as an opaque token. Before
this, that token authenticated as the full user on every route, whatever
scopes the client was granted. Userinfo also returned every claim, since
it saw no session and took the caller for a first-party login.

Parking keys off the session on the request context. The opaque path loads
the session row and finds its ClientID there, but the JWT path loads no
row, so a client's JWT never looked like a client's token. JWTs now carry
the RFC 9068 client_id claim, and tryJWTAuth rebuilds a session from the
claims when it's present, which parkOAuthSession then parks.

Both places that mint a JWT set the claim: issueTokens in the plugin, and
the engine's rotateSession, which the OAuth2 refresh grant goes through.
Without the second one a token would escape again on its first refresh.

Tokens minted before this change carry no client_id and keep full access
until they expire. If you can't wait out one access token lifetime, revoke
the outstanding OAuth2 grants when you deploy.

Covered by TestOAuth2IssuedJWTIsParkedUntilAScopedRouteAdmitsIt, which
issues a token through the real authorize and token endpoints, refreshes
it, and presents both tokens to the engine's middleware.
@juicycleff
juicycleff merged commit d1f9a0e into main Oct 20, 2026
36 of 37 checks passed
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