Add jwt-bearer grant for ID-JAG (Identity Assertion Authorization Grant) support - #462
Conversation
Adds a built-in `jwt-bearer` grant (urn:ietf:params:oauth:grant-type:jwt-bearer,
RFC 7523) implementing the Identity Assertion Authorization Grant (ID-JAG)
draft, letting this library act as the Resource Authorization Server side of
a Cross App Access exchange.
- lib/utils/jwt-util.js: dependency-free JWS decode/verify (RS256/ES256/PS256)
using only Node's built-in crypto module.
- lib/grant-types/jwt-bearer-grant-type.js: the grant itself - assertion
parsing, typ/alg checks, issuer trust + key resolution, claim validation,
replay protection, user resolution, permission hook, scope narrowing, and
token issuance without ever setting a refresh token.
- lib/handlers/token-handler.js, lib/server.js: wire the grant into the
built-in grantTypes map and thread through the new tokenEndpointUri /
idJagClockSkew / jwtBearerAllowedAlgorithms / jwtBearerAllowPublicClients
options.
- lib/model.js, index.d.ts: new required model hooks (getTrustedIssuer,
getRequestingIssuerKey, getUserFromIdJagAssertion, validateIdJagPermission,
validateJti / isJtiUsed+recordJti) and their TypeScript types.
- docs/guide/{grant-types,model}.md, examples/express-id-jag-server.js: usage
docs and a runnable example.
- test/{unit,integration}/...: unit + integration coverage, including the
full security matrix (type confusion, algorithm confusion, audience/claim
validation, clock skew, replay, scope narrowing, refresh-token suppression).
|
@dhensby how should we proceed with PRs that are geared toward non standard RFCs? The RFC 7523 is a draft and likely become standard but it remains unclear when this will happen. |
|
also, how far is this related to #453 ? |
|
@jankapunkt Thanks — both fair questions, and they have a shared answer. Draft-spec statusQuick clarification on scope: RFC 7523 itself is a published standard, so the grant type URN and bearer-assertion mechanics are settled ground. What's at draft is the ID-JAG profile ( I hit the same question implementing ID-JAG for Python Authlib, and the approach we settled on there worked cleanly — authlib/authlib#898, where the ask was exactly this ("ID-JAG is a draft spec. Should it be put into rfc7523?"). The resolution, now merged and released:
The equivalent here would be:
Important to highlight: this also removes the need for the new server options. Because extendedGrantTypes: {
'urn:ietf:params:oauth:grant-type:jwt-bearer':
IdJagGrantType.configure({ tokenEndpointUri: 'https://rs.example.com' }),
}I've verified this end-to-end against Relationship to #453With the above, the overlap largely resolves itself. #453 is the broader and better-positioned change — it makes client authentication pluggable instead of special-casing assertions in the token handler, covers What this PR adds on top is the ID-JAG profile specifically:
So: #453 as the stable generic grant, this as an opt-in draft-scoped profile. Either can land first and neither blocks the other. The one constraint to document is that a deployment binds only one implementation to the URN at a time — The authlib experience is also why I'd suggest keeping this decoupled from #453's grant class rather than subclassing it: we tried inheritance first and it meant disabling three inherited hooks with If the drafts-namespace approach looks right to you, I'll restructure along those lines in one push. Let me know if you'd rather it were shaped differently before I move it. |
Summary
Adds a built-in
jwt-bearergrant (urn:ietf:params:oauth:grant-type:jwt-bearer, RFC 7523) implementing the Identity Assertion Authorization Grant (ID-JAG) draft, so this library can act as the Resource Authorization Server side of a Cross App Access exchange: it verifies an ID-JAG assertion minted by an external Identity Provider and, once verified, issues a locally-scoped access token. Minting the ID-JAG itself (the IdP side, RFC 8693 Token Exchange) is out of scope for this grant.The library currently has zero JWT/crypto dependencies, and per
CONTRIBUTING.mdnew tight dependencies shouldn't be introduced without discussion, so signature verification (RS256/ES256/PS256) is implemented using only Node's built-incryptomodule — no new npm dependency.Linked issue(s)
This continues the discussion in #411 (ecosystem-wide ID-JAG tracking + the requirements spec for this library specifically). I have not yet opened a formal tracking Issue for this PR — happy to do so if a maintainer confirms the existing Discussion isn't sufficient per the "no PR without an issue" contribution guideline. Opening this as a Draft for that reason.
Involved parts of the project
lib/utils/jwt-util.js(new): dependency-free JWS decode/verify (RS256/ES256/PS256) using Node's built-incrypto.lib/grant-types/jwt-bearer-grant-type.js(new): the grant itself.lib/handlers/token-handler.js,lib/server.js: register the grant as built-in (notextendedGrantTypes, since this is a registered IETF grant type) and thread through new options:tokenEndpointUri(required — this AS's own RFC 8414 issuer identifier),idJagClockSkew,jwtBearerAllowedAlgorithms,jwtBearerAllowPublicClients.lib/model.js,index.d.ts: 5 new model hooks —getTrustedIssuer,getRequestingIssuerKey,getUserFromIdJagAssertion,validateIdJagPermission,validateJti(orisJtiUsed+recordJti) — only required if this grant is used, following the same conditionally-required pattern as e.g.getRefreshTokenfor therefresh_tokengrant.docs/guide/grant-types.md,docs/guide/model.md,examples/express-id-jag-server.js: usage docs and a runnable example.OAuth2 workflow involved: token endpoint (
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) only. No changes toauthorize/authenticate.Added tests?
Yes —
test/unit/grant-types/jwt-bearer-grant-type_test.js,test/integration/grant-types/jwt-bearer-grant-type_test.js,test/unit/utils/jwt-util_test.js. The integration suite mints real signed JWTs (RS256/ES256/PS256) and covers the full security matrix:typtype-confusion,algallow-list violations (none,HS256), audience mismatch, expired/future/clock-skew-boundary timestamps, tampered signatures, untrusted issuer, unresolvable key,client_idclaim mismatch, replay (bothvalidateJti()andisJtiUsed()/recordJti()model shapes, plus fail-closed on a throwing replay store), refresh-token suppression, and scope intersection/narrowing.npm run lintandnpm test(528 passing) are clean.OAuth2 standard
draft-ietf-oauth-identity-assertion-authz-grant-03) — the profile this grant implements claim-by-claim (Sections 3.1, 4.4.1, 4.4.3, 8.1, 9 referenced directly in code comments/JSDoc).audis validated against this AS's issuer identifier.OAuthErrorsubclasses (InvalidGrantError,InvalidRequestError,InvalidClientError,InvalidScopeError); no new error classes were needed. Per the draft's error-handling guidance, assertion-validation failures all share one non-specific message so a response can't be used as an oracle for which check failed.Reproduction
See
examples/express-id-jag-server.jsfor a full runnable example (mints a self-signed test assertion and exchanges it end-to-end with no external IdP required —npm install express && node examples/express-id-jag-server.js).Additional note for maintainers
CONTRIBUTING.mdsays to branch fromdevelopment, but that branch is stale (last commit Jan 2026,5.2.2-rc.0) relative tomaster(actively merged into, most recently Jul 2026). This PR targetsmaster; happy to retarget if that's wrong.