Skip to content

Fix/oauth2 jwt scope parking - #136

Closed
juicycleff wants to merge 5 commits into
mainfrom
fix/oauth2-jwt-scope-parking
Closed

juicycleff wants to merge 5 commits into
mainfrom
fix/oauth2-jwt-scope-parking

Conversation

@juicycleff

Copy link
Copy Markdown
Contributor

No description provided.

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.
The README now follows the shape of forge's: a banner, a quick start, Forge integration,
configuration, docs links and a FAQ. The quick start was built and run against the local checkout.

The old quick start couldn't run, because the engine needs Warden, Chronicle and a token encryption
key. The routes are listed under /v1, where they really are.
Every page under docs/content/docs was checked against the source, and the getting-started,
Forge extension and full-example snippets were built and run, with every documented route
checked against the real route table.

NewEngine needs Warden, Chronicle and a token encryption key, routes live under /v1 and not
/v1/auth, and the organization and environment pages use the real plugin and engine methods,
since auth.Orgs() and auth.Environments() don't exist. The bridge pages use the real mailer, SMS
and Chronicle constructors and say the core sends no email itself. plugins/README.md now counts
27 plugins, retention included.
/docs/ui is a folder with no index page, so the website build fails on the link.
An unquoted colon inside a description is invalid YAML, so the website build stops on it.
@juicycleff

Copy link
Copy Markdown
Contributor Author

Closing this one, since everything on the branch is already on main. The JWT scope-parking fix came in through #139, which is the same commit rebased onto current main. The four docs commits landed before that as 4c54de4, 700ffaf, e25e80d and 768578b, and that overlap is why GitHub shows conflicts here. What's left in the diff is just main being newer (the tracing work from #137), so there's nothing to carry over.

The fix/oauth2-jwt-scope-parking branch goes too. If you need it back, GitHub can restore it from this page.

@juicycleff juicycleff closed this Oct 20, 2026
@juicycleff
juicycleff deleted the fix/oauth2-jwt-scope-parking branch October 20, 2026 01:16
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