From c66ba4d613e203299f5aa9d05521595b22726615 Mon Sep 17 00:00:00 2001 From: Gyanesh Gouraw <160731216+gyaneshgouraw-okta@users.noreply.github.com> Date: Tue, 7 May 2024 13:21:03 +0530 Subject: [PATCH] Added changes to support mTLS authentication (#1002) Co-authored-by: gyaneshgouraw-okta --- EXAMPLES.md | 23 +++++++++ package-lock.json | 18 ++++++- package.json | 3 +- src/auth/base-auth-api.ts | 4 ++ src/auth/client-authentication.ts | 9 +++- src/auth/oauth.ts | 6 ++- src/lib/models.ts | 4 +- src/lib/runtime.ts | 2 +- src/management/management-client-options.ts | 2 + src/utils.ts | 5 ++ test/auth/client-authentication.test.ts | 56 ++++++++++++++++++++- test/auth/oauth.test.ts | 4 +- 12 files changed, 125 insertions(+), 11 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index c9c510ed0..ad61d9470 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -6,6 +6,7 @@ - [Use Refresh Tokens](#use-refresh-tokens) - [Complete the Authorization Code flow with PKCE](#complete-the-authorization-code-flow-with-pkce) - [Login with Passwordless](#login-with-passwordless) + - [mTLS request](#mtls-request) - [Management Client](#management-client) - [Paginate through a list of users](#paginate-through-a-list-of-users) - [Paginate through a list of logs using checkpoint pagination](#paginate-through-a-list-of-logs-using-checkpoint-pagination) @@ -129,6 +130,28 @@ const { data: tokens } = await auth.passwordless.loginWithEmail({ }); ``` +### mTLS request + +Refer mTLS documentation for more info - [Link](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authenticate-with-mtls) + +```js +import { AuthenticationClient } from 'auth0'; +const { Agent } = require('undici'); + +const auth = new AuthenticationClient({ + domain: '{YOUR_TENANT_AND REGION}.auth0.com', + clientId: '{YOUR_CLIENT_ID}', + agent: new Agent({ + connect: { cert: 'your_cert', key: 'your_key' }, + }), + useMTLS: true, +}); + +const { data: tokens } = await auth.oauth.clientCredentialsGrant({ + audience: 'you-api', +}); +``` + ## Management Client ### Paginate through a list of users diff --git a/package-lock.json b/package-lock.json index f785a2eb3..091ac198e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,8 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typedoc": "^0.24.6", - "typescript": "4.9.5" + "typescript": "4.9.5", + "undici": "^6.15.0" }, "engines": { "node": ">=18" @@ -7247,6 +7248,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.15.0.tgz", + "integrity": "sha512-VviMt2tlMg1BvQ0FKXxrz1eJuyrcISrL2sPfBf7ZskX/FCEc/7LeThQaoygsMJpNqrATWQIsRVx+1Dpe4jaYuQ==", + "dev": true, + "engines": { + "node": ">=18.17" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -12875,6 +12885,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.15.0.tgz", + "integrity": "sha512-VviMt2tlMg1BvQ0FKXxrz1eJuyrcISrL2sPfBf7ZskX/FCEc/7LeThQaoygsMJpNqrATWQIsRVx+1Dpe4jaYuQ==", + "dev": true + }, "update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", diff --git a/package.json b/package.json index b0b61aff9..74d583312 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typedoc": "^0.24.6", - "typescript": "4.9.5" + "typescript": "4.9.5", + "undici": "^6.15.0" } } diff --git a/src/auth/base-auth-api.ts b/src/auth/base-auth-api.ts index 027245dbf..eca70332d 100644 --- a/src/auth/base-auth-api.ts +++ b/src/auth/base-auth-api.ts @@ -22,6 +22,7 @@ export interface AuthenticationClientOptions extends ClientOptions { clientAssertionSigningAlg?: string; idTokenSigningAlg?: string; // default 'RS256' clockTolerance?: number; // default 60s, + useMTLS?: boolean; } interface AuthApiErrorResponse { @@ -92,6 +93,7 @@ export class BaseAuthAPI extends BaseAPI { clientSecret?: string; clientAssertionSigningKey?: string; clientAssertionSigningAlg?: string; + useMTLS?: boolean; constructor(options: AuthenticationClientOptions) { super({ @@ -107,6 +109,7 @@ export class BaseAuthAPI extends BaseAPI { this.clientSecret = options.clientSecret; this.clientAssertionSigningKey = options.clientAssertionSigningKey; this.clientAssertionSigningAlg = options.clientAssertionSigningAlg; + this.useMTLS = options.useMTLS; } /** @@ -122,6 +125,7 @@ export class BaseAuthAPI extends BaseAPI { clientSecret: this.clientSecret, clientAssertionSigningKey: this.clientAssertionSigningKey, clientAssertionSigningAlg: this.clientAssertionSigningAlg, + useMTLS: this.useMTLS, }); } } diff --git a/src/auth/client-authentication.ts b/src/auth/client-authentication.ts index 1c10fb9f0..9d28f3439 100644 --- a/src/auth/client-authentication.ts +++ b/src/auth/client-authentication.ts @@ -17,6 +17,7 @@ interface AddClientAuthenticationOptions { clientAssertionSigningKey?: string; clientAssertionSigningAlg?: string; clientSecret?: string; + useMTLS?: boolean; } /** @@ -34,6 +35,7 @@ export const addClientAuthentication = async ({ clientAssertionSigningKey, clientAssertionSigningAlg, clientSecret, + useMTLS, }: AddClientAuthenticationOptions): Promise> => { const cid = payload.client_id || clientId; if (clientAssertionSigningKey && !payload.client_assertion) { @@ -55,9 +57,12 @@ export const addClientAuthentication = async ({ } if ( (!payload.client_secret || payload.client_secret.trim().length === 0) && - (!payload.client_assertion || payload.client_assertion.trim().length === 0) + (!payload.client_assertion || payload.client_assertion.trim().length === 0) && + !useMTLS ) { - throw new Error('The client_secret or client_assertion field is required.'); + throw new Error( + 'The client_secret or client_assertion field is required, or it should be mTLS request.' + ); } return payload; }; diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index e6e1eb7b0..2902b1051 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -6,6 +6,7 @@ import { } from '../lib/runtime.js'; import { BaseAuthAPI, AuthenticationClientOptions, grant } from './base-auth-api.js'; import { IDTokenValidateOptions, IDTokenValidator } from './id-token-validator.js'; +import { mtlsPrefix } from '../utils.js'; export interface TokenSet { /** @@ -271,7 +272,10 @@ export interface TokenExchangeGrantRequest { export class OAuth extends BaseAuthAPI { private idTokenValidator: IDTokenValidator; constructor(options: AuthenticationClientOptions) { - super(options); + super({ + ...options, + domain: options.useMTLS ? `${mtlsPrefix}.${options.domain}` : options.domain, + }); this.idTokenValidator = new IDTokenValidator(options); } diff --git a/src/lib/models.ts b/src/lib/models.ts index 2ffbb142c..9c72c0539 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -1,4 +1,5 @@ import { RetryConfiguration } from './retry.js'; +import { Dispatcher } from 'undici'; /** * @private @@ -25,8 +26,7 @@ export interface Configuration { /** * Pass your own http agent to support proxies. */ - // https://github.com/octokit/types.ts/blob/v10.0.0/src/RequestRequestOptions.ts#L13 - agent?: unknown; + agent?: Dispatcher; /** * Custom headers that will be added to every request. */ diff --git a/src/lib/runtime.ts b/src/lib/runtime.ts index bfcf5ccbe..fd519d81e 100644 --- a/src/lib/runtime.ts +++ b/src/lib/runtime.ts @@ -73,7 +73,7 @@ export class BaseAPI { method: context.method, headers, body: context.body, - agent: this.configuration.agent, + dispatcher: this.configuration.agent, }; const overriddenInit: RequestInit = { diff --git a/src/management/management-client-options.ts b/src/management/management-client-options.ts index 933062afb..2ef993a00 100644 --- a/src/management/management-client-options.ts +++ b/src/management/management-client-options.ts @@ -12,12 +12,14 @@ export interface ManagementClientOptionsWithToken extends ManagementClientOption export interface ManagementClientOptionsWithClientSecret extends ManagementClientOptions { clientId: string; clientSecret: string; + useMTLS?: boolean; } export interface ManagementClientOptionsWithClientAssertion extends ManagementClientOptions { clientId: string; clientAssertionSigningKey: string; clientAssertionSigningAlg?: string; + useMTLS?: boolean; } export type ManagementClientOptionsWithClientCredentials = diff --git a/src/utils.ts b/src/utils.ts index 906f20ead..6e668746c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,3 +10,8 @@ export const generateClientInfo = () => ({ node: process.version.replace('v', ''), }, }); + +/** + * @private + */ +export const mtlsPrefix = 'mtls'; diff --git a/test/auth/client-authentication.test.ts b/test/auth/client-authentication.test.ts index 53000d469..9a830b86f 100644 --- a/test/auth/client-authentication.test.ts +++ b/test/auth/client-authentication.test.ts @@ -3,7 +3,7 @@ import { jest } from '@jest/globals'; import * as jose from 'jose'; import { AuthenticationClient } from '../../src/index.js'; import { TEST_PUBLIC_KEY, TEST_PRIVATE_KEY } from '../constants.js'; - +import { Agent } from 'undici'; const URL = 'https://tenant.auth0.com/'; const clientId = 'test-client-id'; const verifyOpts = { @@ -111,7 +111,9 @@ describe('client-authentication', () => { auth0.oauth.clientCredentialsGrant({ audience: 'my-api', }) - ).rejects.toThrow('The client_secret or client_assertion field is required.'); + ).rejects.toThrow( + 'The client_secret or client_assertion field is required, or it should be mTLS request.' + ); }); it('should allow you to pass your own client assertion', async () => { @@ -205,3 +207,53 @@ describe('client-authentication for par endpoint', () => { }); }); }); + +describe('mTLS-authentication', () => { + const path = jest.fn(); + const body = jest.fn(); + const headers = jest.fn(); + const clientAssertion = jest.fn(); + const URL = 'https://mtls.tenant.auth0.com/'; + + beforeEach(() => { + async function handler(this: any, pathIn: unknown, bodyIn: string) { + const bodyParsed = Object.fromEntries(new URLSearchParams(bodyIn)); + path(pathIn); + body(bodyParsed); + headers(this.req.headers); + if ((bodyParsed as any).client_assertion) { + clientAssertion(await verify(bodyParsed.client_assertion, TEST_PUBLIC_KEY, verifyOpts)); + } + return { + access_token: 'test-access-token', + }; + } + + nock(URL, { encodedQueryParams: true }).post('/oauth/token').reply(200, handler).persist(); + }); + + afterEach(() => { + nock.cleanAll(); + jest.clearAllMocks(); + }); + + it('should do client credentials grant without client secret or assertion & only with agent', async () => { + const auth0 = new AuthenticationClient({ + domain: 'tenant.auth0.com', + clientId, + agent: new Agent({ + connect: { cert: 'my-cert', key: 'my-key' }, + }), + useMTLS: true, + }); + await auth0.oauth.clientCredentialsGrant({ + audience: 'my-api', + }); + expect(path).toHaveBeenCalledWith('/oauth/token'); + expect(body).toHaveBeenCalledWith({ + grant_type: 'client_credentials', + client_id: clientId, + audience: 'my-api', + }); + }); +}); diff --git a/test/auth/oauth.test.ts b/test/auth/oauth.test.ts index 0e0cbaa7e..24ca8d13d 100644 --- a/test/auth/oauth.test.ts +++ b/test/auth/oauth.test.ts @@ -310,7 +310,9 @@ describe('OAuth', () => { response_type: 'code', redirect_uri: 'https://example.com', } as PushedAuthorizationRequest) - ).rejects.toThrow('The client_secret or client_assertion field is required.'); + ).rejects.toThrow( + 'The client_secret or client_assertion field is required, or it should be mTLS request.' + ); }); it('should return the par response', async () => {