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 */