Skip to content

Add jwt-bearer grant for ID-JAG (Identity Assertion Authorization Grant) support - #462

Open
manmohan-shaw-okta wants to merge 3 commits into
node-oauth:masterfrom
manmohan-shaw-okta:feat/id-jag-jwt-bearer-grant
Open

manmohan-shaw-okta wants to merge 3 commits into
node-oauth:masterfrom
manmohan-shaw-okta:feat/id-jag-jwt-bearer-grant

Conversation

@manmohan-shaw-okta

Copy link
Copy Markdown

Summary

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, 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.md new tight dependencies shouldn't be introduced without discussion, so signature verification (RS256/ES256/PS256) is implemented using only Node's built-in crypto module — 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-in crypto.
  • 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 (not extendedGrantTypes, 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 (or isJtiUsed+recordJti) — only required if this grant is used, following the same conditionally-required pattern as e.g. getRefreshToken for the refresh_token grant.
  • 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 to authorize/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: typ type-confusion, alg allow-list violations (none, HS256), audience mismatch, expired/future/clock-skew-boundary timestamps, tampered signatures, untrusted issuer, unresolvable key, client_id claim mismatch, replay (both validateJti() and isJtiUsed()/recordJti() model shapes, plus fail-closed on a throwing replay store), refresh-token suppression, and scope intersection/narrowing. npm run lint and npm test (528 passing) are clean.

OAuth2 standard

  • RFC 7523 — JWT Bearer grant.
  • ID-JAG draft (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).
  • RFC 8414 — aud is validated against this AS's issuer identifier.
  • RFC 6749 §5.2 — all failures map to existing OAuthError subclasses (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

const OAuth2Server = require('@node-oauth/oauth2-server');
const oauth = new OAuth2Server({ model, tokenEndpointUri: 'https://rs.example.com' });
// POST /token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<ID-JAG JWT>

See examples/express-id-jag-server.js for 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.md says to branch from development, but that branch is stale (last commit Jan 2026, 5.2.2-rc.0) relative to master (actively merged into, most recently Jul 2026). This PR targets master; happy to retarget if that's wrong.

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).
@jankapunkt

Copy link
Copy Markdown
Member

@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.

@jankapunkt

Copy link
Copy Markdown
Member

also, how far is this related to #453 ?

@manmohan-shaw-okta
manmohan-shaw-okta marked this pull request as ready for review July 20, 2026 02:53
@BinoyOza-okta

Copy link
Copy Markdown

@jankapunkt Thanks — both fair questions, and they have a shared answer.

Draft-spec status

Quick 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 (draft-ietf-oauth-identity-assertion-authz-grant-03), which layers a specific claim set on top. So the question is how to isolate the profile, not whether the underlying grant is stable.

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 grant lives in a purpose-built authlib/oauth2/drafts/ package, not in the stable RFC module
  • it's absent from the stable module's public exports
  • it inherits BaseGrant directly rather than the RFC 7523 grant class, so draft churn can't reach stable code
  • the docs page is named after the draft, not the RFC

The equivalent here would be:

  • grant moves to lib/grant-types/drafts/id-jag-grant-type.js
  • registered via extendedGrantTypes only — not in the built-in grantTypes map — so no existing deployment picks up draft behaviour implicitly
  • the 5 model hooks stay outside the required set (already conditionally required, same pattern as getRefreshToken for refresh_token)
  • docs page states the draft revision it implements and flags the profile as unstable

Important to highlight: this also removes the need for the new server options. Because extendedGrantTypes takes a class rather than an instance, config can be injected by the consumer:

extendedGrantTypes: {
  'urn:ietf:params:oauth:grant-type:jwt-bearer':
    IdJagGrantType.configure({ tokenEndpointUri: 'https://rs.example.com' }),
}

I've verified this end-to-end against master: the URN routes correctly (it clears the format gate via isFormat.uri), config reaches the grant, and core defaults still apply. So the 9 lines currently added to token-handler.js and the 4 in server.js can all come out — the PR becomes purely additive, with no existing source file modified.

Relationship to #453

With 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 private_key_jwt / client_secret_jwt alongside the grant, and has already been through a review round. It should own the generic RFC 7523 grant.

What this PR adds on top is the ID-JAG profile specifically:

  • typ pinned to exactly oauth-id-jag+jwt (§3.1); the generic JWT value rejected as type confusion
  • aud validated against the AS's own RFC 8414 issuer identifier rather than the token endpoint URL
  • scope narrowed to the intersection of the assertion's scope claim and the request (§4.4.1)
  • refresh token never issued (§4.4.3 — stricter than the draft's SHOULD NOT)
  • confidential clients only (§8.1)
  • replay keyed on (iss, jti), since jti uniqueness is per-issuer

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 — extendedGrantTypes overrides built-ins via Object.assign, so precedence is already well-defined. We documented the same constraint in authlib.

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 NotImplementedError, which is what prompted the decoupling commit. Keeping them independent also means the JWT plumbing here stays self-contained — signature verification is ~155 lines on Node's built-in crypto, no new dependency and no engine change, per the CONTRIBUTING.md note on new tight dependencies.

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.

@jankapunkt

jankapunkt commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

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.

This branch has not been deployed

No deployments
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.

3 participants