Add jwt-bearer grant for ID-JAG (Identity Assertion Authorization Grant) support - #462
manmohan-shaw-okta wants to merge 3 commits into
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. |
|
Hey guys @manmohan-shaw-okta @BinoyOza-okta @dhensby I'm coming back at this one. TL;DR this is not a rejection of ID-JAG. I mainly want to ensure that supporting an emerging specification does not unintentionally commit the core project to unstable APIs or maintenance obligations that we have not explicitly agreed to or can support in terms of resources. Long version Personally I am very positive to support emerging standards and as you already realized I am rather concerned with the process itself and the governance questions that arise. My remaining concern is less about ID-JAG itself and more about what policy we want this project to have for specifications that are still under active IETF development. Once an implementation is merged here, maintenance of that implementation ultimately becomes the responsibility of this project, even if the original contributors later move on. One very specific example: The PR currently refers to draft -03, while the current RFC is already -04 and contains further changes. I think this illustrates the maintenance cost that comes with supporting a draft before it stabilizes. This would especially apply if draft inclusions would imply code being introduced to core (lib/model for example). I think proposing the drafts/ + extendedGrantTypes approach addresses an important part of my concern. I therefore see a useful distinction between the generic mechanisms in #453 and this PR. RFC 7523 is stable and broadly useful independently of ID-JAG, so generic JWT bearer support via #453 seems appropriate for merge first. library. ID-JAG could then build on the library as an explicitly experimental extension. Why we need a policy? I would be comfortable exploring either an opt-in drafts/ implementation that does not affect stable core APIs, or a separately maintained extension package. In either case I think we should document exactly which draft revision is implemented, make its experimental compatibility status explicit, and establish who will track changes to the draft until it either becomes an RFC or the implementation is retired. @dhensby This is, however, something I would like us to turn into a general project policy rather than decide specifically for ID-JAG. The same rules should apply to a draft proposed by another vendor, or an independent contributor. Separately, I think the JWT/JWS implementation deserves its own architectural/security discussion. Avoiding another dependency has benefits, but maintaining our own verification layer also creates a long-term security responsibility, especially if the generic RFC 7523 work gives us another implementation path. How to move forward I don't want to stall but keep this moving forward. However, for this I'd like to refocus on #453 and I'd like to ask you guys to support us in reviewing and testing it. While the governance side of things has to be discussed by us library maintainers (under involvement of the community of course), I think there is no problem at all to include everyone on the technical side of things. My personal premise: The better we can decouple extensions technically from the core code the more I am positive with their inclusion. However this decoupling needs to remain reasonable (avoiding over-engineering because this comes with its own complexities and maintenance costs). Additionally I would like to focus on merging the remaining PRs first and release another stable version. I know things move slowly but I think this is in the best interest for all of us. |
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.