From 3282d4a8fd176698ff3a0917a91cdf32a2ba8686 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 14 Mar 2025 11:34:29 +0000 Subject: [PATCH 1/3] chore: support relevant syntax in eslint --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 8e36b31..df45459 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 9, + "ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures" : { "globalReturn": false, From 87e708a59f289a8d5a9cfbf511f5d243b86b8a1d Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 14 Mar 2025 11:35:36 +0000 Subject: [PATCH 2/3] feat: add support for assertion framework --- index.d.ts | 16 ++++++++++++ lib/handlers/token-handler.js | 48 +++++++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/index.d.ts b/index.d.ts index bb1396c..5fbdddc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -235,6 +235,12 @@ declare namespace OAuth2Server { extendedGrantTypes?: Record; } + interface AssertionCredential { + clientAssertion: string; + clientAssertionType: string; + clientId?: string; + } + /** * For returning falsey parameters in cases of failure */ @@ -258,6 +264,16 @@ declare namespace OAuth2Server { * */ saveToken(token: Token, 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 33c70ec..c4baf10 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -114,25 +114,36 @@ 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'); @@ -167,7 +178,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) { @@ -183,6 +197,10 @@ class TokenHandler { return { clientId: request.body.client_id, clientSecret: request.body.client_secret }; } + 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 }; @@ -289,6 +307,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 */ From 95cbfc506fe5d1a75dc5a9e61d39e98692c7863c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 10 Apr 2025 13:54:04 +0100 Subject: [PATCH 3/3] feat: add requestProcess helper for extracting data from requests --- index.d.ts | 15 +++++++++++++++ lib/handlers/token-handler.js | 12 +++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5fbdddc..b838cf4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -203,6 +203,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) @@ -233,6 +243,11 @@ declare namespace OAuth2Server { * Additional supported grant types. */ extendedGrantTypes?: Record; + + /** + * Request processor + */ + requestProcessor?: ((request: Request) => TokenRequest) } interface AssertionCredential { diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index c4baf10..6c3c1bf 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -61,6 +61,7 @@ class TokenHandler { this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; this.requireClientAuthentication = options.requireClientAuthentication || {}; this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false; + this.requestProcessor = options.requestProcessor; } /** @@ -85,8 +86,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 }); const tokenType = this.getTokenType(model); @@ -247,7 +253,7 @@ class TokenHandler { accessTokenLifetime: accessTokenLifetime, model: this.model, refreshTokenLifetime: refreshTokenLifetime, - alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken + alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, }; return new Type(options).handle(request, client);