From 97d1471208f83b7aa299f5ec1f6175e6aaf3c2b9 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Thu, 4 Dec 2025 00:53:21 +0530 Subject: [PATCH 1/6] fix: enhance safeJson to handle OAuth 2.0 errors and improve JSON parsing --- src/core/services/HttpClient.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/core/services/HttpClient.ts b/src/core/services/HttpClient.ts index ff86f988..f2d72b05 100644 --- a/src/core/services/HttpClient.ts +++ b/src/core/services/HttpClient.ts @@ -73,20 +73,42 @@ export class HttpClient { /** * Safely parses a JSON response, handling cases where the body might be empty or invalid JSON. * This prevents "body already consumed" errors by reading text first, then parsing. + * + * For error responses (4xx/5xx), if the body is not valid JSON, we check the WWW-Authenticate + * header for OAuth 2.0 Bearer token errors (RFC 6750), which is how endpoints like /userinfo + * return errors. */ private async safeJson(response: Response): Promise { if (response.status === 204) { // No Content return {}; } - let text = 'Failed to parse response body'; + + let text = ''; try { text = await response.text(); return JSON.parse(text); } catch { + // For error responses, check WWW-Authenticate header (RFC 6750) + // This is how OAuth 2.0 protected resources like /userinfo return errors + if (!response.ok) { + const wwwAuthError = this.parseWwwAuthenticateHeader(response); + if (wwwAuthError) { + return wwwAuthError; + } + + // Fallback: return a generic HTTP error with the status code + return { + error: `http_error_${response.status}`, + error_description: + text || response.statusText || `HTTP ${response.status} error`, + }; + } + + // For successful responses with invalid JSON, return invalid_json error return { error: 'invalid_json', - error_description: text, + error_description: text || 'Failed to parse response body', }; } } From 457d8eab72d92e7ae0cba2073ba35df4c5bc9f1e Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Thu, 4 Dec 2025 00:54:26 +0530 Subject: [PATCH 2/6] feat: implement DPoP support in authentication flows and enhance HttpClient for token handling --- .../services/AuthenticationOrchestrator.ts | 46 ++- src/core/services/HttpClient.ts | 51 +++ .../services/ManagementApiOrchestrator.ts | 58 +++- .../AuthenticationOrchestrator.spec.ts | 64 +++- .../services/__tests__/HttpClient.spec.ts | 307 ++++++++++++++++++ .../ManagementApiOrchestrator.spec.ts | 34 +- .../native/adapters/NativeAuth0Client.ts | 34 +- src/platforms/web/adapters/WebAuth0Client.ts | 34 +- .../adapters/__tests__/WebAuth0Client.spec.ts | 21 +- 9 files changed, 614 insertions(+), 35 deletions(-) create mode 100644 src/core/services/__tests__/HttpClient.spec.ts diff --git a/src/core/services/AuthenticationOrchestrator.ts b/src/core/services/AuthenticationOrchestrator.ts index 0f0d0538..e29167e7 100644 --- a/src/core/services/AuthenticationOrchestrator.ts +++ b/src/core/services/AuthenticationOrchestrator.ts @@ -29,7 +29,7 @@ import { AuthError, } from '../models'; import { validateParameters } from '../utils/validation'; -import { HttpClient } from './HttpClient'; +import { HttpClient, getBearerHeader, TokenType } from './HttpClient'; import { deepCamelCase } from '../utils'; // Represents the raw user profile returned by an API (snake_case) @@ -59,6 +59,18 @@ function includeRequiredScope(scope?: string): string { return scope; } +/** + * Function type for getting DPoP headers from the native/platform layer. + * This allows the orchestrator to request DPoP proof generation when needed. + */ +export type DPoPHeadersProvider = (params: { + url: string; + method: string; + accessToken: string; + tokenType: string; + nonce?: string; +}) => Promise>; + /** * Orchestrates all direct authentication flows by making calls to the Auth0 Authentication API. * This class is platform-agnostic and relies on an injected HttpClient. @@ -66,10 +78,22 @@ function includeRequiredScope(scope?: string): string { export class AuthenticationOrchestrator implements IAuthenticationProvider { private readonly client: HttpClient; private readonly clientId: string; + private readonly tokenType: TokenType; + private readonly baseUrl: string; + private readonly getDPoPHeaders?: DPoPHeadersProvider; - constructor(options: { clientId: string; httpClient: HttpClient }) { + constructor(options: { + clientId: string; + httpClient: HttpClient; + tokenType?: TokenType; + baseUrl?: string; + getDPoPHeaders?: DPoPHeadersProvider; + }) { this.clientId = options.clientId; this.client = options.httpClient; + this.tokenType = options.tokenType ?? TokenType.bearer; + this.baseUrl = options.baseUrl ?? ''; + this.getDPoPHeaders = options.getDPoPHeaders; } authorizeUrl(parameters: AuthorizeUrlParameters): string { @@ -369,7 +393,23 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider { async userInfo(parameters: UserInfoParameters): Promise { const { token, headers } = parameters; - const requestHeaders = { Authorization: `Bearer ${token}`, ...headers }; + + let authHeader: Record; + + // For DPoP tokens, we need to generate a DPoP proof using the native layer + if (this.tokenType === TokenType.dpop && this.getDPoPHeaders) { + const userInfoUrl = `${this.baseUrl}/userinfo`; + authHeader = await this.getDPoPHeaders({ + url: userInfoUrl, + method: 'GET', + accessToken: token, + tokenType: TokenType.dpop, + }); + } else { + authHeader = getBearerHeader(token); + } + + const requestHeaders = { ...authHeader, ...headers }; const { json, response } = await this.client.get( '/userinfo', undefined, diff --git a/src/core/services/HttpClient.ts b/src/core/services/HttpClient.ts index f2d72b05..37caa7eb 100644 --- a/src/core/services/HttpClient.ts +++ b/src/core/services/HttpClient.ts @@ -4,6 +4,25 @@ import { AuthError } from '../models'; import base64 from 'base-64'; import { telemetry } from '../utils/telemetry'; +/** + * Token type representing the type of token to use for authentication. + */ +export enum TokenType { + /** Standard Bearer token authentication (default) */ + bearer = 'Bearer', + /** Demonstrating Proof-of-Possession token authentication */ + dpop = 'DPoP', +} + +/** + * Returns the Bearer authentication header. + * @param token - The token value + * @returns A record with the Authorization header containing the Bearer token + */ +export function getBearerHeader(token: string): Record { + return { Authorization: `${TokenType.bearer} ${token}` }; +} + export interface HttpClientOptions { baseUrl: string; timeout?: number; @@ -70,6 +89,38 @@ export class HttpClient { return url; } + /** + * Parses the WWW-Authenticate header to extract error information. + * Per RFC 6750, OAuth 2.0 Bearer Token errors are returned in this header with format: + * Bearer error="invalid_token", error_description="The access token expired" + * + * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3 + */ + private parseWwwAuthenticateHeader( + response: Response + ): { error: string; error_description?: string } | null { + const wwwAuthenticate = response.headers.get('WWW-Authenticate'); + if (!wwwAuthenticate) { + return null; + } + + // Parse key="value" pairs from the header + // Matches: error="invalid_token", error_description="The access token expired" + const errorMatch = wwwAuthenticate.match(/error="([^"]+)"/); + const descriptionMatch = wwwAuthenticate.match( + /error_description="([^"]+)"/ + ); + + if (errorMatch?.[1]) { + return { + error: errorMatch[1], + error_description: descriptionMatch?.[1], + }; + } + + return null; + } + /** * Safely parses a JSON response, handling cases where the body might be empty or invalid JSON. * This prevents "body already consumed" errors by reading text first, then parsing. diff --git a/src/core/services/ManagementApiOrchestrator.ts b/src/core/services/ManagementApiOrchestrator.ts index 851a6d62..cc397a94 100644 --- a/src/core/services/ManagementApiOrchestrator.ts +++ b/src/core/services/ManagementApiOrchestrator.ts @@ -1,29 +1,65 @@ import type { IUsersClient } from '../interfaces/IUsersClient'; import type { GetUserParameters, PatchUserParameters, User } from '../../types'; import { Auth0User, AuthError } from '../models'; -import { HttpClient } from '../services/HttpClient'; +import { HttpClient, getBearerHeader, TokenType } from '../services/HttpClient'; import { deepCamelCase } from '../utils'; +/** + * Function type for getting DPoP headers from the native/platform layer. + */ +export type DPoPHeadersProvider = (params: { + url: string; + method: string; + accessToken: string; + tokenType: string; + nonce?: string; +}) => Promise>; + /** * Orchestrates interactions with the Auth0 Management API's user endpoints. */ export class ManagementApiOrchestrator implements IUsersClient { private readonly client: HttpClient; private readonly token: string; + private readonly tokenType: TokenType; + private readonly baseUrl: string; + private readonly getDPoPHeaders?: DPoPHeadersProvider; - constructor(options: { token: string; httpClient: HttpClient }) { + constructor(options: { + token: string; + httpClient: HttpClient; + tokenType?: TokenType; + baseUrl?: string; + getDPoPHeaders?: DPoPHeadersProvider; + }) { this.token = options.token; this.client = options.httpClient; + this.tokenType = options.tokenType ?? TokenType.bearer; + this.baseUrl = options.baseUrl ?? ''; + this.getDPoPHeaders = options.getDPoPHeaders; } + /** * Creates the specific headers required for Management API requests, - * including the Bearer token. - * @returns A record of headers for the request. + * including the Bearer or DPoP token based on tokenType. + * @param path - The API path (used to build full URL for DPoP proof generation) + * @param method - The HTTP method (needed for DPoP proof generation) + * @returns A promise that resolves to a record of headers for the request. */ - private getRequestHeaders(): Record { - return { - Authorization: `Bearer ${this.token}`, - }; + private async getRequestHeaders( + path: string, + method: string + ): Promise> { + if (this.tokenType === TokenType.dpop && this.getDPoPHeaders) { + const fullUrl = `${this.baseUrl}${path}`; + return this.getDPoPHeaders({ + url: fullUrl, + method, + accessToken: this.token, + tokenType: TokenType.dpop, + }); + } + return getBearerHeader(this.token); } /** @@ -44,11 +80,12 @@ export class ManagementApiOrchestrator implements IUsersClient { async getUser(parameters: GetUserParameters): Promise { const path = `/api/v2/users/${encodeURIComponent(parameters.id)}`; + const headers = await this.getRequestHeaders(path, 'GET'); const { json, response } = await this.client.get( path, undefined, // No query parameters - this.getRequestHeaders() + headers ); if (!response.ok) { @@ -64,11 +101,12 @@ export class ManagementApiOrchestrator implements IUsersClient { const body = { user_metadata: parameters.metadata, }; + const headers = await this.getRequestHeaders(path, 'PATCH'); const { json, response } = await this.client.patch( path, body, - this.getRequestHeaders() + headers ); if (!response.ok) { diff --git a/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts b/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts index 9b74f1d6..40c33193 100644 --- a/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts +++ b/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts @@ -6,8 +6,34 @@ import { Credentials as CredentialsModel, } from '../../models'; -// 1. Mock the HttpClient. We only need to mock the methods we use. -jest.mock('../HttpClient'); +// Mock HttpClient but preserve getAuthHeader +jest.mock('../HttpClient', () => { + const actual = jest.requireActual('../HttpClient'); + return { + ...actual, + HttpClient: jest.fn().mockImplementation(() => ({ + get: jest.fn(), + post: jest.fn(), + patch: jest.fn(), + buildUrl: jest.fn((path: string, query?: Record) => { + let url = `https://samples.auth0.com${path}`; + if (query) { + const params = new URLSearchParams(); + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)); + } + }); + const queryString = params.toString(); + if (queryString) { + url += `?${queryString}`; + } + } + return url; + }), + })), + }; +}); const MockHttpClient = HttpClient as jest.MockedClass; // 2. Define test data, mirroring the old auth.spec.js file. @@ -44,7 +70,12 @@ const userProfileResponse = { describe('AuthenticationOrchestrator', () => { let orchestrator: AuthenticationOrchestrator; - let mockHttpClientInstance: jest.Mocked; + let mockHttpClientInstance: { + get: jest.Mock; + post: jest.Mock; + patch: jest.Mock; + buildUrl: jest.Mock; + }; beforeAll(() => { // Set a fixed date for consistent `expiresAt` calculations @@ -57,12 +88,35 @@ describe('AuthenticationOrchestrator', () => { beforeEach(() => { // Reset mocks and create new instances for each test + mockHttpClientInstance = { + get: jest.fn(), + post: jest.fn(), + patch: jest.fn(), + buildUrl: jest.fn((path: string, query?: Record) => { + let url = `${baseUrl}${path}`; + if (query) { + const params = new URLSearchParams(); + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)); + } + }); + const queryString = params.toString(); + if (queryString) { + url += `?${queryString}`; + } + } + return url; + }), + }; MockHttpClient.mockClear(); + (MockHttpClient as jest.Mock).mockImplementation( + () => mockHttpClientInstance + ); orchestrator = new AuthenticationOrchestrator({ clientId, - httpClient: new MockHttpClient({ baseUrl }), + httpClient: mockHttpClientInstance as unknown as HttpClient, }); - mockHttpClientInstance = MockHttpClient.mock.instances[0]; }); // Note: Constructor and URL builder tests are not applicable here. diff --git a/src/core/services/__tests__/HttpClient.spec.ts b/src/core/services/__tests__/HttpClient.spec.ts new file mode 100644 index 00000000..e57f57f5 --- /dev/null +++ b/src/core/services/__tests__/HttpClient.spec.ts @@ -0,0 +1,307 @@ +import { HttpClient, getBearerHeader } from '../HttpClient'; + +// Mock the telemetry module +jest.mock('../../utils/telemetry', () => ({ + telemetry: { name: 'test', version: '1.0.0' }, +})); + +describe('HttpClient', () => { + const baseUrl = 'https://test.auth0.com'; + let httpClient: HttpClient; + + beforeEach(() => { + httpClient = new HttpClient({ baseUrl }); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('getBearerHeader', () => { + it('should return Bearer header', () => { + const result = getBearerHeader('test-token'); + expect(result).toEqual({ Authorization: 'Bearer test-token' }); + }); + }); + + describe('safeJson - WWW-Authenticate header parsing', () => { + it('should parse error from WWW-Authenticate header on 401 response', async () => { + const mockResponse = { + ok: false, + status: 401, + statusText: 'Unauthorized', + text: jest.fn().mockResolvedValue(''), + headers: new Map([ + [ + 'WWW-Authenticate', + 'Bearer error="invalid_token", error_description="The access token expired"', + ], + ]) as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn((name: string) => { + if (name === 'WWW-Authenticate') { + return 'Bearer error="invalid_token", error_description="The access token expired"'; + } + return null; + }); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/userinfo'); + + expect(json).toEqual({ + error: 'invalid_token', + error_description: 'The access token expired', + }); + }); + + it('should parse error without description from WWW-Authenticate header', async () => { + const mockResponse = { + ok: false, + status: 401, + statusText: 'Unauthorized', + text: jest.fn().mockResolvedValue(''), + headers: new Map([ + ['WWW-Authenticate', 'Bearer error="invalid_token"'], + ]) as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn((name: string) => { + if (name === 'WWW-Authenticate') { + return 'Bearer error="invalid_token"'; + } + return null; + }); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/userinfo'); + + expect(json).toEqual({ + error: 'invalid_token', + error_description: undefined, + }); + }); + + it('should fallback to http_error_401 when WWW-Authenticate has no error', async () => { + const mockResponse = { + ok: false, + status: 401, + statusText: 'Unauthorized', + text: jest.fn().mockResolvedValue(''), + headers: new Map([ + ['WWW-Authenticate', 'Bearer'], + ]) as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn((name: string) => { + if (name === 'WWW-Authenticate') { + return 'Bearer'; + } + return null; + }); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/userinfo'); + + expect(json).toEqual({ + error: 'http_error_401', + error_description: 'Unauthorized', + }); + }); + + it('should fallback to http_error when no WWW-Authenticate header', async () => { + const mockResponse = { + ok: false, + status: 403, + statusText: 'Forbidden', + text: jest.fn().mockResolvedValue('Access denied'), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/some-endpoint'); + + expect(json).toEqual({ + error: 'http_error_403', + error_description: 'Access denied', + }); + }); + + it('should use statusText when response body is empty', async () => { + const mockResponse = { + ok: false, + status: 500, + statusText: 'Internal Server Error', + text: jest.fn().mockResolvedValue(''), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/some-endpoint'); + + expect(json).toEqual({ + error: 'http_error_500', + error_description: 'Internal Server Error', + }); + }); + + it('should return invalid_json for successful response with invalid JSON', async () => { + const mockResponse = { + ok: true, + status: 200, + statusText: 'OK', + text: jest.fn().mockResolvedValue('not valid json'), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/some-endpoint'); + + expect(json).toEqual({ + error: 'invalid_json', + error_description: 'not valid json', + }); + }); + + it('should parse valid JSON response correctly', async () => { + const mockResponse = { + ok: true, + status: 200, + statusText: 'OK', + text: jest + .fn() + .mockResolvedValue('{"sub": "user123", "name": "Test User"}'), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/userinfo'); + + expect(json).toEqual({ + sub: 'user123', + name: 'Test User', + }); + }); + + it('should return empty object for 204 No Content', async () => { + const mockResponse = { + ok: true, + status: 204, + statusText: 'No Content', + text: jest.fn(), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/some-endpoint'); + + expect(json).toEqual({}); + expect(mockResponse.text).not.toHaveBeenCalled(); + }); + + it('should parse JSON error response from body when available', async () => { + const mockResponse = { + ok: false, + status: 400, + statusText: 'Bad Request', + text: jest + .fn() + .mockResolvedValue( + '{"error": "invalid_request", "error_description": "Missing parameter"}' + ), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/oauth/token'); + + expect(json).toEqual({ + error: 'invalid_request', + error_description: 'Missing parameter', + }); + }); + + it('should handle insufficient_scope error in WWW-Authenticate', async () => { + const mockResponse = { + ok: false, + status: 403, + statusText: 'Forbidden', + text: jest.fn().mockResolvedValue(''), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn((name: string) => { + if (name === 'WWW-Authenticate') { + return 'Bearer error="insufficient_scope", error_description="The request requires higher privileges"'; + } + return null; + }); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/api/admin'); + + expect(json).toEqual({ + error: 'insufficient_scope', + error_description: 'The request requires higher privileges', + }); + }); + + it('should handle invalid_request error in WWW-Authenticate', async () => { + const mockResponse = { + ok: false, + status: 400, + statusText: 'Bad Request', + text: jest.fn().mockResolvedValue(''), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn((name: string) => { + if (name === 'WWW-Authenticate') { + return 'Bearer error="invalid_request", error_description="The request is missing a required parameter"'; + } + return null; + }); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/userinfo'); + + expect(json).toEqual({ + error: 'invalid_request', + error_description: 'The request is missing a required parameter', + }); + }); + + it('should use text body as description when WWW-Authenticate is not present', async () => { + const mockResponse = { + ok: false, + status: 401, + statusText: 'Unauthorized', + text: jest.fn().mockResolvedValue('Token has expired'), + headers: new Map() as unknown as Headers, + } as unknown as Response; + mockResponse.headers.get = jest.fn(() => null); + + global.fetch = jest.fn().mockResolvedValue(mockResponse); + + const { json } = await httpClient.get('/userinfo'); + + expect(json).toEqual({ + error: 'http_error_401', + error_description: 'Token has expired', + }); + }); + }); +}); diff --git a/src/core/services/__tests__/ManagementApiOrchestrator.spec.ts b/src/core/services/__tests__/ManagementApiOrchestrator.spec.ts index 2d805936..f469a2a0 100644 --- a/src/core/services/__tests__/ManagementApiOrchestrator.spec.ts +++ b/src/core/services/__tests__/ManagementApiOrchestrator.spec.ts @@ -2,10 +2,21 @@ import { ManagementApiOrchestrator } from '../ManagementApiOrchestrator'; import { HttpClient } from '../HttpClient'; import { AuthError, Auth0User } from '../../models'; -jest.mock('../HttpClient'); +// Mock HttpClient but preserve getBearerHeader +jest.mock('../HttpClient', () => { + const actual = jest.requireActual('../HttpClient'); + return { + ...actual, + HttpClient: jest.fn().mockImplementation(() => ({ + get: jest.fn(), + post: jest.fn(), + patch: jest.fn(), + buildUrl: jest.fn(), + })), + }; +}); const MockHttpClient = HttpClient as jest.MockedClass; -const baseUrl = 'https://samples.auth0.com'; const managementToken = 'a.management.api.token'; const userId = 'auth0|53b995f8bce68d9fc900099c'; @@ -34,14 +45,27 @@ const managementApiErrorResponse = { describe('ManagementApiOrchestrator', () => { let orchestrator: ManagementApiOrchestrator; - let mockHttpClientInstance: jest.Mocked; + let mockHttpClientInstance: { + get: jest.Mock; + post: jest.Mock; + patch: jest.Mock; + buildUrl: jest.Mock; + }; beforeEach(() => { jest.clearAllMocks(); - mockHttpClientInstance = new MockHttpClient({ baseUrl }); + mockHttpClientInstance = { + get: jest.fn(), + post: jest.fn(), + patch: jest.fn(), + buildUrl: jest.fn(), + }; + (MockHttpClient as jest.Mock).mockImplementation( + () => mockHttpClientInstance + ); orchestrator = new ManagementApiOrchestrator({ token: managementToken, - httpClient: mockHttpClientInstance, + httpClient: mockHttpClientInstance as unknown as HttpClient, }); }); diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index ead5bfa9..489747e0 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -13,6 +13,10 @@ import { ManagementApiOrchestrator, } from '../../../core/services'; import { HttpClient } from '../../../core/services/HttpClient'; +import { + type TokenType, + TokenType as TokenTypeEnum, +} from '../../../core/services/HttpClient'; import { AuthError, DPoPError } from '../../../core/models'; export class NativeAuth0Client implements IAuth0Client { @@ -21,21 +25,44 @@ export class NativeAuth0Client implements IAuth0Client { readonly auth: IAuthenticationProvider; private ready: Promise; private readonly httpClient: HttpClient; + private readonly tokenType: TokenType; private readonly bridge: INativeBridge; + private readonly baseUrl: string; + private readonly getDPoPHeadersForOrchestrator?: ( + params: DPoPHeadersParams + ) => Promise>; constructor(options: NativeAuth0Options) { const baseUrl = `https://${options.domain}`; + this.baseUrl = baseUrl; + const useDPoP = options.useDPoP ?? true; + this.tokenType = useDPoP ? TokenTypeEnum.dpop : TokenTypeEnum.bearer; + this.httpClient = new HttpClient({ baseUrl: baseUrl, timeout: options.timeout, headers: options.headers, }); + + const bridge = new NativeBridgeManager(); + this.bridge = bridge; + + // Create a bound getDPoPHeaders function for the orchestrator + const getDPoPHeadersForOrchestrator = async (params: DPoPHeadersParams) => { + await this.ready; + return this.bridge.getDPoPHeaders(params); + }; + this.getDPoPHeadersForOrchestrator = useDPoP + ? getDPoPHeadersForOrchestrator + : undefined; + this.auth = new AuthenticationOrchestrator({ clientId: options.clientId, httpClient: this.httpClient, + tokenType: this.tokenType, + baseUrl: baseUrl, + getDPoPHeaders: useDPoP ? getDPoPHeadersForOrchestrator : undefined, }); - const bridge = new NativeBridgeManager(); - this.bridge = bridge; this.ready = this.initialize(bridge, options); @@ -71,6 +98,9 @@ export class NativeAuth0Client implements IAuth0Client { return new ManagementApiOrchestrator({ token: token, httpClient: this.httpClient, + tokenType: this.tokenType, + baseUrl: this.baseUrl, + getDPoPHeaders: this.getDPoPHeadersForOrchestrator, }); } diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index ad365df4..3657ce53 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -12,7 +12,7 @@ import { AuthenticationOrchestrator, ManagementApiOrchestrator, } from '../../../core/services'; -import { HttpClient } from '../../../core/services/HttpClient'; +import { HttpClient, TokenType } from '../../../core/services/HttpClient'; import { AuthError, DPoPError } from '../../../core/models'; export class WebAuth0Client implements IAuth0Client { @@ -21,6 +21,11 @@ export class WebAuth0Client implements IAuth0Client { readonly auth: AuthenticationOrchestrator; private readonly httpClient: HttpClient; + private readonly tokenType: TokenType; + private readonly baseUrl: string; + private readonly getDPoPHeadersForOrchestrator?: ( + params: DPoPHeadersParams + ) => Promise>; public readonly client: Auth0Client; private static spaClient: Auth0Client | null = null; @@ -51,6 +56,9 @@ export class WebAuth0Client implements IAuth0Client { constructor(options: WebAuth0Options) { const baseUrl = `https://${options.domain}`; + this.baseUrl = baseUrl; + const useDPoP = options.useDPoP ?? true; + this.tokenType = useDPoP ? TokenType.dpop : TokenType.bearer; this.httpClient = new HttpClient({ baseUrl: baseUrl, @@ -58,11 +66,6 @@ export class WebAuth0Client implements IAuth0Client { headers: options.headers, }); - this.auth = new AuthenticationOrchestrator({ - clientId: options.clientId, - httpClient: this.httpClient, - }); - const clientOptions: Auth0ClientOptions = { domain: options.domain, clientId: options.clientId, @@ -83,6 +86,22 @@ export class WebAuth0Client implements IAuth0Client { const client = WebAuth0Client.getSpaClient(clientOptions); this.client = client; + // Create a bound getDPoPHeaders function for the orchestrator + const getDPoPHeadersForOrchestrator = async (params: DPoPHeadersParams) => { + return this.getDPoPHeaders(params); + }; + this.getDPoPHeadersForOrchestrator = useDPoP + ? getDPoPHeadersForOrchestrator + : undefined; + + this.auth = new AuthenticationOrchestrator({ + clientId: options.clientId, + httpClient: this.httpClient, + tokenType: this.tokenType, + baseUrl: baseUrl, + getDPoPHeaders: useDPoP ? getDPoPHeadersForOrchestrator : undefined, + }); + this.webAuth = new WebWebAuthProvider(this.client); this.credentialsManager = new WebCredentialsManager(this.client); } @@ -91,6 +110,9 @@ export class WebAuth0Client implements IAuth0Client { return new ManagementApiOrchestrator({ token: token, httpClient: this.httpClient, + tokenType: this.tokenType, + baseUrl: this.baseUrl, + getDPoPHeaders: this.getDPoPHeadersForOrchestrator, }); } diff --git a/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts b/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts index c50a202e..8c9088e6 100644 --- a/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts +++ b/src/platforms/web/adapters/__tests__/WebAuth0Client.spec.ts @@ -116,10 +116,14 @@ describe('WebAuth0Client', () => { headers: undefined, }); - expect(MockAuthenticationOrchestrator).toHaveBeenCalledWith({ - clientId: defaultOptions.clientId, - httpClient: mockHttpClient, - }); + expect(MockAuthenticationOrchestrator).toHaveBeenCalledWith( + expect.objectContaining({ + clientId: defaultOptions.clientId, + httpClient: mockHttpClient, + tokenType: 'DPoP', + baseUrl: `https://${defaultOptions.domain}`, + }) + ); expect(MockWebWebAuthProvider).toHaveBeenCalledWith(mockSpaClient); expect(MockWebCredentialsManager).toHaveBeenCalledWith(mockSpaClient); @@ -182,6 +186,9 @@ describe('WebAuth0Client', () => { expect(MockManagementApiOrchestrator).toHaveBeenCalledWith({ token, httpClient: mockHttpClient, + tokenType: 'DPoP', + baseUrl: `https://${defaultOptions.domain}`, + getDPoPHeaders: expect.any(Function), }); expect(usersClient).toBeDefined(); }); @@ -199,10 +206,16 @@ describe('WebAuth0Client', () => { expect(MockManagementApiOrchestrator).toHaveBeenNthCalledWith(1, { token: token1, httpClient: mockHttpClient, + tokenType: 'DPoP', + baseUrl: `https://${defaultOptions.domain}`, + getDPoPHeaders: expect.any(Function), }); expect(MockManagementApiOrchestrator).toHaveBeenNthCalledWith(2, { token: token2, httpClient: mockHttpClient, + tokenType: 'DPoP', + baseUrl: `https://${defaultOptions.domain}`, + getDPoPHeaders: expect.any(Function), }); }); }); From c5ca94b78b3630428c0d1ba9cc0848a65d1d0b43 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Thu, 4 Dec 2025 00:59:14 +0530 Subject: [PATCH 3/6] allow to pass tokentype --- src/Auth0.ts | 6 ++++-- src/core/interfaces/IAuth0Client.ts | 3 ++- src/core/services/AuthenticationOrchestrator.ts | 7 +++++-- src/index.ts | 1 + src/platforms/native/adapters/NativeAuth0Client.ts | 14 +++++++++++--- src/platforms/web/adapters/WebAuth0Client.ts | 14 +++++++++++--- src/types/parameters.ts | 5 +++++ 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Auth0.ts b/src/Auth0.ts index b9ca90b7..b2ec2ef4 100644 --- a/src/Auth0.ts +++ b/src/Auth0.ts @@ -57,9 +57,11 @@ class Auth0 { /** * Provides access to the Management API (e.g., for user patching). + * @param token An access token with the required permissions for the management operations. + * @param tokenType Optional token type ('Bearer' or 'DPoP'). Defaults to the client's configured token type. */ - users(token: string) { - return this.client.users(token); + users(token: string, tokenType?: string) { + return this.client.users(token, tokenType); } /** diff --git a/src/core/interfaces/IAuth0Client.ts b/src/core/interfaces/IAuth0Client.ts index cf465dcc..247e8479 100644 --- a/src/core/interfaces/IAuth0Client.ts +++ b/src/core/interfaces/IAuth0Client.ts @@ -31,9 +31,10 @@ export interface IAuth0Client { * Creates a client for interacting with the Auth0 Management API's user endpoints. * * @param token An access token with the required permissions for the management operations. + * @param tokenType Optional token type ('Bearer' or 'DPoP'). Defaults to the client's configured token type. * @returns An `IUsersClient` instance configured with the provided token. */ - users(token: string): IUsersClient; + users(token: string, tokenType?: string): IUsersClient; /** * Generates DPoP headers for making authenticated requests to custom APIs. diff --git a/src/core/services/AuthenticationOrchestrator.ts b/src/core/services/AuthenticationOrchestrator.ts index e29167e7..6a060886 100644 --- a/src/core/services/AuthenticationOrchestrator.ts +++ b/src/core/services/AuthenticationOrchestrator.ts @@ -392,12 +392,15 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider { } async userInfo(parameters: UserInfoParameters): Promise { - const { token, headers } = parameters; + const { token, tokenType: paramTokenType, headers } = parameters; + + // Use parameter tokenType if provided, otherwise use client's default + const effectiveTokenType = (paramTokenType as TokenType) ?? this.tokenType; let authHeader: Record; // For DPoP tokens, we need to generate a DPoP proof using the native layer - if (this.tokenType === TokenType.dpop && this.getDPoPHeaders) { + if (effectiveTokenType === TokenType.dpop && this.getDPoPHeaders) { const userInfoUrl = `${this.baseUrl}/userinfo`; authHeader = await this.getDPoPHeaders({ url: userInfoUrl, diff --git a/src/index.ts b/src/index.ts index ed873e40..64db4010 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export { DPoPError, } from './core/models'; export { TimeoutError } from './core/utils/fetchWithTimeout'; +export { TokenType } from './core/services/HttpClient'; export { Auth0Provider } from './hooks/Auth0Provider'; export { useAuth0 } from './hooks/useAuth0'; export * from './types'; diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index 489747e0..3d32652b 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -94,13 +94,21 @@ export class NativeAuth0Client implements IAuth0Client { } } - users(token: string): IUsersClient { + users(token: string, tokenType?: string): IUsersClient { + // Use provided tokenType or fall back to client's default + const effectiveTokenType = (tokenType as TokenType) ?? this.tokenType; + // Only provide getDPoPHeaders if the effective token type is DPoP + const getDPoPHeaders = + effectiveTokenType === TokenTypeEnum.dpop + ? this.getDPoPHeadersForOrchestrator + : undefined; + return new ManagementApiOrchestrator({ token: token, httpClient: this.httpClient, - tokenType: this.tokenType, + tokenType: effectiveTokenType, baseUrl: this.baseUrl, - getDPoPHeaders: this.getDPoPHeadersForOrchestrator, + getDPoPHeaders, }); } diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index 3657ce53..b2585464 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -106,13 +106,21 @@ export class WebAuth0Client implements IAuth0Client { this.credentialsManager = new WebCredentialsManager(this.client); } - users(token: string): IUsersClient { + users(token: string, tokenType?: string): IUsersClient { + // Use provided tokenType or fall back to client's default + const effectiveTokenType = (tokenType as TokenType) ?? this.tokenType; + // Only provide getDPoPHeaders if the effective token type is DPoP + const getDPoPHeaders = + effectiveTokenType === TokenType.dpop + ? this.getDPoPHeadersForOrchestrator + : undefined; + return new ManagementApiOrchestrator({ token: token, httpClient: this.httpClient, - tokenType: this.tokenType, + tokenType: effectiveTokenType, baseUrl: this.baseUrl, - getDPoPHeaders: this.getDPoPHeadersForOrchestrator, + getDPoPHeaders, }); } diff --git a/src/types/parameters.ts b/src/types/parameters.ts index 8aef8fc8..57c40f70 100644 --- a/src/types/parameters.ts +++ b/src/types/parameters.ts @@ -214,6 +214,11 @@ export interface MfaChallengeParameters extends RequestOptions { /** Parameters for accessing the `/userinfo` endpoint. */ export interface UserInfoParameters extends RequestOptions { token: string; + /** + * The type of the token. When 'DPoP', DPoP headers will be generated automatically. + * Defaults to the client's configured token type. + */ + tokenType?: string; } /** Parameters for requesting a password reset email. */ From 1bb31e7727630740cde44def942eb7a167f56188 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Thu, 4 Dec 2025 12:24:26 +0530 Subject: [PATCH 4/6] feat: add DPoPHeadersProvider type and integrate into Authentication and Management API orchestrators --- .../services/AuthenticationOrchestrator.ts | 19 ++++++------------- src/core/services/HttpClient.ts | 11 +++++++++++ .../services/ManagementApiOrchestrator.ts | 18 ++++++------------ 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/core/services/AuthenticationOrchestrator.ts b/src/core/services/AuthenticationOrchestrator.ts index 6a060886..3203563f 100644 --- a/src/core/services/AuthenticationOrchestrator.ts +++ b/src/core/services/AuthenticationOrchestrator.ts @@ -29,7 +29,12 @@ import { AuthError, } from '../models'; import { validateParameters } from '../utils/validation'; -import { HttpClient, getBearerHeader, TokenType } from './HttpClient'; +import { + HttpClient, + getBearerHeader, + TokenType, + type DPoPHeadersProvider, +} from './HttpClient'; import { deepCamelCase } from '../utils'; // Represents the raw user profile returned by an API (snake_case) @@ -59,18 +64,6 @@ function includeRequiredScope(scope?: string): string { return scope; } -/** - * Function type for getting DPoP headers from the native/platform layer. - * This allows the orchestrator to request DPoP proof generation when needed. - */ -export type DPoPHeadersProvider = (params: { - url: string; - method: string; - accessToken: string; - tokenType: string; - nonce?: string; -}) => Promise>; - /** * Orchestrates all direct authentication flows by making calls to the Auth0 Authentication API. * This class is platform-agnostic and relies on an injected HttpClient. diff --git a/src/core/services/HttpClient.ts b/src/core/services/HttpClient.ts index 37caa7eb..00718f58 100644 --- a/src/core/services/HttpClient.ts +++ b/src/core/services/HttpClient.ts @@ -4,6 +4,17 @@ import { AuthError } from '../models'; import base64 from 'base-64'; import { telemetry } from '../utils/telemetry'; +/** + * Function type for getting DPoP headers from the native/platform layer. + */ +export type DPoPHeadersProvider = (params: { + url: string; + method: string; + accessToken: string; + tokenType: string; + nonce?: string; +}) => Promise>; + /** * Token type representing the type of token to use for authentication. */ diff --git a/src/core/services/ManagementApiOrchestrator.ts b/src/core/services/ManagementApiOrchestrator.ts index cc397a94..14888631 100644 --- a/src/core/services/ManagementApiOrchestrator.ts +++ b/src/core/services/ManagementApiOrchestrator.ts @@ -1,20 +1,14 @@ import type { IUsersClient } from '../interfaces/IUsersClient'; import type { GetUserParameters, PatchUserParameters, User } from '../../types'; import { Auth0User, AuthError } from '../models'; -import { HttpClient, getBearerHeader, TokenType } from '../services/HttpClient'; +import { + HttpClient, + getBearerHeader, + TokenType, + type DPoPHeadersProvider, +} from '../services/HttpClient'; import { deepCamelCase } from '../utils'; -/** - * Function type for getting DPoP headers from the native/platform layer. - */ -export type DPoPHeadersProvider = (params: { - url: string; - method: string; - accessToken: string; - tokenType: string; - nonce?: string; -}) => Promise>; - /** * Orchestrates interactions with the Auth0 Management API's user endpoints. */ From e7f5fa0ac5defaa2a915246c2ccdf6031929f673 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Thu, 4 Dec 2025 12:27:31 +0530 Subject: [PATCH 5/6] feat: enhance TokenType enum documentation for better clarity on access token types --- src/core/services/HttpClient.ts | 38 ++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/core/services/HttpClient.ts b/src/core/services/HttpClient.ts index 00718f58..d239868d 100644 --- a/src/core/services/HttpClient.ts +++ b/src/core/services/HttpClient.ts @@ -16,12 +16,44 @@ export type DPoPHeadersProvider = (params: { }) => Promise>; /** - * Token type representing the type of token to use for authentication. + * Represents the type of access token used for API authentication. + * + * This enum provides type-safe constants for token types returned by Auth0 + * and used when making authenticated API requests. + * + * @remarks + * - `TokenType.bearer` - Standard OAuth 2.0 Bearer token (default) + * - `TokenType.dpop` - Demonstrating Proof-of-Possession (DPoP) bound token + * + * @example + * ```typescript + * import { TokenType } from 'react-native-auth0'; + * + * // Check if credentials use DPoP + * if (credentials.tokenType === TokenType.dpop) { + * const headers = await auth0.getDPoPHeaders({ + * url: 'https://api.example.com/data', + * method: 'GET', + * accessToken: credentials.accessToken, + * tokenType: credentials.tokenType + * }); + * } + * ``` + * + * @public */ export enum TokenType { - /** Standard Bearer token authentication (default) */ + /** + * Standard OAuth 2.0 Bearer token authentication. + * This is the default token type used by most OAuth 2.0 implementations. + */ bearer = 'Bearer', - /** Demonstrating Proof-of-Possession token authentication */ + /** + * Demonstrating Proof-of-Possession (DPoP) token authentication. + * DPoP tokens are sender-constrained, providing additional security + * by cryptographically binding the token to the client. + * @see {@link https://datatracker.ietf.org/doc/html/rfc9449 | RFC 9449} + */ dpop = 'DPoP', } From 3ae41c7a56d81b691974eade74f3062e9ffcba0a Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Fri, 5 Dec 2025 18:49:29 +0530 Subject: [PATCH 6/6] feat: update tokenType parameter to use TokenType type across Auth0 interfaces and implementations --- src/Auth0.ts | 3 +- src/core/interfaces/IAuth0Client.ts | 4 +- .../services/AuthenticationOrchestrator.ts | 4 +- src/core/services/HttpClient.ts | 43 +------------------ .../services/ManagementApiOrchestrator.ts | 8 +++- src/index.ts | 2 +- .../native/adapters/NativeAuth0Client.ts | 13 +++--- src/platforms/web/adapters/WebAuth0Client.ts | 7 +-- src/types/common.ts | 42 ++++++++++++++++++ src/types/parameters.ts | 4 +- 10 files changed, 68 insertions(+), 62 deletions(-) diff --git a/src/Auth0.ts b/src/Auth0.ts index b2ec2ef4..d9276368 100644 --- a/src/Auth0.ts +++ b/src/Auth0.ts @@ -1,4 +1,5 @@ import type { IAuth0Client } from './core/interfaces/IAuth0Client'; +import type { TokenType } from './types/common'; import { Auth0ClientFactory } from './factory/Auth0ClientFactory'; import type { Auth0Options, DPoPHeadersParams } from './types'; @@ -60,7 +61,7 @@ class Auth0 { * @param token An access token with the required permissions for the management operations. * @param tokenType Optional token type ('Bearer' or 'DPoP'). Defaults to the client's configured token type. */ - users(token: string, tokenType?: string) { + users(token: string, tokenType?: TokenType) { return this.client.users(token, tokenType); } diff --git a/src/core/interfaces/IAuth0Client.ts b/src/core/interfaces/IAuth0Client.ts index 247e8479..db42feab 100644 --- a/src/core/interfaces/IAuth0Client.ts +++ b/src/core/interfaces/IAuth0Client.ts @@ -2,7 +2,7 @@ import type { IWebAuthProvider } from './IWebAuthProvider'; import type { ICredentialsManager } from './ICredentialsManager'; import type { IAuthenticationProvider } from './IAuthenticationProvider'; import type { IUsersClient } from './IUsersClient'; -import type { DPoPHeadersParams } from '../../types'; +import type { DPoPHeadersParams, TokenType } from '../../types'; /** * The primary interface for the Auth0 client. @@ -34,7 +34,7 @@ export interface IAuth0Client { * @param tokenType Optional token type ('Bearer' or 'DPoP'). Defaults to the client's configured token type. * @returns An `IUsersClient` instance configured with the provided token. */ - users(token: string, tokenType?: string): IUsersClient; + users(token: string, tokenType?: TokenType): IUsersClient; /** * Generates DPoP headers for making authenticated requests to custom APIs. diff --git a/src/core/services/AuthenticationOrchestrator.ts b/src/core/services/AuthenticationOrchestrator.ts index 3203563f..041ad2f2 100644 --- a/src/core/services/AuthenticationOrchestrator.ts +++ b/src/core/services/AuthenticationOrchestrator.ts @@ -32,9 +32,9 @@ import { validateParameters } from '../utils/validation'; import { HttpClient, getBearerHeader, - TokenType, type DPoPHeadersProvider, } from './HttpClient'; +import { TokenType } from '../../types/common'; import { deepCamelCase } from '../utils'; // Represents the raw user profile returned by an API (snake_case) @@ -388,7 +388,7 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider { const { token, tokenType: paramTokenType, headers } = parameters; // Use parameter tokenType if provided, otherwise use client's default - const effectiveTokenType = (paramTokenType as TokenType) ?? this.tokenType; + const effectiveTokenType = paramTokenType ?? this.tokenType; let authHeader: Record; diff --git a/src/core/services/HttpClient.ts b/src/core/services/HttpClient.ts index d239868d..6701d86c 100644 --- a/src/core/services/HttpClient.ts +++ b/src/core/services/HttpClient.ts @@ -1,6 +1,7 @@ import { fetchWithTimeout, TimeoutError } from '../utils/fetchWithTimeout'; import { toUrlQueryParams } from '../utils'; import { AuthError } from '../models'; +import { TokenType } from '../../types/common'; import base64 from 'base-64'; import { telemetry } from '../utils/telemetry'; @@ -15,48 +16,6 @@ export type DPoPHeadersProvider = (params: { nonce?: string; }) => Promise>; -/** - * Represents the type of access token used for API authentication. - * - * This enum provides type-safe constants for token types returned by Auth0 - * and used when making authenticated API requests. - * - * @remarks - * - `TokenType.bearer` - Standard OAuth 2.0 Bearer token (default) - * - `TokenType.dpop` - Demonstrating Proof-of-Possession (DPoP) bound token - * - * @example - * ```typescript - * import { TokenType } from 'react-native-auth0'; - * - * // Check if credentials use DPoP - * if (credentials.tokenType === TokenType.dpop) { - * const headers = await auth0.getDPoPHeaders({ - * url: 'https://api.example.com/data', - * method: 'GET', - * accessToken: credentials.accessToken, - * tokenType: credentials.tokenType - * }); - * } - * ``` - * - * @public - */ -export enum TokenType { - /** - * Standard OAuth 2.0 Bearer token authentication. - * This is the default token type used by most OAuth 2.0 implementations. - */ - bearer = 'Bearer', - /** - * Demonstrating Proof-of-Possession (DPoP) token authentication. - * DPoP tokens are sender-constrained, providing additional security - * by cryptographically binding the token to the client. - * @see {@link https://datatracker.ietf.org/doc/html/rfc9449 | RFC 9449} - */ - dpop = 'DPoP', -} - /** * Returns the Bearer authentication header. * @param token - The token value diff --git a/src/core/services/ManagementApiOrchestrator.ts b/src/core/services/ManagementApiOrchestrator.ts index 14888631..4e96b96c 100644 --- a/src/core/services/ManagementApiOrchestrator.ts +++ b/src/core/services/ManagementApiOrchestrator.ts @@ -1,10 +1,14 @@ import type { IUsersClient } from '../interfaces/IUsersClient'; -import type { GetUserParameters, PatchUserParameters, User } from '../../types'; +import { + TokenType, + type GetUserParameters, + type PatchUserParameters, + type User, +} from '../../types'; import { Auth0User, AuthError } from '../models'; import { HttpClient, getBearerHeader, - TokenType, type DPoPHeadersProvider, } from '../services/HttpClient'; import { deepCamelCase } from '../utils'; diff --git a/src/index.ts b/src/index.ts index 64db4010..442d3c5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export { DPoPError, } from './core/models'; export { TimeoutError } from './core/utils/fetchWithTimeout'; -export { TokenType } from './core/services/HttpClient'; +export { TokenType } from './types/common'; export { Auth0Provider } from './hooks/Auth0Provider'; export { useAuth0 } from './hooks/useAuth0'; export * from './types'; diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index 3d32652b..91803ecc 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -13,10 +13,7 @@ import { ManagementApiOrchestrator, } from '../../../core/services'; import { HttpClient } from '../../../core/services/HttpClient'; -import { - type TokenType, - TokenType as TokenTypeEnum, -} from '../../../core/services/HttpClient'; +import { TokenType } from '../../../types/common'; import { AuthError, DPoPError } from '../../../core/models'; export class NativeAuth0Client implements IAuth0Client { @@ -36,7 +33,7 @@ export class NativeAuth0Client implements IAuth0Client { const baseUrl = `https://${options.domain}`; this.baseUrl = baseUrl; const useDPoP = options.useDPoP ?? true; - this.tokenType = useDPoP ? TokenTypeEnum.dpop : TokenTypeEnum.bearer; + this.tokenType = useDPoP ? TokenType.dpop : TokenType.bearer; this.httpClient = new HttpClient({ baseUrl: baseUrl, @@ -94,12 +91,12 @@ export class NativeAuth0Client implements IAuth0Client { } } - users(token: string, tokenType?: string): IUsersClient { + users(token: string, tokenType?: TokenType): IUsersClient { // Use provided tokenType or fall back to client's default - const effectiveTokenType = (tokenType as TokenType) ?? this.tokenType; + const effectiveTokenType = tokenType ?? this.tokenType; // Only provide getDPoPHeaders if the effective token type is DPoP const getDPoPHeaders = - effectiveTokenType === TokenTypeEnum.dpop + effectiveTokenType === TokenType.dpop ? this.getDPoPHeadersForOrchestrator : undefined; diff --git a/src/platforms/web/adapters/WebAuth0Client.ts b/src/platforms/web/adapters/WebAuth0Client.ts index b2585464..394b3fbc 100644 --- a/src/platforms/web/adapters/WebAuth0Client.ts +++ b/src/platforms/web/adapters/WebAuth0Client.ts @@ -12,7 +12,8 @@ import { AuthenticationOrchestrator, ManagementApiOrchestrator, } from '../../../core/services'; -import { HttpClient, TokenType } from '../../../core/services/HttpClient'; +import { HttpClient } from '../../../core/services/HttpClient'; +import { TokenType } from '../../../types/common'; import { AuthError, DPoPError } from '../../../core/models'; export class WebAuth0Client implements IAuth0Client { @@ -106,9 +107,9 @@ export class WebAuth0Client implements IAuth0Client { this.credentialsManager = new WebCredentialsManager(this.client); } - users(token: string, tokenType?: string): IUsersClient { + users(token: string, tokenType?: TokenType): IUsersClient { // Use provided tokenType or fall back to client's default - const effectiveTokenType = (tokenType as TokenType) ?? this.tokenType; + const effectiveTokenType = tokenType ?? this.tokenType; // Only provide getDPoPHeaders if the effective token type is DPoP const getDPoPHeaders = effectiveTokenType === TokenType.dpop diff --git a/src/types/common.ts b/src/types/common.ts index 0a62681b..512a0452 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -143,6 +143,48 @@ export type MfaChallengeResponse = // ========= DPoP Types ========= +/** + * Represents the type of access token used for API authentication. + * + * This enum provides type-safe constants for token types returned by Auth0 + * and used when making authenticated API requests. + * + * @remarks + * - `TokenType.bearer` - Standard OAuth 2.0 Bearer token (default) + * - `TokenType.dpop` - Demonstrating Proof-of-Possession (DPoP) bound token + * + * @example + * ```typescript + * import { TokenType } from 'react-native-auth0'; + * + * // Check if credentials use DPoP + * if (credentials.tokenType === TokenType.dpop) { + * const headers = await auth0.getDPoPHeaders({ + * url: 'https://api.example.com/data', + * method: 'GET', + * accessToken: credentials.accessToken, + * tokenType: credentials.tokenType + * }); + * } + * ``` + * + * @public + */ +export enum TokenType { + /** + * Standard OAuth 2.0 Bearer token authentication. + * This is the default token type used by most OAuth 2.0 implementations. + */ + bearer = 'Bearer', + /** + * Demonstrating Proof-of-Possession (DPoP) token authentication. + * DPoP tokens are sender-constrained, providing additional security + * by cryptographically binding the token to the client. + * @see {@link https://datatracker.ietf.org/doc/html/rfc9449 | RFC 9449} + */ + dpop = 'DPoP', +} + /** * Parameters required to generate DPoP headers for custom API requests. * These headers cryptographically bind the access token to the specific HTTP request. diff --git a/src/types/parameters.ts b/src/types/parameters.ts index 57c40f70..05a35956 100644 --- a/src/types/parameters.ts +++ b/src/types/parameters.ts @@ -1,3 +1,5 @@ +import type { TokenType } from './common'; + /** A base interface for API calls that allow passing custom headers. * @hidden */ @@ -218,7 +220,7 @@ export interface UserInfoParameters extends RequestOptions { * The type of the token. When 'DPoP', DPoP headers will be generated automatically. * Defaults to the client's configured token type. */ - tokenType?: string; + tokenType?: TokenType; } /** Parameters for requesting a password reset email. */