Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default defineConfig({
{text: 'Model', link: '/guide/model'},
{text: 'Token types', link: '/guide/token-types'},
{text: 'PKCE', link: '/guide/pkce'},
{text: 'Client authentication', link: '/guide/client-authentication'},
{text: 'Adapters', link: '/guide/adapters'},
{text: 'Compliance', link: '/guide/compliance'},
{text: 'Migrating to v5', link: '/guide/migrating-to-v5'},
Expand Down Expand Up @@ -61,6 +62,7 @@ export default defineConfig({
{ text: 'Client Credentials', link: '/api/grant-types/client-credentials-grant-type' },
{ text: 'Password', link: '/api/grant-types/password-grant-type' },
{ text: 'Refresh Token', link: '/api/grant-types/refresh-token-grant-type' },
{ text: 'JWT Bearer', link: '/api/grant-types/jwt-bearer-grant-type' },
]
},
{
Expand All @@ -70,6 +72,16 @@ export default defineConfig({
{ text: 'Token Handler', link: '/api/handlers/token-handler' },
]
},
{
text: 'Client Authentication', items: [
{ text: 'Abstract', link: '/api/client-authentication/abstract-client-authentication' },
{ text: 'Abstract Client Secret', link: '/api/client-authentication/abstract-client-secret-authentication' },
{ text: 'Client Secret Basic', link: '/api/client-authentication/client-secret-basic' },
{ text: 'Client Secret Post', link: '/api/client-authentication/client-secret-post' },
{ text: 'None (public client)', link: '/api/client-authentication/none' },
{ text: 'JWT Bearer', link: '/api/client-authentication/jwt-bearer-client-authentication' },
]
},
{
text: 'Models', items: [
{ text: 'Token Model', link: '/api/models/token-model' },
Expand All @@ -96,6 +108,7 @@ export default defineConfig({
text: 'Utils', items: [
{ text: 'Crypto', link: '/api/utils/crypto-util' },
{ text: 'Date', link: '/api/utils/date-util' },
{ text: 'JWS', link: '/api/utils/jws-util' },
{ text: 'Scope', link: '/api/utils/scope-util' },
{ text: 'String', link: '/api/utils/string-util' },
{ text: 'Token', link: '/api/utils/token-util' },
Expand Down
85 changes: 85 additions & 0 deletions docs/api/client-authentication/abstract-client-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<a name="AbstractClientAuthentication"></a>

## *AbstractClientAuthentication*
Port for a single client-authentication method — an OAuth
`token_endpoint_auth_method` (e.g. `client_secret_basic`, `private_key_jwt`).

Concrete adapters are responsible only for (a) recognising their own
credential shape on the incoming request and (b) verifying those
credentials and resolving the client. *Selection* (which method applies,
rejecting requests that present more than one) and *post-validation* (the
client's `grants`) are owned by the orchestrator, not the adapter.

This is deliberately minimal so that new methods — mTLS
(`tls_client_auth`), attestation, etc. — can be added without touching the
token handler. The built-in methods are themselves implemented against this
same port.

**Kind**: global abstract class

* *[AbstractClientAuthentication](#AbstractClientAuthentication)*
* *[.requiresCredentials](#AbstractClientAuthentication+requiresCredentials) ⇒ <code>boolean</code>*
* *[.presentedMethod(request)](#AbstractClientAuthentication+presentedMethod) ⇒ <code>string</code>*
* *[.matches(request)](#AbstractClientAuthentication+matches) ⇒ <code>boolean</code>*
* *[.authenticate(request, context)](#AbstractClientAuthentication+authenticate) ⇒ <code>Promise.&lt;Client&gt;</code>*

<a name="AbstractClientAuthentication+requiresCredentials"></a>

### *abstractClientAuthentication.requiresCredentials ⇒ <code>boolean</code>*
Whether this method presents client *credentials* (`true`) or merely
identifies a public client (`false`, e.g. the `none` method).

The orchestrator uses this — not the `method` identifier — to enforce
that a request presents at most one credentialed mechanism and to decide
when a public client is acceptable. A new credentialed method (e.g.
`tls_client_auth`) therefore needs no changes elsewhere.

**Kind**: instance property of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)
<a name="AbstractClientAuthentication+presentedMethod"></a>

### *abstractClientAuthentication.presentedMethod(request) ⇒ <code>string</code>*
The OAuth `token_endpoint_auth_method` this request presents
(e.g. `client_secret_basic`, `private_key_jwt`). For most methods this is
a constant; for JWT client assertions it is derived from the assertion's
algorithm. The orchestrator uses it to enforce a client's registered
`tokenEndpointAuthMethod`, when the client declares one.

**Kind**: instance method of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)

| Param | Type |
| --- | --- |
| request | <code>Request</code> |

<a name="AbstractClientAuthentication+matches"></a>

### *abstractClientAuthentication.matches(request) ⇒ <code>boolean</code>*
Does the request present credentials for this method?

MUST be a cheap, side-effect-free predicate: no model calls, no network,
no throwing. The orchestrator calls this on every registered method to
decide which one applies.

**Kind**: instance method of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)

| Param | Type | Description |
| --- | --- | --- |
| request | <code>Request</code> | the incoming token request |

<a name="AbstractClientAuthentication+authenticate"></a>

### *abstractClientAuthentication.authenticate(request, context) ⇒ <code>Promise.&lt;Client&gt;</code>*
Verify the presented credentials and resolve the authenticated client.

Implementations MUST throw an `InvalidClientError` when authentication
fails (or an `InvalidRequestError` for malformed input) and MUST NOT
return a falsy client for a credential they accepted.

**Kind**: instance method of [<code>AbstractClientAuthentication</code>](#AbstractClientAuthentication)
**Returns**: <code>Promise.&lt;Client&gt;</code> - the authenticated client

| Param | Type | Description |
| --- | --- | --- |
| request | <code>Request</code> | the incoming token request |
| context | <code>object</code> | |
| context.model | <code>Model</code> | the configured model |

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<a name="AbstractClientSecretAuthentication"></a>

## *AbstractClientSecretAuthentication ⇐ <code>AbstractClientAuthentication</code>*
Shared behaviour for the secret-based methods (`client_secret_basic`,
`client_secret_post`): validate the credential format, then delegate
verification of the secret to `model.getClient(clientId, clientSecret)`.

Subclasses differ only in how the credentials are carried on the wire
(`getCredentials`).

**Kind**: global abstract class
**Extends**: <code>AbstractClientAuthentication</code>
<a name="AbstractClientSecretAuthentication+getCredentials"></a>

### **abstractClientSecretAuthentication.getCredentials(request) ⇒ <code>Object</code>**
Extract `{ clientId, clientSecret }` from the request for this transport.

**Kind**: instance abstract method of [<code>AbstractClientSecretAuthentication</code>](#AbstractClientSecretAuthentication)

| Param | Type |
| --- | --- |
| request | <code>Request</code> |

9 changes: 9 additions & 0 deletions docs/api/client-authentication/client-secret-basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<a name="ClientSecretBasic"></a>

## ClientSecretBasic ⇐ <code>AbstractClientSecretAuthentication</code>
`client_secret_basic`: client credentials supplied via the HTTP Basic
`Authorization` header.

**Kind**: global class
**Extends**: <code>AbstractClientSecretAuthentication</code>
**See**: https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
9 changes: 9 additions & 0 deletions docs/api/client-authentication/client-secret-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<a name="ClientSecretPost"></a>

## ClientSecretPost ⇐ <code>AbstractClientSecretAuthentication</code>
`client_secret_post`: `client_id` and `client_secret` supplied as
`application/x-www-form-urlencoded` request-body parameters.

**Kind**: global class
**Extends**: <code>AbstractClientSecretAuthentication</code>
**See**: https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
55 changes: 55 additions & 0 deletions docs/api/client-authentication/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<a name="module_client-authentication"></a>

## client-authentication
Pluggable client authentication for the token endpoint. Each
`token_endpoint_auth_method` is an adapter against
[AbstractClientAuthentication](AbstractClientAuthentication); this module owns *selection* (pick the
one method that applies, reject requests presenting more than one) and the
shared *post-validation* of the resolved client.


* [client-authentication](#module_client-authentication)
* [~defaultClientAuthenticationMethods()](#module_client-authentication..defaultClientAuthenticationMethods) ⇒ <code>Object.&lt;string, AbstractClientAuthentication&gt;</code>
* [~authenticateClient(request, response, options)](#module_client-authentication..authenticateClient) ⇒ <code>Promise.&lt;Client&gt;</code>
* [~selectMethod()](#module_client-authentication..selectMethod)

<a name="module_client-authentication..defaultClientAuthenticationMethods"></a>

### client-authentication~defaultClientAuthenticationMethods() ⇒ <code>Object.&lt;string, AbstractClientAuthentication&gt;</code>
The client-authentication methods enabled by default. These reproduce the
library's historical behaviour (HTTP Basic, request-body credentials, and
public clients). JWT client assertions are intentionally NOT enabled by
default — they require per-deployment configuration (the expected
`audience`) — and are added via the `extendedClientAuthentication` option.

**Kind**: inner method of [<code>client-authentication</code>](#module_client-authentication)
<a name="module_client-authentication..authenticateClient"></a>

### client-authentication~authenticateClient(request, response, options) ⇒ <code>Promise.&lt;Client&gt;</code>
Select the single client-authentication method that applies to the request
and use it to resolve and validate the authenticated client.

**Kind**: inner method of [<code>client-authentication</code>](#module_client-authentication)
**Returns**: <code>Promise.&lt;Client&gt;</code> - the authenticated client

| Param | Type | Description |
| --- | --- | --- |
| request | <code>Request</code> | |
| response | <code>Response</code> | |
| options | <code>object</code> | |
| options.model | <code>Model</code> | the configured model |
| options.methods | <code>Object.&lt;string, AbstractClientAuthentication&gt;</code> | the enabled methods |
| options.clientAuthenticationRequired | <code>boolean</code> | whether the grant requires client authentication |
| options.isPKCE | <code>boolean</code> | whether this is a PKCE request (public clients are always permitted) |

<a name="module_client-authentication..selectMethod"></a>

### client-authentication~selectMethod()
Decide which method authenticates this request.

Rejects requests that present more than one credential-bearing mechanism
(RFC 6749 §2.3). When no credentials are presented, a public (`none`)
client is accepted only for PKCE requests or grants that do not require
client authentication.

**Kind**: inner method of [<code>client-authentication</code>](#module_client-authentication)
137 changes: 137 additions & 0 deletions docs/api/client-authentication/jwt-bearer-client-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
## Classes

<dl>
<dt><a href="#JwtBearerClientAuthentication">JwtBearerClientAuthentication</a> ⇐ <code>AbstractClientAuthentication</code></dt>
<dd><p>JWT client assertion authentication — <code>client_assertion</code> +
<code>client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer</code>.</p>
<p>Covers both OIDC methods, distinguished by the JWS <code>alg</code> of the assertion:</p>
<ul>
<li><code>client_secret_jwt</code> — HMAC (<code>HS*</code>), keyed by the client secret;</li>
<li><code>private_key_jwt</code> — asymmetric (<code>RS*</code>/<code>PS*</code>/<code>ES*</code>), verified against
the client&#39;s registered public keys (a JWK Set).</li>
</ul>
<p>The library owns the <em>protocol</em> (parse → resolve client → verify → bind);
key material and replay state come from the model/client. This method is
opt-in (it requires per-deployment <code>audience</code> configuration); register it
via the <code>extendedClientAuthentication</code> server option.</p>
</dd>
<dt><a href="#JwtBearerClientAuthentication">JwtBearerClientAuthentication</a></dt>
<dd></dd>
</dl>

## Constants

<dl>
<dt><a href="#CLIENT_ASSERTION_TYPE">CLIENT_ASSERTION_TYPE</a></dt>
<dd><p>The <code>client_assertion_type</code> value identifying a JWT client assertion.</p>
</dd>
</dl>

<a name="JwtBearerClientAuthentication"></a>

## JwtBearerClientAuthentication ⇐ <code>AbstractClientAuthentication</code>
JWT client assertion authentication — `client_assertion` +
`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`.

Covers both OIDC methods, distinguished by the JWS `alg` of the assertion:
- `client_secret_jwt` — HMAC (`HS*`), keyed by the client secret;
- `private_key_jwt` — asymmetric (`RS*`/`PS*`/`ES*`), verified against
the client's registered public keys (a JWK Set).

The library owns the *protocol* (parse → resolve client → verify → bind);
key material and replay state come from the model/client. This method is
opt-in (it requires per-deployment `audience` configuration); register it
via the `extendedClientAuthentication` server option.

**Kind**: global class
**Extends**: <code>AbstractClientAuthentication</code>
**See**

- https://datatracker.ietf.org/doc/html/rfc7521#section-4.2
- https://datatracker.ietf.org/doc/html/rfc7523#section-2.2
- https://datatracker.ietf.org/doc/html/rfc7523#section-3
- https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication


* [JwtBearerClientAuthentication](#JwtBearerClientAuthentication) ⇐ <code>AbstractClientAuthentication</code>
* [new JwtBearerClientAuthentication(options)](#new_JwtBearerClientAuthentication_new)
* [.defaultGetKey()](#JwtBearerClientAuthentication+defaultGetKey)
* [.assertNotReplayed()](#JwtBearerClientAuthentication+assertNotReplayed)

<a name="new_JwtBearerClientAuthentication_new"></a>

### new JwtBearerClientAuthentication(options)

| Param | Type | Description |
| --- | --- | --- |
| options | <code>object</code> | |
| 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. |
| [options.maxTokenAge] | <code>number</code> | maximum assertion age in seconds, measured from `iat` (enabling this requires assertions to carry `iat`). |
| [options.clockTolerance] | <code>number</code> | clock skew tolerance in seconds. |
| [options.algorithms] | <code>Array.&lt;string&gt;</code> | override the accepted JWS algorithms. |
| [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`. |

<a name="JwtBearerClientAuthentication+defaultGetKey"></a>

### jwtBearerClientAuthentication.defaultGetKey()
Default key resolution: HMAC from the client secret, asymmetric from the
client's registered JWK Set (inline `jwks` or remote `jwksUri`).

**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
<a name="JwtBearerClientAuthentication+assertNotReplayed"></a>

### jwtBearerClientAuthentication.assertNotReplayed()
Single-use replay protection. Opt-in: only enforced when the model
implements the replay hooks. The identifier passed to the hooks is the
assertion's `jti` when present, otherwise a fingerprint of its signing
input — so replay protection applies even to assertions without a `jti`
(OIDC Core §9 requires `jti`; RFC 7523 §3 makes it optional).

**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
<a name="JwtBearerClientAuthentication"></a>

## JwtBearerClientAuthentication
**Kind**: global class

* [JwtBearerClientAuthentication](#JwtBearerClientAuthentication)
* [new JwtBearerClientAuthentication(options)](#new_JwtBearerClientAuthentication_new)
* [.defaultGetKey()](#JwtBearerClientAuthentication+defaultGetKey)
* [.assertNotReplayed()](#JwtBearerClientAuthentication+assertNotReplayed)

<a name="new_JwtBearerClientAuthentication_new"></a>

### new JwtBearerClientAuthentication(options)

| Param | Type | Description |
| --- | --- | --- |
| options | <code>object</code> | |
| 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. |
| [options.maxTokenAge] | <code>number</code> | maximum assertion age in seconds, measured from `iat` (enabling this requires assertions to carry `iat`). |
| [options.clockTolerance] | <code>number</code> | clock skew tolerance in seconds. |
| [options.algorithms] | <code>Array.&lt;string&gt;</code> | override the accepted JWS algorithms. |
| [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`. |

<a name="JwtBearerClientAuthentication+defaultGetKey"></a>

### jwtBearerClientAuthentication.defaultGetKey()
Default key resolution: HMAC from the client secret, asymmetric from the
client's registered JWK Set (inline `jwks` or remote `jwksUri`).

**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
<a name="JwtBearerClientAuthentication+assertNotReplayed"></a>

### jwtBearerClientAuthentication.assertNotReplayed()
Single-use replay protection. Opt-in: only enforced when the model
implements the replay hooks. The identifier passed to the hooks is the
assertion's `jti` when present, otherwise a fingerprint of its signing
input — so replay protection applies even to assertions without a `jti`
(OIDC Core §9 requires `jti`; RFC 7523 §3 makes it optional).

**Kind**: instance method of [<code>JwtBearerClientAuthentication</code>](#JwtBearerClientAuthentication)
<a name="CLIENT_ASSERTION_TYPE"></a>

## CLIENT\_ASSERTION\_TYPE
The `client_assertion_type` value identifying a JWT client assertion.

**Kind**: global constant
**See**: https://datatracker.ietf.org/doc/html/rfc7523#section-2.2
15 changes: 15 additions & 0 deletions docs/api/client-authentication/none.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<a name="None"></a>

## None ⇐ <code>AbstractClientAuthentication</code>
`none`: a public client that identifies itself with `client_id` only and
presents no secret (e.g. a PKCE flow, or a grant for which
`requireClientAuthentication` is disabled).

This adapter only resolves the client. Whether a secret-less client is
*acceptable* for the request is a policy decision owned by the orchestrator
(it knows the grant type, the `requireClientAuthentication` config and
whether this is a PKCE request).

**Kind**: global class
**Extends**: <code>AbstractClientAuthentication</code>
**See**: https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
Loading
Loading