diff --git a/docs/api/handlers/token-handler.md b/docs/api/handlers/token-handler.md
index d0817a0..9d15045 100644
--- a/docs/api/handlers/token-handler.md
+++ b/docs/api/handlers/token-handler.md
@@ -53,8 +53,14 @@ Get client credentials.
The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively,
the `client_id` and `client_secret` can be embedded in the body.
+Also support the assertion framework for client authentication.
+
**Kind**: instance method of [TokenHandler](#TokenHandler)
-**See**: https://tools.ietf.org/html/rfc6749#section-2.3.1
+**See**
+
+- https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
+- https://datatracker.ietf.org/doc/html/rfc7521
+
### tokenHandler.handleGrantType()
diff --git a/index.d.ts b/index.d.ts
index 74e554f..4027ee5 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -210,6 +210,16 @@ declare namespace OAuth2Server {
authorizationCodeLifetime?: number;
}
+ interface TokenRequest {
+ grant_type: string;
+ client_assertion?: string;
+ client_assertion_type?: string;
+ client_id?: string;
+ client_secret?: string;
+ code_verifier?: string;
+ scope?: string;
+ }
+
interface TokenOptions {
/**
* Lifetime of generated access tokens in seconds (default = 1 hour)
@@ -243,6 +253,17 @@ declare namespace OAuth2Server {
* Additional supported grant types.
*/
extendedGrantTypes?: Record;
+
+ /**
+ * Request processor
+ */
+ requestProcessor?: ((request: Request) => TokenRequest)
+ }
+
+ interface AssertionCredential {
+ clientAssertion: string;
+ clientAssertionType: string;
+ clientId?: string;
}
/**
@@ -268,6 +289,16 @@ declare namespace OAuth2Server {
*
*/
saveToken(token: Omit, client: Client, user: User): Promise;
+
+ /**
+ * Invoked to retrieve a client using a client assertion.
+ *
+ * It is for the model to decide if it supports the assertion framework and, if so, which
+ * assertion frameworks are supported. The function can return null if no model is found or
+ * throw an `InvalidClientError` if the assertion is invalid or not supported.
+ *
+ */
+ getClientFromAssertion?(assertion: AssertionCredential): Promise;
}
interface RequestAuthenticationModel {
diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js
index c6ebebb..d5d6de0 100644
--- a/lib/handlers/token-handler.js
+++ b/lib/handlers/token-handler.js
@@ -62,6 +62,7 @@ class TokenHandler {
this.requireClientAuthentication = options.requireClientAuthentication || {};
this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false;
this.enablePlainPKCE = options.enablePlainPKCE === true;
+ this.requestProcessor = options.requestProcessor;
}
/**
@@ -86,8 +87,13 @@ class TokenHandler {
}
try {
- const client = await this.getClient(request, response);
- const data = await this.handleGrantType(request, client);
+ const body = this.requestProcessor?.(request) ?? request.body;
+ const req = new Request({
+ ...request,
+ body,
+ });
+ const client = await this.getClient(req, response);
+ const data = await this.handleGrantType(req, client);
const model = new TokenModel(data, {
allowExtendedTokenAttributes: this.allowExtendedTokenAttributes,
});
@@ -117,25 +123,38 @@ class TokenHandler {
const grantType = request.body.grant_type;
const codeVerifier = request.body.code_verifier;
const isPkce = pkce.isPKCERequest({ grantType, codeVerifier });
+ const isAssertion = this.isClientAssertionRequest(request);
- if (!credentials.clientId) {
- throw new InvalidRequestError('Missing parameter: `client_id`');
- }
+ // @todo - if multiple authentication schemes exist, throw an error
+ if (!isAssertion) {
+ if (!credentials.clientId) {
+ throw new InvalidRequestError('Missing parameter: `client_id`');
+ }
- if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) {
- throw new InvalidRequestError('Missing parameter: `client_secret`');
- }
+ if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) {
+ throw new InvalidRequestError('Missing parameter: `client_secret`');
+ }
- if (!isFormat.vschar(credentials.clientId)) {
- throw new InvalidRequestError('Invalid parameter: `client_id`');
- }
+ if (!isFormat.vschar(credentials.clientId)) {
+ throw new InvalidRequestError('Invalid parameter: `client_id`');
+ }
- if (credentials.clientSecret && !isFormat.vschar(credentials.clientSecret)) {
- throw new InvalidRequestError('Invalid parameter: `client_secret`');
+ if (credentials.clientSecret && !isFormat.vschar(credentials.clientSecret)) {
+ throw new InvalidRequestError('Invalid parameter: `client_secret`');
+ }
+ } else {
+ if (!credentials.clientAssertion) {
+ throw new InvalidClientError('Missing parameter: `client_assertion`');
+ }
+ if (!credentials.clientAssertionType) {
+ throw new InvalidClientError('Missing parameter: `client_assertion_type`');
+ }
}
try {
- const client = await this.model.getClient(credentials.clientId, credentials.clientSecret);
+ const client = await (isAssertion
+ ? this.model.getClientFromAssertion?.(credentials)
+ : this.model.getClient(credentials.clientId, credentials.clientSecret));
if (!client) {
throw new InvalidClientError('Invalid client: client is invalid');
@@ -170,7 +189,10 @@ class TokenHandler {
* The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively,
* the `client_id` and `client_secret` can be embedded in the body.
*
- * @see https://tools.ietf.org/html/rfc6749#section-2.3.1
+ * Also support the assertion framework for client authentication.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
+ * @see https://datatracker.ietf.org/doc/html/rfc7521
*/
getClientCredentials(request) {
@@ -189,6 +211,14 @@ class TokenHandler {
};
}
+ if (this.isClientAssertionRequest(request)) {
+ return {
+ clientId: request.body.client_id,
+ clientAssertion: request.body.client_assertion,
+ clientAssertionType: request.body.client_assertion_type,
+ };
+ }
+
if (pkce.isPKCERequest({ grantType, codeVerifier })) {
if (request.body.client_id) {
return { clientId: request.body.client_id };
@@ -302,6 +332,10 @@ class TokenHandler {
response.status = error.code;
}
+ isClientAssertionRequest({ body }) {
+ return body.client_assertion && body.client_assertion_type;
+ }
+
/**
* Given a grant type, check if client authentication is required
*/