handler: complete the OAuth2 handler port - #52
Merged
Merged
Conversation
guimard
force-pushed
the
fix/oauth2-handler-port
branch
3 times, most recently
from
September 8, 2026 12:13
a832d05 to
d0f8f2a
Compare
The OAuth2 handler used the `jti` claim of a Bearer token (or the raw token) directly as a user session id, and fell back to the main session storage. As a result it diverged from Lemonldap::NG::Handler::Lib::OAuth2 on every point but the token parsing: - a genuine LLNG access token resolved to the access token session itself, which holds no user attribute, so the forged headers were empty - conversely, any user session id was accepted as an access token - token attributes were never exposed, making scope based rules unusable - unauthenticated API calls were redirected to the portal instead of getting the RFC 6750 challenge - the access token was forwarded to the protected application - offline access tokens were not handled fetchId() now resolves the token into its access token session (in the OIDC storage), then follows `user_session_id` or `offline_session_id`, as the Perl handler does. The token payload is still only used to find that session id: the trust anchor is the session storage. To make this possible: - init() loads `oidcStorage`, `oidcStorageOptions` and `oidcRPMetaDataOptions`, which were declared in the TSV but never read - fetchId() may return a promise, since resolving a token needs a store lookup - retrieveSession() receives the request, to reach the per-request token attributes Also adds the first tests of the OAuth2 handler type, including the forged token scenario reported in GHSA-jj3p-7p8x-j82f. Co-Authored-By: Claude
guimard
force-pushed
the
fix/oauth2-handler-port
branch
from
September 8, 2026 15:54
d0f8f2a to
6615247
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Reviewing the report behind GHSA-jj3p-7p8x-j82f (rejected: the
jtiof a LLNGaccess token is the credential, and the Perl reference does not verify the JWT
signature on that path either) showed that the OAuth2 handler port is
incomplete. It used the
jticlaim, or the raw Bearer token, directly as auser session id, with a fallback to the main session storage.
Compared to
Lemonldap::NG::Handler::Lib::OAuth2, the port diverged on everypoint but the token parsing:
which holds no user attribute — the forged identity headers were empty, so
the handler simply did not work with real tokens;
the RFC 6750 challenge;
Changes
fetchId()now resolves the Bearer token into its access token session (inthe OIDC storage), then follows
user_session_idoroffline_session_id, asthe Perl handler does. The token payload is still only used to find that session
id: the trust anchor is the session storage, not the token.
jtiused as user session idjti→ access token session →user_session_id/offline_session_id401_scope,_clientId,_clientConfKey,_oidc_grant_type,_audiences,_accessToken302to the portal401+WWW-Authenticate: Bearer error="invalid_token"AuthorizationheaderSupporting changes:
init.tsloadsoidcStorage,oidcStorageOptionsandoidcRPMetaDataOptions, which were declared in the TSV but never read — sothe OAuth2 handler always used the main session storage, whatever the
configuration said. The OIDC accessor is shared with the main one when both
storages are identical.
fetchId()may now return a promise, since resolving a token requires a storelookup.
run()awaits it; the other handler types are unchanged.retrieveSession()receives the request as an optional second argument, toreach the per-request token attributes (the equivalent of Perl's
$req->data).Attributes are merged into a copy of the session, so the session caches are
never polluted.
@lemonldap-ng/jwt: documented whygetAccessTokenSessionId()verifies nosignature, and what callers must do with its result.
Tests
packages/handler/src/handlerOAuth2.test.ts— 21 tests, the first ones for thishandler type. They run the real handler over an Express app with a File session
backend, in two configurations (dedicated
oidcStorage, and storage shared withuser sessions), and cover:
alg:none), including the GHSA-jj3p-7p8x-j82f scenario:a user session id presented as a Bearer token is now rejected in both
configurations;
Authorizationand of the LLNGcookie;
sharing the handler's internal session cache.
18 of the 21 fail against the previous implementation.
npm run testis green (139 tests), as areeslint,prettier --checkandnpm run build --workspace=packages/handler. Test files are now excluded fromthat package's
tsc --noEmit, as they are in every other package.