Skip to content

Commit 1e506f2

Browse files
dhensbyclaude
andcommitted
docs: document the JWT bearer authorization grant
Add a grant-types guide section and model-function notes for the JWT bearer authorization grant, and regenerate the API reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e15dc25 commit 1e506f2

5 files changed

Lines changed: 265 additions & 0 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default defineConfig({
6262
{ text: 'Client Credentials', link: '/api/grant-types/client-credentials-grant-type' },
6363
{ text: 'Password', link: '/api/grant-types/password-grant-type' },
6464
{ text: 'Refresh Token', link: '/api/grant-types/refresh-token-grant-type' },
65+
{ text: 'JWT Bearer', link: '/api/grant-types/jwt-bearer-grant-type' },
6566
]
6667
},
6768
{
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
## Classes
2+
3+
<dl>
4+
<dt><a href="#JwtBearerGrantType">JwtBearerGrantType</a> ⇐ <code>AbstractGrantType</code></dt>
5+
<dd><p>The JWT bearer authorization grant (RFC 7521 §4.1, RFC 7523 §2.1/§3): a signed
6+
JWT <em>is</em> the authorization grant. The assertion&#39;s <code>iss</code> identifies a trusted
7+
issuer (whose key verifies the assertion) and <code>sub</code> identifies the principal
8+
the access token is issued for. Unlike JWT <em>client authentication</em>, <code>sub</code> is
9+
the user/principal — not the client — and <code>iss</code>/<code>sub</code> are not bound to the
10+
client id.</p>
11+
<p>This is an extension grant; register it via <code>extendedGrantTypes</code>. The
12+
requested <code>scope</code> is taken from the body parameter (RFC 7521 §4.1), and no
13+
refresh token is issued (RFC 7521 §5.2).</p>
14+
<p>The model must implement:</p>
15+
<ul>
16+
<li><code>getJWTBearerIssuer(issuer)</code> → <code>{ audience, jwks | jwksUri | secret }</code> (or
17+
falsy for an untrusted issuer) — the verification key material and the
18+
expected <code>aud</code>.</li>
19+
<li><code>getJWTBearerUser({ issuer, subject, client, scope, jti, assertionId, exp })</code> → the
20+
authorized user (or falsy to deny). Replay (<code>jti</code>) can be enforced here.</li>
21+
</ul>
22+
</dd>
23+
</dl>
24+
25+
## Constants
26+
27+
<dl>
28+
<dt><a href="#GRANT_TYPE">GRANT_TYPE</a></dt>
29+
<dd><p>The <code>grant_type</code> value for the JWT bearer authorization grant.</p>
30+
</dd>
31+
</dl>
32+
33+
<a name="JwtBearerGrantType"></a>
34+
35+
## JwtBearerGrantType ⇐ <code>AbstractGrantType</code>
36+
The JWT bearer authorization grant (RFC 7521 §4.1, RFC 7523 §2.1/§3): a signed
37+
JWT *is* the authorization grant. The assertion's `iss` identifies a trusted
38+
issuer (whose key verifies the assertion) and `sub` identifies the principal
39+
the access token is issued for. Unlike JWT *client authentication*, `sub` is
40+
the user/principal — not the client — and `iss`/`sub` are not bound to the
41+
client id.
42+
43+
This is an extension grant; register it via `extendedGrantTypes`. The
44+
requested `scope` is taken from the body parameter (RFC 7521 §4.1), and no
45+
refresh token is issued (RFC 7521 §5.2).
46+
47+
The model must implement:
48+
- `getJWTBearerIssuer(issuer)``{ audience, jwks | jwksUri | secret }` (or
49+
falsy for an untrusted issuer) — the verification key material and the
50+
expected `aud`.
51+
- `getJWTBearerUser({ issuer, subject, client, scope, jti, assertionId, exp })` → the
52+
authorized user (or falsy to deny). Replay (`jti`) can be enforced here.
53+
54+
**Kind**: global class
55+
**Extends**: <code>AbstractGrantType</code>
56+
**See**
57+
58+
- https://datatracker.ietf.org/doc/html/rfc7521#section-4.1
59+
- https://datatracker.ietf.org/doc/html/rfc7523#section-2.1
60+
- https://datatracker.ietf.org/doc/html/rfc7523#section-3
61+
62+
63+
* [JwtBearerGrantType](#JwtBearerGrantType) ⇐ <code>AbstractGrantType</code>
64+
* [new JwtBearerGrantType()](#new_JwtBearerGrantType_new)
65+
* [.handle(request, client)](#JwtBearerGrantType+handle)
66+
* [.verifyAssertion()](#JwtBearerGrantType+verifyAssertion)
67+
* [.getKey()](#JwtBearerGrantType+getKey)
68+
* [.getUser()](#JwtBearerGrantType+getUser)
69+
* [.saveToken()](#JwtBearerGrantType+saveToken)
70+
71+
<a name="new_JwtBearerGrantType_new"></a>
72+
73+
### new JwtBearerGrantType()
74+
**Example**
75+
```js
76+
new OAuth2Server({
77+
model,
78+
extendedGrantTypes: {
79+
'urn:ietf:params:oauth:grant-type:jwt-bearer': JwtBearerGrantType
80+
},
81+
// typically a public requester; identify it with `client_id`:
82+
requireClientAuthentication: { 'urn:ietf:params:oauth:grant-type:jwt-bearer': false }
83+
});
84+
```
85+
<a name="JwtBearerGrantType+handle"></a>
86+
87+
### jwtBearerGrantType.handle(request, client)
88+
Handle the JWT bearer grant.
89+
90+
**Kind**: instance method of [<code>JwtBearerGrantType</code>](#JwtBearerGrantType)
91+
**See**: https://datatracker.ietf.org/doc/html/rfc7523#section-2.1
92+
93+
| Param | Type |
94+
| --- | --- |
95+
| request | <code>Request</code> |
96+
| client | <code>ClientData</code> |
97+
98+
<a name="JwtBearerGrantType+verifyAssertion"></a>
99+
100+
### jwtBearerGrantType.verifyAssertion()
101+
Verify the `assertion` and return its (trusted) claims.
102+
103+
**Kind**: instance method of [<code>JwtBearerGrantType</code>](#JwtBearerGrantType)
104+
<a name="JwtBearerGrantType+getKey"></a>
105+
106+
### jwtBearerGrantType.getKey()
107+
Resolve the verification key for the issuer (HMAC secret or asymmetric JWKS).
108+
109+
**Kind**: instance method of [<code>JwtBearerGrantType</code>](#JwtBearerGrantType)
110+
<a name="JwtBearerGrantType+getUser"></a>
111+
112+
### jwtBearerGrantType.getUser()
113+
Resolve and authorize the principal (`sub`) the token is issued for.
114+
115+
**Kind**: instance method of [<code>JwtBearerGrantType</code>](#JwtBearerGrantType)
116+
<a name="JwtBearerGrantType+saveToken"></a>
117+
118+
### jwtBearerGrantType.saveToken()
119+
Save and return the access token. No refresh token is issued (RFC 7521 §5.2).
120+
121+
**Kind**: instance method of [<code>JwtBearerGrantType</code>](#JwtBearerGrantType)
122+
<a name="GRANT_TYPE"></a>
123+
124+
## GRANT\_TYPE
125+
The `grant_type` value for the JWT bearer authorization grant.
126+
127+
**Kind**: global constant
128+
**See**: https://datatracker.ietf.org/doc/html/rfc7523#section-2.1

docs/api/model.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ as well as generators, are supported.
6262
* [.validateRedirectUri(redirectUri, client)](#Model+validateRedirectUri) ⇒ <code>Promise.&lt;boolean&gt;</code>
6363
* [.isClientAssertionJtiUsed(jti)](#Model+isClientAssertionJtiUsed) ⇒ <code>Promise.&lt;boolean&gt;</code>
6464
* [.saveClientAssertionJti(jti, exp)](#Model+saveClientAssertionJti) ⇒ <code>Promise.&lt;void&gt;</code>
65+
* [.getJWTBearerIssuer(issuer)](#Model+getJWTBearerIssuer) ⇒ <code>Promise.&lt;object&gt;</code>
66+
* [.getJWTBearerUser(params)](#Model+getJWTBearerUser) ⇒ <code>Promise.&lt;object&gt;</code>
6567
* _static_
6668
* [.from(impl)](#Model.from)[<code>Model</code>](#Model)
6769

@@ -648,6 +650,41 @@ both this and `isClientAssertionJtiUsed` are implemented.
648650
| jti | <code>string</code> | The `jti` claim of the client assertion. |
649651
| exp | <code>number</code> | The assertion's `exp` (epoch seconds), for use as a TTL. |
650652

653+
<a name="Model+getJWTBearerIssuer"></a>
654+
655+
### model.getJWTBearerIssuer(issuer) ⇒ <code>Promise.&lt;object&gt;</code>
656+
Invoked to resolve a trusted issuer for the JWT bearer authorization grant.
657+
**Required** when the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant is enabled.
658+
659+
**Invoked during:**
660+
- JWT bearer authorization grant (`JwtBearerGrantType`)
661+
662+
**Kind**: instance method of [<code>Model</code>](#Model)
663+
**Returns**: <code>Promise.&lt;object&gt;</code> - Resolves to the issuer's verification key material and the
664+
expected audience: `{ audience, jwks | jwksUri | secret }`. Resolve to a falsy value
665+
to reject the issuer as untrusted (`invalid_grant`).
666+
667+
| Param | Type | Description |
668+
| --- | --- | --- |
669+
| issuer | <code>string</code> | The assertion's `iss` claim. |
670+
671+
<a name="Model+getJWTBearerUser"></a>
672+
673+
### model.getJWTBearerUser(params) ⇒ <code>Promise.&lt;object&gt;</code>
674+
Invoked to resolve and authorize the principal a JWT bearer assertion is issued for.
675+
**Required** when the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant is enabled.
676+
677+
**Invoked during:**
678+
- JWT bearer authorization grant (`JwtBearerGrantType`)
679+
680+
**Kind**: instance method of [<code>Model</code>](#Model)
681+
**Returns**: <code>Promise.&lt;object&gt;</code> - Resolves to the authorized user, or a falsy value to deny the
682+
grant (`invalid_grant`). Single-use (`jti`) replay protection can be enforced here.
683+
684+
| Param | Type | Description |
685+
| --- | --- | --- |
686+
| params | <code>object</code> | `{ issuer, subject, client, scope, jti, assertionId, exp }` from the verified assertion. `assertionId` is the `jti`, or a fingerprint of the assertion's signing input when it has no `jti` — use it for single-use replay protection. |
687+
651688
<a name="Model.from"></a>
652689

653690
### Model.from(impl) ⇒ [<code>Model</code>](#Model)

docs/guide/grant-types.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,95 @@ The client can request an access token using only its client credentials (or oth
5151
when requesting access to the protected resources under its control.
5252
The client credentials grant type **must** only be used by confidential clients.
5353

54+
## JWT Bearer Grant Type
55+
56+
**Defined in:** [RFC 7523 §2.1](https://datatracker.ietf.org/doc/html/rfc7523#section-2.1) (a profile of the [assertion framework, RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521)).
57+
58+
A signed JWT *is* the authorization grant: the assertion's `iss` identifies a trusted issuer
59+
and `sub` the principal the access token is issued for. This is the flow used for service-account
60+
and trusted-IdP token exchange. It ships as an opt-in extension grant, `JwtBearerGrantType`,
61+
registered through `extendedGrantTypes`:
62+
63+
``` js
64+
const OAuth2Server = require('@node-oauth/oauth2-server');
65+
const { JwtBearerGrantType } = OAuth2Server;
66+
67+
const server = new OAuth2Server({
68+
model,
69+
extendedGrantTypes: {
70+
'urn:ietf:params:oauth:grant-type:jwt-bearer': JwtBearerGrantType
71+
},
72+
// the requester MUST be identified by a registered `client_id` (see note below):
73+
requireClientAuthentication: { 'urn:ietf:params:oauth:grant-type:jwt-bearer': false }
74+
});
75+
```
76+
77+
The client sends the assertion as a normal token request; `scope` is a body parameter (it is not
78+
read from the assertion):
79+
80+
```
81+
POST /oauth/token
82+
Content-Type: application/x-www-form-urlencoded
83+
84+
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
85+
&assertion=eyJhbGciOiJSUzI1Ni...
86+
&client_id=client-1
87+
&scope=read
88+
```
89+
90+
**Model requirements:** in addition to `getClient` and `saveToken`, the model must implement:
91+
92+
- `getJWTBearerIssuer(issuer)` — resolve a trusted issuer's verification key material and expected
93+
audience (`{ audience, jwks | jwksUri | secret }`); return a falsy value to reject an untrusted issuer.
94+
- `getJWTBearerUser({ issuer, subject, client, scope, jti, assertionId, exp })` — resolve and
95+
authorize the principal the token is for; return a falsy value to deny. Enforce single-use replay
96+
protection here if required, keyed on `assertionId` (the `jti`, or a signing-input fingerprint when
97+
the assertion has no `jti`).
98+
99+
``` js
100+
const model = {
101+
async getJWTBearerIssuer (issuer) {
102+
const trusted = await db.findTrustedIssuer(issuer)
103+
if (!trusted) return false
104+
return { audience: 'https://as.example.com/oauth/token', jwks: trusted.jwks }
105+
},
106+
async getJWTBearerUser ({ issuer, subject, assertionId, exp }) {
107+
// MUST verify this issuer is permitted to assert this subject, otherwise any
108+
// trusted issuer could obtain a token for any user:
109+
if (!await db.issuerMayAssert(issuer, subject)) return false
110+
// Enforce single-use to prevent replay. `assertionId` is the `jti`, or a
111+
// signing-input fingerprint when the assertion carries no `jti`:
112+
if (await db.assertionIdUsed(assertionId)) return false
113+
await db.saveAssertionId(assertionId, exp)
114+
return db.findUser(subject)
115+
}
116+
}
117+
```
118+
119+
The assertion is verified per [RFC 7523 §3](https://datatracker.ietf.org/doc/html/rfc7523#section-3)
120+
(signature against the issuer's key, required `iss`/`sub`/`aud`/`exp`, audience and algorithm
121+
checks); a failed assertion is rejected with `invalid_grant`. No refresh token is issued
122+
([RFC 7521 §5.2](https://datatracker.ietf.org/doc/html/rfc7521#section-5.2)).
123+
124+
> [!WARNING]
125+
> **Replay:** the library does not track used assertions itself. Unless `getJWTBearerUser` enforces
126+
> single-use on `assertionId` (and/or you keep `exp` short), a captured assertion is replayable
127+
> until it expires — and each replay yields a fresh access token. `assertionId` is stable whether or
128+
> not the assertion carries a `jti`, so track it and prefer short-lived assertions.
129+
130+
> [!WARNING]
131+
> **Issuer/subject authorization:** `getJWTBearerUser` MUST verify that the `issuer` is permitted to
132+
> assert the given `subject`. If it merely resolves the subject to a user, any trusted issuer can
133+
> obtain a token for any user.
134+
135+
> [!NOTE]
136+
> This grant requires a registered `client_id` (resolved via `getClient`); it does not support the
137+
> assertion-only request (no `client_id`) used by some providers, because the client is resolved
138+
> before the grant runs. It asserts *who is authorized* and is distinct from
139+
> [JWT client authentication](./client-authentication.md#jwt-client-authentication), where the JWT
140+
> proves the *client's own* identity (`iss == sub == client_id`). The two are separable and may be
141+
> combined.
142+
54143
## Extension Grants
55144

56145
**Defined in:** [Section 4.5 of RFC 6749](https://www.rfc-editor.org/rfc/rfc6749#section-4.4).

docs/guide/model.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ Model functions used by the [password grant](grant-types.md#password-grant-type)
103103
- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode)
104104
- [validateScope](../api/model.md#modelvalidatescopeuser-client-scope--codepromisebooleancode)
105105

106+
### JWT Bearer Grant
107+
108+
Model functions used by the [JWT bearer authorization grant](grant-types.md#jwt-bearer-grant-type)
109+
(`JwtBearerGrantType`, registered via `extendedGrantTypes`):
110+
111+
- [getClient](../api/model.md#modelgetclientclientid-clientsecret--code-promiseclientdata-code)
112+
- [saveToken](../api/model.md#modelsavetokentoken-client-user--codepromiseobjectcode)
113+
- `getJWTBearerIssuer` — resolve a trusted issuer's verification keys and expected audience
114+
- `getJWTBearerUser` — resolve and authorize the assertion subject (and optionally enforce `jti` single-use)
115+
106116
### Extension Grants
107117

108118
The authorization server may also implement custom grant types to issue access (and optionally refresh) tokens.

0 commit comments

Comments
 (0)