Skip to content

Commit

Permalink
Added changes to support mTLS authentication (#1002)
Browse files Browse the repository at this point in the history
Co-authored-by: gyaneshgouraw-okta <[email protected]>
  • Loading branch information
gyaneshgouraw-okta and gyaneshgouraw-okta authored May 7, 2024
1 parent 3bc86cb commit c66ba4d
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 11 deletions.
23 changes: 23 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
4 changes: 4 additions & 0 deletions src/auth/base-auth-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface AuthenticationClientOptions extends ClientOptions {
clientAssertionSigningAlg?: string;
idTokenSigningAlg?: string; // default 'RS256'
clockTolerance?: number; // default 60s,
useMTLS?: boolean;
}

interface AuthApiErrorResponse {
Expand Down Expand Up @@ -92,6 +93,7 @@ export class BaseAuthAPI extends BaseAPI {
clientSecret?: string;
clientAssertionSigningKey?: string;
clientAssertionSigningAlg?: string;
useMTLS?: boolean;

constructor(options: AuthenticationClientOptions) {
super({
Expand All @@ -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;
}

/**
Expand All @@ -122,6 +125,7 @@ export class BaseAuthAPI extends BaseAPI {
clientSecret: this.clientSecret,
clientAssertionSigningKey: this.clientAssertionSigningKey,
clientAssertionSigningAlg: this.clientAssertionSigningAlg,
useMTLS: this.useMTLS,
});
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/auth/client-authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface AddClientAuthenticationOptions {
clientAssertionSigningKey?: string;
clientAssertionSigningAlg?: string;
clientSecret?: string;
useMTLS?: boolean;
}

/**
Expand All @@ -34,6 +35,7 @@ export const addClientAuthentication = async ({
clientAssertionSigningKey,
clientAssertionSigningAlg,
clientSecret,
useMTLS,
}: AddClientAuthenticationOptions): Promise<Record<string, unknown>> => {
const cid = payload.client_id || clientId;
if (clientAssertionSigningKey && !payload.client_assertion) {
Expand All @@ -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;
};
6 changes: 5 additions & 1 deletion src/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RetryConfiguration } from './retry.js';
import { Dispatcher } from 'undici';

/**
* @private
Expand All @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/lib/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 2 additions & 0 deletions src/management/management-client-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ export const generateClientInfo = () => ({
node: process.version.replace('v', ''),
},
});

/**
* @private
*/
export const mtlsPrefix = 'mtls';
56 changes: 54 additions & 2 deletions test/auth/client-authentication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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',
});
});
});
4 changes: 3 additions & 1 deletion test/auth/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down

0 comments on commit c66ba4d

Please sign in to comment.