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, diff --git a/index.d.ts b/index.d.ts index bb1396c..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,17 @@ declare namespace OAuth2Server { * Additional supported grant types. */ extendedGrantTypes?: Record; + + /** + * Request processor + */ + requestProcessor?: ((request: Request) => TokenRequest) + } + + interface AssertionCredential { + clientAssertion: string; + clientAssertionType: string; + clientId?: string; } /** @@ -258,6 +279,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..64fd92f 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 ?? ((req) => req.body); } /** @@ -85,8 +86,15 @@ 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 client = await this.getClient({ + ...request, + body, + }, response); + const data = await this.handleGrantType({ + ...request, + body, + }, client); const model = new TokenModel(data, { allowExtendedTokenAttributes: this.allowExtendedTokenAttributes }); const tokenType = this.getTokenType(model); @@ -114,25 +122,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 +186,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 +205,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 }; @@ -229,7 +255,7 @@ class TokenHandler { accessTokenLifetime: accessTokenLifetime, model: this.model, refreshTokenLifetime: refreshTokenLifetime, - alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken + alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, }; return new Type(options).handle(request, client); @@ -289,6 +315,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 */