Skip to content

Commit 2bb0b42

Browse files
dhensbyclaude
andcommitted
docs: document client authentication
Add a client authentication guide (built-in methods, JWT client assertions, per-client pinning, replay protection and writing a custom method), document the new optional model functions and `ClientData` fields, add guide and API sidebar entries, and regenerate the API reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 13b0f16 commit 2bb0b42

13 files changed

Lines changed: 734 additions & 10 deletions

docs/.vitepress/config.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default defineConfig({
2323
{text: 'Model', link: '/guide/model'},
2424
{text: 'Token types', link: '/guide/token-types'},
2525
{text: 'PKCE', link: '/guide/pkce'},
26+
{text: 'Client authentication', link: '/guide/client-authentication'},
2627
{text: 'Adapters', link: '/guide/adapters'},
2728
{text: 'Compliance', link: '/guide/compliance'},
2829
{text: 'Migrating to v5', link: '/guide/migrating-to-v5'},
@@ -70,6 +71,16 @@ export default defineConfig({
7071
{ text: 'Token Handler', link: '/api/handlers/token-handler' },
7172
]
7273
},
74+
{
75+
text: 'Client Authentication', items: [
76+
{ text: 'Abstract', link: '/api/client-authentication/abstract-client-authentication' },
77+
{ text: 'Abstract Client Secret', link: '/api/client-authentication/abstract-client-secret-authentication' },
78+
{ text: 'Client Secret Basic', link: '/api/client-authentication/client-secret-basic' },
79+
{ text: 'Client Secret Post', link: '/api/client-authentication/client-secret-post' },
80+
{ text: 'None (public client)', link: '/api/client-authentication/none' },
81+
{ text: 'JWT Bearer', link: '/api/client-authentication/jwt-bearer-client-authentication' },
82+
]
83+
},
7384
{
7485
text: 'Models', items: [
7586
{ text: 'Token Model', link: '/api/models/token-model' },
@@ -96,6 +107,7 @@ export default defineConfig({
96107
text: 'Utils', items: [
97108
{ text: 'Crypto', link: '/api/utils/crypto-util' },
98109
{ text: 'Date', link: '/api/utils/date-util' },
110+
{ text: 'JWS', link: '/api/utils/jws-util' },
99111
{ text: 'Scope', link: '/api/utils/scope-util' },
100112
{ text: 'String', link: '/api/utils/string-util' },
101113
{ text: 'Token', link: '/api/utils/token-util' },
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<a name="AbstractClientAuthentication"></a>
2+
3+
## *AbstractClientAuthentication*
4+
Port for a single client-authentication method — an OAuth
5+
`token_endpoint_auth_method` (e.g. `client_secret_basic`, `private_key_jwt`).
6+
7+
Concrete adapters are responsible only for (a) recognising their own
8+
credential shape on the incoming request and (b) verifying those
9+
credentials and resolving the client. *Selection* (which method applies,
10+
rejecting requests that present more than one) and *post-validation* (the
11+
client's `grants`) are owned by the orchestrator, not the adapter.
12+
13+
This is deliberately minimal so that new methods — mTLS
14+
(`tls_client_auth`), attestation, etc. — can be added without touching the
15+
token handler. The built-in methods are themselves implemented against this
16+
same port.
17+
18+
**Kind**: global abstract class
19+
20+
* *[AbstractClientAuthentication](#AbstractClientAuthentication)*
21+
* *[.requiresCredentials](#AbstractClientAuthentication+requiresCredentials) ⇒ <code>boolean</code>*
22+
* *[.presentedMethod(request)](#AbstractClientAuthentication+presentedMethod) ⇒ <code>string</code>*
23+
* *[.matches(request)](#AbstractClientAuthentication+matches) ⇒ <code>boolean</code>*
24+
* *[.authenticate(request, context)](#AbstractClientAuthentication+authenticate) ⇒ <code>Promise.&lt;Client&gt;</code>*
25+
26+
<a name="AbstractClientAuthentication+requiresCredentials"></a>
27+
28+
### *abstractClientAuthentication.requiresCredentials ⇒ <code>boolean</code>*
29+
Whether this method presents client *credentials* (`true`) or merely
30+
identifies a public client (`false`, e.g. the `none` method).
31+
32+
The orchestrator uses this — not the `method` identifier — to enforce
33+
that a request presents at most one credentialed mechanism and to decide
34+
when a public client is acceptable. A new credentialed method (e.g.
35+
`tls_client_auth`) therefore needs no changes elsewhere.
36+
37+
**Kind**: instance property of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)
38+
<a name="AbstractClientAuthentication+presentedMethod"></a>
39+
40+
### *abstractClientAuthentication.presentedMethod(request) ⇒ <code>string</code>*
41+
The OAuth `token_endpoint_auth_method` this request presents
42+
(e.g. `client_secret_basic`, `private_key_jwt`). For most methods this is
43+
a constant; for JWT client assertions it is derived from the assertion's
44+
algorithm. The orchestrator uses it to enforce a client's registered
45+
`tokenEndpointAuthMethod`, when the client declares one.
46+
47+
**Kind**: instance method of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)
48+
49+
| Param | Type |
50+
| --- | --- |
51+
| request | <code>Request</code> |
52+
53+
<a name="AbstractClientAuthentication+matches"></a>
54+
55+
### *abstractClientAuthentication.matches(request) ⇒ <code>boolean</code>*
56+
Does the request present credentials for this method?
57+
58+
MUST be a cheap, side-effect-free predicate: no model calls, no network,
59+
no throwing. The orchestrator calls this on every registered method to
60+
decide which one applies.
61+
62+
**Kind**: instance method of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)
63+
64+
| Param | Type | Description |
65+
| --- | --- | --- |
66+
| request | <code>Request</code> | the incoming token request |
67+
68+
<a name="AbstractClientAuthentication+authenticate"></a>
69+
70+
### *abstractClientAuthentication.authenticate(request, context) ⇒ <code>Promise.&lt;Client&gt;</code>*
71+
Verify the presented credentials and resolve the authenticated client.
72+
73+
Implementations MUST throw an `InvalidClientError` when authentication
74+
fails (or an `InvalidRequestError` for malformed input) and MUST NOT
75+
return a falsy client for a credential they accepted.
76+
77+
**Kind**: instance method of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)
78+
**Returns**: <code>Promise.&lt;Client&gt;</code> - the authenticated client
79+
80+
| Param | Type | Description |
81+
| --- | --- | --- |
82+
| request | <code>Request</code> | the incoming token request |
83+
| context | <code>object</code> | |
84+
| context.model | <code>Model</code> | the configured model |
85+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<a name="AbstractClientSecretAuthentication"></a>
2+
3+
## *AbstractClientSecretAuthentication ⇐ <code>AbstractClientAuthentication</code>*
4+
Shared behaviour for the secret-based methods (`client_secret_basic`,
5+
`client_secret_post`): validate the credential format, then delegate
6+
verification of the secret to `model.getClient(clientId, clientSecret)`.
7+
8+
Subclasses differ only in how the credentials are carried on the wire
9+
(`getCredentials`).
10+
11+
**Kind**: global abstract class
12+
**Extends**: <code>AbstractClientAuthentication</code>
13+
<a name="AbstractClientSecretAuthentication+getCredentials"></a>
14+
15+
### **abstractClientSecretAuthentication.getCredentials(request) ⇒ <code>Object</code>**
16+
Extract `{ clientId, clientSecret }` from the request for this transport.
17+
18+
**Kind**: instance abstract method of [<code>AbstractClientSecretAuthentication</code>](#AbstractClientSecretAuthentication)
19+
20+
| Param | Type |
21+
| --- | --- |
22+
| request | <code>Request</code> |
23+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<a name="ClientSecretBasic"></a>
2+
3+
## ClientSecretBasic ⇐ <code>AbstractClientSecretAuthentication</code>
4+
`client_secret_basic`: client credentials supplied via the HTTP Basic
5+
`Authorization` header.
6+
7+
**Kind**: global class
8+
**Extends**: <code>AbstractClientSecretAuthentication</code>
9+
**See**: https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<a name="ClientSecretPost"></a>
2+
3+
## ClientSecretPost ⇐ <code>AbstractClientSecretAuthentication</code>
4+
`client_secret_post`: `client_id` and `client_secret` supplied as
5+
`application/x-www-form-urlencoded` request-body parameters.
6+
7+
**Kind**: global class
8+
**Extends**: <code>AbstractClientSecretAuthentication</code>
9+
**See**: https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<a name="module_client-authentication"></a>
2+
3+
## client-authentication
4+
Pluggable client authentication for the token endpoint. Each
5+
`token_endpoint_auth_method` is an adapter against
6+
[AbstractClientAuthentication](AbstractClientAuthentication); this module owns *selection* (pick the
7+
one method that applies, reject requests presenting more than one) and the
8+
shared *post-validation* of the resolved client.
9+
10+
11+
* [client-authentication](#module_client-authentication)
12+
* [~defaultClientAuthenticationMethods()](#module_client-authentication..defaultClientAuthenticationMethods) ⇒ <code>Object.&lt;string, AbstractClientAuthentication&gt;</code>
13+
* [~authenticateClient(request, response, options)](#module_client-authentication..authenticateClient) ⇒ <code>Promise.&lt;Client&gt;</code>
14+
* [~selectMethod()](#module_client-authentication..selectMethod)
15+
16+
<a name="module_client-authentication..defaultClientAuthenticationMethods"></a>
17+
18+
### client-authentication~defaultClientAuthenticationMethods() ⇒ <code>Object.&lt;string, AbstractClientAuthentication&gt;</code>
19+
The client-authentication methods enabled by default. These reproduce the
20+
library's historical behaviour (HTTP Basic, request-body credentials, and
21+
public clients). JWT client assertions are intentionally NOT enabled by
22+
default — they require per-deployment configuration (the expected
23+
`audience`) — and are added via the `extendedClientAuthentication` option.
24+
25+
**Kind**: inner method of [<code>client-authentication</code>](#module_client-authentication)
26+
<a name="module_client-authentication..authenticateClient"></a>
27+
28+
### client-authentication~authenticateClient(request, response, options) ⇒ <code>Promise.&lt;Client&gt;</code>
29+
Select the single client-authentication method that applies to the request
30+
and use it to resolve and validate the authenticated client.
31+
32+
**Kind**: inner method of [<code>client-authentication</code>](#module_client-authentication)
33+
**Returns**: <code>Promise.&lt;Client&gt;</code> - the authenticated client
34+
35+
| Param | Type | Description |
36+
| --- | --- | --- |
37+
| request | <code>Request</code> | |
38+
| response | <code>Response</code> | |
39+
| options | <code>object</code> | |
40+
| options.model | <code>Model</code> | the configured model |
41+
| options.methods | <code>Object.&lt;string, AbstractClientAuthentication&gt;</code> | the enabled methods |
42+
| options.clientAuthenticationRequired | <code>boolean</code> | whether the grant requires client authentication |
43+
| options.isPKCE | <code>boolean</code> | whether this is a PKCE request (public clients are always permitted) |
44+
45+
<a name="module_client-authentication..selectMethod"></a>
46+
47+
### client-authentication~selectMethod()
48+
Decide which method authenticates this request.
49+
50+
Rejects requests that present more than one credential-bearing mechanism
51+
(RFC 6749 §2.3). When no credentials are presented, a public (`none`)
52+
client is accepted only for PKCE requests or grants that do not require
53+
client authentication.
54+
55+
**Kind**: inner method of [<code>client-authentication</code>](#module_client-authentication)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
## Classes
2+
3+
<dl>
4+
<dt><a href="#JwtBearerClientAuthentication">JwtBearerClientAuthentication</a> ⇐ <code>AbstractClientAuthentication</code></dt>
5+
<dd><p>JWT client assertion authentication — <code>client_assertion</code> +
6+
<code>client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer</code>.</p>
7+
<p>Covers both OIDC methods, distinguished by the JWS <code>alg</code> of the assertion:</p>
8+
<ul>
9+
<li><code>client_secret_jwt</code> — HMAC (<code>HS*</code>), keyed by the client secret;</li>
10+
<li><code>private_key_jwt</code> — asymmetric (<code>RS*</code>/<code>PS*</code>/<code>ES*</code>), verified against
11+
the client&#39;s registered public keys (a JWK Set).</li>
12+
</ul>
13+
<p>The library owns the <em>protocol</em> (parse → resolve client → verify → bind);
14+
key material and replay state come from the model/client. This method is
15+
opt-in (it requires per-deployment <code>audience</code> configuration); register it
16+
via the <code>extendedClientAuthentication</code> server option.</p>
17+
</dd>
18+
<dt><a href="#JwtBearerClientAuthentication">JwtBearerClientAuthentication</a></dt>
19+
<dd></dd>
20+
</dl>
21+
22+
## Constants
23+
24+
<dl>
25+
<dt><a href="#CLIENT_ASSERTION_TYPE">CLIENT_ASSERTION_TYPE</a></dt>
26+
<dd><p>The <code>client_assertion_type</code> value identifying a JWT client assertion.</p>
27+
</dd>
28+
</dl>
29+
30+
<a name="JwtBearerClientAuthentication"></a>
31+
32+
## JwtBearerClientAuthentication ⇐ <code>AbstractClientAuthentication</code>
33+
JWT client assertion authentication — `client_assertion` +
34+
`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`.
35+
36+
Covers both OIDC methods, distinguished by the JWS `alg` of the assertion:
37+
- `client_secret_jwt` — HMAC (`HS*`), keyed by the client secret;
38+
- `private_key_jwt` — asymmetric (`RS*`/`PS*`/`ES*`), verified against
39+
the client's registered public keys (a JWK Set).
40+
41+
The library owns the *protocol* (parse → resolve client → verify → bind);
42+
key material and replay state come from the model/client. This method is
43+
opt-in (it requires per-deployment `audience` configuration); register it
44+
via the `extendedClientAuthentication` server option.
45+
46+
**Kind**: global class
47+
**Extends**: <code>AbstractClientAuthentication</code>
48+
**See**
49+
50+
- https://datatracker.ietf.org/doc/html/rfc7521#section-4.2
51+
- https://datatracker.ietf.org/doc/html/rfc7523#section-2.2
52+
- https://datatracker.ietf.org/doc/html/rfc7523#section-3
53+
- https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
54+
55+
56+
* [JwtBearerClientAuthentication](#JwtBearerClientAuthentication) ⇐ <code>AbstractClientAuthentication</code>
57+
* [new JwtBearerClientAuthentication(options)](#new_JwtBearerClientAuthentication_new)
58+
* [.defaultGetKey()](#JwtBearerClientAuthentication+defaultGetKey)
59+
* [.assertNotReplayed()](#JwtBearerClientAuthentication+assertNotReplayed)
60+
61+
<a name="new_JwtBearerClientAuthentication_new"></a>
62+
63+
### new JwtBearerClientAuthentication(options)
64+
65+
| Param | Type | Description |
66+
| --- | --- | --- |
67+
| options | <code>object</code> | |
68+
| options.audience | <code>string</code> \| <code>Array.&lt;string&gt;</code> | the value(s) the assertion's `aud` claim must contain — typically this authorization server's token endpoint URL and/or its issuer identifier. REQUIRED. |
69+
| [options.maxTokenAge] | <code>number</code> | maximum assertion age in seconds, measured from `iat` (enabling this requires assertions to carry `iat`). |
70+
| [options.clockTolerance] | <code>number</code> | clock skew tolerance in seconds. |
71+
| [options.algorithms] | <code>Array.&lt;string&gt;</code> | override the accepted JWS algorithms. |
72+
| [options.getKey] | <code>function</code> | `(client, header) => key` override key resolution. By default HMAC keys derive from `client.secret` and asymmetric keys come from `client.jwks` (a JWK Set) or `client.jwksUri`. |
73+
74+
<a name="JwtBearerClientAuthentication+defaultGetKey"></a>
75+
76+
### jwtBearerClientAuthentication.defaultGetKey()
77+
Default key resolution: HMAC from the client secret, asymmetric from the
78+
client's registered JWK Set (inline `jwks` or remote `jwksUri`).
79+
80+
**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
81+
<a name="JwtBearerClientAuthentication+assertNotReplayed"></a>
82+
83+
### jwtBearerClientAuthentication.assertNotReplayed()
84+
Single-use replay protection. Opt-in: only enforced when the model
85+
implements the replay hooks. The identifier passed to the hooks is the
86+
assertion's `jti` when present, otherwise a fingerprint of its signing
87+
input — so replay protection applies even to assertions without a `jti`
88+
(OIDC Core §9 requires `jti`; RFC 7523 §3 makes it optional).
89+
90+
**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
91+
<a name="JwtBearerClientAuthentication"></a>
92+
93+
## JwtBearerClientAuthentication
94+
**Kind**: global class
95+
96+
* [JwtBearerClientAuthentication](#JwtBearerClientAuthentication)
97+
* [new JwtBearerClientAuthentication(options)](#new_JwtBearerClientAuthentication_new)
98+
* [.defaultGetKey()](#JwtBearerClientAuthentication+defaultGetKey)
99+
* [.assertNotReplayed()](#JwtBearerClientAuthentication+assertNotReplayed)
100+
101+
<a name="new_JwtBearerClientAuthentication_new"></a>
102+
103+
### new JwtBearerClientAuthentication(options)
104+
105+
| Param | Type | Description |
106+
| --- | --- | --- |
107+
| options | <code>object</code> | |
108+
| options.audience | <code>string</code> \| <code>Array.&lt;string&gt;</code> | the value(s) the assertion's `aud` claim must contain — typically this authorization server's token endpoint URL and/or its issuer identifier. REQUIRED. |
109+
| [options.maxTokenAge] | <code>number</code> | maximum assertion age in seconds, measured from `iat` (enabling this requires assertions to carry `iat`). |
110+
| [options.clockTolerance] | <code>number</code> | clock skew tolerance in seconds. |
111+
| [options.algorithms] | <code>Array.&lt;string&gt;</code> | override the accepted JWS algorithms. |
112+
| [options.getKey] | <code>function</code> | `(client, header) => key` override key resolution. By default HMAC keys derive from `client.secret` and asymmetric keys come from `client.jwks` (a JWK Set) or `client.jwksUri`. |
113+
114+
<a name="JwtBearerClientAuthentication+defaultGetKey"></a>
115+
116+
### jwtBearerClientAuthentication.defaultGetKey()
117+
Default key resolution: HMAC from the client secret, asymmetric from the
118+
client's registered JWK Set (inline `jwks` or remote `jwksUri`).
119+
120+
**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
121+
<a name="JwtBearerClientAuthentication+assertNotReplayed"></a>
122+
123+
### jwtBearerClientAuthentication.assertNotReplayed()
124+
Single-use replay protection. Opt-in: only enforced when the model
125+
implements the replay hooks. The identifier passed to the hooks is the
126+
assertion's `jti` when present, otherwise a fingerprint of its signing
127+
input — so replay protection applies even to assertions without a `jti`
128+
(OIDC Core §9 requires `jti`; RFC 7523 §3 makes it optional).
129+
130+
**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
131+
<a name="CLIENT_ASSERTION_TYPE"></a>
132+
133+
## CLIENT\_ASSERTION\_TYPE
134+
The `client_assertion_type` value identifying a JWT client assertion.
135+
136+
**Kind**: global constant
137+
**See**: https://datatracker.ietf.org/doc/html/rfc7523#section-2.2
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<a name="None"></a>
2+
3+
## None ⇐ <code>AbstractClientAuthentication</code>
4+
`none`: a public client that identifies itself with `client_id` only and
5+
presents no secret (e.g. a PKCE flow, or a grant for which
6+
`requireClientAuthentication` is disabled).
7+
8+
This adapter only resolves the client. Whether a secret-less client is
9+
*acceptable* for the request is a policy decision owned by the orchestrator
10+
(it knows the grant type, the `requireClientAuthentication` config and
11+
whether this is a PKCE request).
12+
13+
**Kind**: global class
14+
**Extends**: <code>AbstractClientAuthentication</code>
15+
**See**: https://datatracker.ietf.org/doc/html/rfc6749#section-2.1

0 commit comments

Comments
 (0)