diff --git a/EXAMPLES.md b/EXAMPLES.md index a3ef634e..c8e99f17 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -9,6 +9,10 @@ - [Login with Passwordless](#login-with-passwordless) - [Create user in database connection](#create-user-in-database-connection) - [Using HTTPS callback URLs](#using-https-callback-urls) + - [Using Custom Headers](#using-custom-headers) + - [Set global headers during initialization](#set-global-headers-during-initialization) + - [Using custom headers with Auth0Provider component](#using-custom-headers-with-auth0provider-component) + - [Set request-specific headers](#set-request-specific-headers) - [Management API (Users)](#management-api-users) - [Patch user with user_metadata](#patch-user-with-user_metadata) - [Get full user profile](#get-full-user-profile) @@ -171,6 +175,67 @@ auth0.webAuth .catch((error) => console.log(error)); ``` +### Using Custom Headers + +You can set custom headers to be included in all requests to the Auth0 API. This can be useful for implementing custom security requirements, logging, or tracking. + +#### Set global headers during initialization + +Global headers are included in all requests made by the SDK: + +```js +// Set global headers during Auth0 initialization +const auth0 = new Auth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + headers: { + 'Accept-Language': 'fr-CA', + 'X-Tracking-Id': 'user-tracking-id-123' + } +}); +``` + + +#### Using custom headers with Auth0Provider component + +If you're using the hooks-based approach with Auth0Provider, you can provide headers during initialization: + +```jsx +import { Auth0Provider } from 'react-native-auth0'; + +// In your app component + + + +``` + + +#### Set request-specific headers + +You can also provide headers for specific API calls, which will override global headers with the same name: + +```js +// For specific authentication requests +auth0.auth.passwordRealm({ + username: 'info@auth0.com', + password: 'password', + realm: 'myconnection', + headers: { + 'X-Custom-Header': 'request-specific-value', + 'X-Request-ID': 'unique-request-id-456' + } +}) +.then(console.log) +.catch(console.error); +``` + ## Management API (Users) ### Patch user with user_metadata diff --git a/README.md b/README.md index 4e94ba7a..80cef45b 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,26 @@ const App = () => { export default App; ``` +You can also pass custom headers that will be included in all API requests: + +```js +import { Auth0Provider } from 'react-native-auth0'; + +const App = () => { + return ( + + {/* YOUR APP */} + + ); +}; + +export default App; +``` +
Using the `Auth0` class @@ -360,6 +380,19 @@ const auth0 = new Auth0({ }); ``` +You can also pass custom headers that will be included in all API requests: + +```js +import Auth0 from 'react-native-auth0'; + +const auth0 = new Auth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + headers: { + 'X-Custom-Header': 'custom-value', + } +}); +```
Then import the hook into a component where you want to get access to the properties and methods for integrating with Auth0: diff --git a/src/auth/__tests__/index.spec.js b/src/auth/__tests__/index.spec.js index 2666e1fd..681a459b 100644 --- a/src/auth/__tests__/index.spec.js +++ b/src/auth/__tests__/index.spec.js @@ -57,6 +57,12 @@ describe('auth', () => { it('should fail without domain', () => { expect(() => new Auth({ clientId })).toThrowErrorMatchingSnapshot(); }); + + it('should accept custom headers', () => { + const headers = { 'X-Custom-Header': 'custom-value' }; + const auth = new Auth({ baseUrl, clientId, headers }); + expect(auth.client.globalHeaders).toEqual(headers); + }); }); describe('authorizeUrl', () => { @@ -1053,4 +1059,52 @@ describe('auth', () => { ).resolves.toMatchSnapshot(); }); }); + + describe('method-specific custom headers', () => { + it('should accept and use custom headers in passwordRealm that don\'t conflict with defaults', async () => { + fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); + const customHeaders = { 'X-Custom-Header': 'custom-value' }; + + await auth.passwordRealm({ + username: 'info@auth0.com', + password: 'secret pass', + realm: 'Username-Password-Authentication', + headers: customHeaders + }); + + const [_, fetchOptions] = fetchMock.lastCall(); + expect(fetchOptions.headers.get('X-Custom-Header')).toBe('custom-value'); + }); + + it('should accept and use custom headers in userInfo that don\'t conflict with defaults', async () => { + const success = { + status: 200, + body: { sub: 'auth0|1029837475' }, + headers: { 'Content-Type': 'application/json' }, + }; + fetchMock.getOnce('https://samples.auth0.com/userinfo', success); + const customHeaders = { 'X-Custom-Header': 'custom-value' }; + + await auth.userInfo({ + token: 'an access token of a user', + headers: customHeaders + }); + + const [_, fetchOptions] = fetchMock.lastCall(); + expect(fetchOptions.headers.get('X-Custom-Header')).toBe('custom-value'); + }); + + it('should accept and use custom headers in refreshToken that don\'t conflict with defaults', async () => { + fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); + const customHeaders = { 'X-Custom-Header': 'custom-value' }; + + await auth.refreshToken({ + refreshToken: 'a refresh token of a user', + headers: customHeaders + }); + + const [_, fetchOptions] = fetchMock.lastCall(); + expect(fetchOptions.headers.get('X-Custom-Header')).toBe('custom-value'); + }); + }); }); diff --git a/src/auth/index.ts b/src/auth/index.ts index bf31eda0..1bcedcf4 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -82,6 +82,7 @@ class Auth { telemetry?: Telemetry; token?: string; timeout?: number; + headers?: Record; }) { this.client = new Client(options); this.domain = this.client.domain; @@ -147,6 +148,7 @@ class Auth { * @see https://auth0.com/docs/api-auth/grant/authorization-code-pkce */ exchange(parameters: ExchangeOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -155,14 +157,14 @@ class Auth { redirectUri: { required: true, toName: 'redirect_uri' }, }, }, - parameters + payloadParams ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'authorization_code', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -179,6 +181,7 @@ class Auth { exchangeNativeSocial( parameters: ExchangeNativeSocialOptions ): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -189,14 +192,14 @@ class Auth { scope: { required: false }, }, }, - parameters + payloadParams ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', - }) + }, headers) .then((response) => { return convertTimestampInCredentials( responseHandler(response) @@ -211,12 +214,13 @@ class Auth { * @see https://auth0.com/docs/api-auth/grant/password#realm-support */ passwordRealm(parameters: PasswordRealmOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; return this.client .post('/oauth/token', { - ...parameters, + ...payloadParams, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/password-realm', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -231,6 +235,7 @@ class Auth { * @see https://auth0.com/docs/tokens/refresh-token/current#use-a-refresh-token */ refreshToken(parameters: RefreshTokenOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -239,14 +244,14 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'refresh_token', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -262,12 +267,13 @@ class Auth { passwordlessWithEmail( parameters: PasswordlessWithEmailOptions ): Promise { + const { headers, ...payloadParams } = parameters || {}; return this.client .post('/passwordless/start', { - ...parameters, + ...payloadParams, connection: 'email', client_id: this.clientId, - }) + }, headers) .then((response) => responseHandler(response)); } @@ -277,6 +283,7 @@ class Auth { * This should be completed later using a call to {@link loginWithSMS}, passing the OTP that was sent to the user. */ passwordlessWithSMS(parameters: PasswordlessWithSMSOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -286,14 +293,14 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client .post('/passwordless/start', { ...payload, connection: 'sms', client_id: this.clientId, - }) + }, headers) .then((response) => responseHandler(response)); } @@ -303,6 +310,7 @@ class Auth { * @returns A populated instance of {@link Credentials}. */ loginWithEmail(parameters: LoginWithEmailOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -313,7 +321,7 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client .post('/oauth/token', { @@ -321,7 +329,7 @@ class Auth { client_id: this.clientId, realm: 'email', grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -335,6 +343,7 @@ class Auth { * @returns A populated instance of {@link Credentials}. */ loginWithSMS(parameters: LoginWithSMSOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -345,7 +354,7 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client .post('/oauth/token', { @@ -353,7 +362,7 @@ class Auth { client_id: this.clientId, realm: 'sms', grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -371,6 +380,7 @@ class Auth { * @returns A populated instance of {@link Credentials}. */ loginWithOTP(parameters: LoginWithOTPOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -379,14 +389,14 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/mfa-otp', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -404,6 +414,7 @@ class Auth { */ loginWithOOB(parameters: LoginWithOOBOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -413,7 +424,7 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client @@ -421,7 +432,7 @@ class Auth { ...payload, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/mfa-oob', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -440,6 +451,7 @@ class Auth { loginWithRecoveryCode( parameters: LoginWithRecoveryCodeOptions ): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -448,7 +460,7 @@ class Auth { }, whitelist: false, }, - parameters + payloadParams ); return this.client @@ -456,7 +468,7 @@ class Auth { ...payload, client_id: this.clientId, grant_type: 'http://auth0.com/oauth/grant-type/mfa-recovery-code', - }) + }, headers) .then((response) => convertTimestampInCredentials( responseHandler(response) @@ -474,6 +486,7 @@ class Auth { multifactorChallenge( parameters: MultifactorChallengeOptions ): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -482,13 +495,13 @@ class Auth { authenticatorId: { required: false, toName: 'authenticator_id' }, }, }, - parameters + payloadParams ); return this.client .post('/mfa/challenge', { ...payload, client_id: this.clientId, - }) + }, headers) .then((response) => responseHandler< RawMultifactorChallengeResponse, @@ -501,19 +514,20 @@ class Auth { * Revoke an issued refresh token */ revoke(parameters: RevokeOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { refreshToken: { required: true, toName: 'token' }, }, }, - parameters + payloadParams ); return this.client .post('/oauth/revoke', { ...payload, client_id: this.clientId, - }) + }, headers) .then((response) => { if (response.ok) { return; @@ -529,7 +543,8 @@ class Auth { */ userInfo(parameters: UserInfoOptions): Promise { const { baseUrl, telemetry } = this.client; - const client = new Client({ baseUrl, telemetry, token: parameters.token }); + const { token, headers } = parameters || {}; + const client = new Client({ baseUrl, telemetry, token: token }); const claims = [ 'sub', 'name', @@ -552,7 +567,7 @@ class Auth { 'address', 'updated_at', ]; - return client.get('/userinfo').then((response) => + return client.get('/userinfo', undefined, headers).then((response) => responseHandler(response, { attributes: claims, whitelist: true, @@ -564,11 +579,12 @@ class Auth { * Request an email with instructions to change password of a user */ resetPassword(parameters: ResetPasswordOptions): Promise { + const { headers, ...payloadParams } = parameters || {}; return this.client .post('/dbconnections/change_password', { - ...parameters, + ...payloadParams, client_id: this.clientId, - }) + }, headers) .then((response) => { if (response.ok) { return; @@ -583,6 +599,7 @@ class Auth { * @returns An instance of {@link User}. */ createUser(parameters: CreateUserOptions): Promise> { + const { headers, ...payloadParams } = parameters || {}; const payload = apply( { parameters: { @@ -598,14 +615,14 @@ class Auth { metadata: { required: false, toName: 'user_metadata' }, }, }, - parameters + payloadParams ); return this.client .post>('/dbconnections/signup', { ...payload, client_id: this.clientId, - }) + }, headers) .then((response) => { if (response.ok && response.json) { return toCamelCase>(response.json); diff --git a/src/auth0.ts b/src/auth0.ts index 46038174..1c6a256e 100644 --- a/src/auth0.ts +++ b/src/auth0.ts @@ -1,10 +1,9 @@ import Auth from './auth'; import CredentialsManager from './credentials-manager'; import Users from './management/users'; -import type { Telemetry } from './networking/telemetry'; import WebAuth from './webauth'; -import type { LocalAuthenticationOptions } from './credentials-manager/localAuthenticationOptions'; import addDefaultLocalAuthOptions from './utils/addDefaultLocalAuthOptions'; +import { Auth0Options } from './types'; /** * Auth0 for React Native client @@ -13,8 +12,8 @@ class Auth0 { public auth: Auth; public webAuth: WebAuth; public credentialsManager: CredentialsManager; - private options; - + private options: Auth0Options; + private globalHeaders?: Record; /** * Creates an instance of Auth0. * @param {Object} options Your Auth0 application information @@ -25,19 +24,12 @@ class Auth0 { * @param {String} options.timeout Timeout to be set for requests. * @param {LocalAuthenticationOptions} options.localAuthenticationOptions The options for configuring the display of local authentication prompt, authentication level (Android only) and evaluation policy (iOS only). */ - constructor(options: { - domain: string; - clientId: string; - telemetry?: Telemetry; - token?: string; - timeout?: number; - localAuthenticationOptions?: LocalAuthenticationOptions; - }) { - const { domain, clientId, ...extras } = options; + constructor(options: Auth0Options) { + const { domain, clientId, headers, ...extras } = options; const localAuthenticationOptions = options.localAuthenticationOptions ? addDefaultLocalAuthOptions(options.localAuthenticationOptions) : undefined; - this.auth = new Auth({ baseUrl: domain, clientId, ...extras }); + this.auth = new Auth({ baseUrl: domain, clientId, headers, ...extras }); this.webAuth = new WebAuth(this.auth, localAuthenticationOptions); this.credentialsManager = new CredentialsManager( domain, @@ -45,6 +37,7 @@ class Auth0 { localAuthenticationOptions ); this.options = options; + this.globalHeaders = headers } /** @@ -54,7 +47,7 @@ class Auth0 { */ users(token: string) { const { domain, ...extras } = this.options; - return new Users({ baseUrl: domain, ...extras, token }); + return new Users({ baseUrl: domain, ...extras, token, headers: this.globalHeaders }); } } diff --git a/src/hooks/__tests__/use-auth0.spec.jsx b/src/hooks/__tests__/use-auth0.spec.jsx index e25b4223..1aa11099 100644 --- a/src/hooks/__tests__/use-auth0.spec.jsx +++ b/src/hooks/__tests__/use-auth0.spec.jsx @@ -1216,3 +1216,49 @@ describe('The useAuth0 hook', () => { ).toHaveBeenCalledWith(100); }); }); + +describe('The Auth0Provider component', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + mockAuth0.credentialsManager.hasValidCredentials.mockResolvedValue(false); + }); + + it('should pass custom headers to Auth0 client when provided', () => { + // Save the original mock implementation + const originalMockImplementation = require('../../auth0').mockImplementation; + + // Override the mock for this specific test + const mockAuth0Constructor = require('../../auth0'); + mockAuth0Constructor.mockImplementation((options) => { + // Capture the options passed to the Auth0 constructor + mockAuth0Constructor.mockOptions = options; + // Return the standard mockAuth0 object + return mockAuth0; + }); + + const customHeaders = { 'X-Custom-Header': 'custom-value' }; + + const customHeadersWrapper = ({ children }) => ( + + {children} + + ); + + renderHook(() => useAuth0(), { wrapper: customHeadersWrapper }); + + // Verify headers were passed correctly + expect(mockAuth0Constructor.mockOptions).toEqual( + expect.objectContaining({ + headers: customHeaders + }) + ); + + // Restore the original mock implementation + mockAuth0Constructor.mockImplementation(originalMockImplementation); + }); +}); diff --git a/src/hooks/auth0-provider.tsx b/src/hooks/auth0-provider.tsx index f5e26417..048e4e62 100644 --- a/src/hooks/auth0-provider.tsx +++ b/src/hooks/auth0-provider.tsx @@ -6,6 +6,7 @@ import Auth0Context from './auth0-context'; import Auth0 from '../auth0'; import reducer from './reducer'; import type { + Auth0Options, ClearSessionOptions, ClearSessionParameters, Credentials, @@ -27,7 +28,6 @@ import type { } from '../types'; import type { CustomJwtPayload } from '../internal-types'; import { convertUser } from '../utils/userConversion'; -import type { LocalAuthenticationOptions } from '../credentials-manager/localAuthenticationOptions'; import type BaseError from '../utils/baseError'; const initialState = { @@ -76,15 +76,11 @@ const Auth0Provider = ({ clientId, localAuthenticationOptions, timeout, + headers, children, -}: PropsWithChildren<{ - domain: string; - clientId: string; - localAuthenticationOptions?: LocalAuthenticationOptions; - timeout?: number; -}>) => { +}: PropsWithChildren) => { const client = useMemo( - () => new Auth0({ domain, clientId, localAuthenticationOptions, timeout }), + () => new Auth0({ domain, clientId, localAuthenticationOptions, timeout, headers }), [domain, clientId, localAuthenticationOptions, timeout] ); const [state, dispatch] = useReducer(reducer, initialState); @@ -446,6 +442,7 @@ Auth0Provider.propTypes = { domain: PropTypes.string.isRequired, clientId: PropTypes.string.isRequired, children: PropTypes.element.isRequired, + headers: PropTypes.object, }; export default Auth0Provider; diff --git a/src/management/users.ts b/src/management/users.ts index b5af6994..9082f3c6 100644 --- a/src/management/users.ts +++ b/src/management/users.ts @@ -47,6 +47,7 @@ class Users { telemetry?: Telemetry; token?: string; timeout?: number; + headers?: Record; }) { this.client = new Client(options); if (!options.token) { @@ -65,8 +66,9 @@ class Users { * @memberof Users */ getUser(parameters: GetUserOptions): Promise { + const { id, headers } = parameters || {}; return this.client - .get(`/api/v2/users/${encodeURIComponent(parameters.id)}`) + .get(`/api/v2/users/${encodeURIComponent(id)}`, undefined, headers) .then((response) => responseHandler(response, { attributes, @@ -88,10 +90,11 @@ class Users { * @memberof Users */ patchUser(parameters: PatchUserOptions): Promise { + const { id, headers } = parameters || {}; return this.client - .patch(`/api/v2/users/${encodeURIComponent(parameters.id)}`, { + .patch(`/api/v2/users/${encodeURIComponent(id)}`, { user_metadata: parameters.metadata, - }) + }, headers) .then((response) => responseHandler(response, { attributes, diff --git a/src/networking/__tests__/index.spec.js b/src/networking/__tests__/index.spec.js index 815e850d..c3041cc1 100644 --- a/src/networking/__tests__/index.spec.js +++ b/src/networking/__tests__/index.spec.js @@ -45,6 +45,12 @@ describe('client', () => { expect(client.bearer).toEqual('Bearer a.bearer.token'); }); + it('should accept global headers', () => { + const headers = { 'X-Custom-Header': 'custom-value' }; + const client = new Client({baseUrl, headers}); + expect(client.globalHeaders).toEqual(headers); + }); + it('should fail with no domain', () => { expect(() => new Client()).toThrowErrorMatchingSnapshot(); }); @@ -219,6 +225,57 @@ describe('client', () => { }); }); + describe('headers', () => { + const globalHeaders = { 'X-Global-Header': 'global-value' }; + const client = new Client({ + baseUrl, + telemetry: {name: 'react-native-auth0', version: '1.0.0'}, + token: 'a.bearer.token', + headers: globalHeaders + }); + + const response = { + body: { + key: 'value', + }, + headers: { + 'Content-Type': 'application/json', + }, + }; + + beforeEach(fetchMock.restore); + + it('should include global headers in request', async () => { + fetchMock.postOnce('https://samples.auth0.com/method', response); + expect.assertions(1); + await client.post('/method', {}); + const [_, fetchOptions] = fetchMock.lastCall(); + expect(fetchOptions.headers.get('X-Global-Header')).toBe('global-value'); + }); + + it('should allow request-specific headers that don\'t conflict with default headers', async () => { + fetchMock.postOnce('https://samples.auth0.com/method', response); + expect.assertions(1); + const requestHeaders = { + 'X-Request-Header': 'request-value' + }; + await client.post('/method', {}, requestHeaders); + const [_, fetchOptions] = fetchMock.lastCall(); + expect(fetchOptions.headers.get('X-Request-Header')).toBe('request-value'); + }); + + it('should not override default headers with custom headers', async () => { + fetchMock.postOnce('https://samples.auth0.com/method', response); + expect.assertions(1); + const requestHeaders = { + 'Content-Type': 'text/plain' // This should not override the default + }; + await client.post('/method', {}, requestHeaders); + const [_, fetchOptions] = fetchMock.lastCall(); + expect(fetchOptions.headers.get('Content-Type')).toBe('application/json'); + }); + }); + describe('url', () => { const client = new Client({ baseUrl, diff --git a/src/networking/index.ts b/src/networking/index.ts index bff45a31..1dc92aac 100644 --- a/src/networking/index.ts +++ b/src/networking/index.ts @@ -13,23 +13,27 @@ class Client { public domain: string; private bearer?: string; private timeout: number; + private globalHeaders: Record; constructor(options: { baseUrl: string; telemetry?: Telemetry; token?: string; timeout?: number; + headers?: Record; }) { const { baseUrl, telemetry = {}, token, timeout = 10000, + headers = {}, }: { baseUrl: string; telemetry?: Telemetry; token?: string; timeout?: number; + headers?: Record } = options; if (!baseUrl) { throw new Error('Missing Auth0 domain'); @@ -51,18 +55,19 @@ class Client { } this.timeout = timeout; + this.globalHeaders = headers; } - post(path: string, body: TBody) { - return this.request('POST', this.url(path), body); + post(path: string, body: TBody, headers?: Record) { + return this.request('POST', this.url(path), body, headers); } - patch(path: string, body: TBody) { - return this.request('PATCH', this.url(path), body); + patch(path: string, body: TBody, headers?: Record) { + return this.request('PATCH', this.url(path), body, headers); } - get(path: string, query?: unknown) { - return this.request('GET', this.url(path, query)); + get(path: string, query?: unknown, headers?: Record) { + return this.request('GET', this.url(path, query), undefined, headers); } url(path: string, query?: any, includeTelemetry: boolean = false) { @@ -81,22 +86,36 @@ class Client { request( method: 'GET' | 'POST' | 'PATCH', url: string, - body?: TBody + body?: TBody, + requestHeaders?: Record ): Promise> { const headers = new Headers(); + // Set default headers first headers.set('Accept', 'application/json'); headers.set('Content-Type', 'application/json'); headers.set('Auth0-Client', this._encodedTelemetry()); + if (this.bearer) { + headers.set('Authorization', this.bearer); + } + + // Combine global headers with request-specific headers + const finalHeaders = { ...this.globalHeaders, ...requestHeaders }; + + // Apply custom headers, but don't override headers that are already set + for (const key in finalHeaders) { + if (Object.prototype.hasOwnProperty.call(finalHeaders, key)) { + if (finalHeaders[key] !== undefined && !headers.has(key)) { + headers.set(key, finalHeaders[key] as string); + } + } + } + const options: RequestInit = { method, headers, }; - - if (this.bearer) { - headers.set('Authorization', this.bearer); - } if (body) { options.body = JSON.stringify(body); } diff --git a/src/types.ts b/src/types.ts index ee737fb6..b2498ee1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,6 @@ +import { LocalAuthenticationOptions } from "./credentials-manager/localAuthenticationOptions"; +import { Telemetry } from "./networking/telemetry"; + export type Credentials = { /** * A token in JWT format that has user claims @@ -191,12 +194,20 @@ export interface ClearSessionOptions { export interface GetUserOptions { id: string; [key: string]: any; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; } export interface PatchUserOptions { id: string; metadata: object; [key: string]: any; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; } /** @@ -289,9 +300,43 @@ export interface ExchangeNativeSocialOptions { * The scopes requested for the issued tokens. e.g. `openid profile` */ scope?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } +/** + * Options for configuring the Auth0 client. + */ +export interface Auth0Options { + /** + * The Auth0 domain for your tenant. + */ + domain: string; + /** + * The client identifier of your application. + */ + clientId: string; + /** + * Telemetry information to include in requests. + */ + telemetry?: Telemetry; + /** + * The timeout in milliseconds for network requests. + */ + timeout?: number; + /** + * Options for configuring local authentication. + */ + localAuthenticationOptions?: LocalAuthenticationOptions; + /** + * (Optional) Custom headers to include in requests. + */ + headers?: Record; +} + /** * Options for authenticating using the username & password grant. */ @@ -316,6 +361,10 @@ export interface PasswordRealmOptions { * The scopes requested for the issued tokens. e.g. `openid profile` */ scope?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -331,6 +380,10 @@ export interface RefreshTokenOptions { * The scopes requested for the issued tokens. e.g. `openid profile` */ scope?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -350,6 +403,10 @@ export interface PasswordlessWithEmailOptions { * Optional parameters, used when strategy is 'linkˁ' */ authParams?: object; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -369,6 +426,10 @@ export interface PasswordlessWithSMSOptions { * Optional passwordless parameters */ authParams?: object; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -392,6 +453,10 @@ export interface LoginWithEmailOptions { * The scopes to request */ scope?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -415,6 +480,10 @@ export interface LoginWithSMSOptions { * Optional scopes to request */ scope?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -435,6 +504,10 @@ export interface LoginWithOTPOptions { * The API audience */ audience?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -456,6 +529,10 @@ export interface LoginWithOOBOptions { * delivered as part of the challenge message. */ bindingCode?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -471,6 +548,10 @@ export interface LoginWithRecoveryCodeOptions { * The recovery code provided by the end-user. */ recoveryCode: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -492,6 +573,10 @@ export interface MultifactorChallengeOptions { * The ID of the authenticator to challenge. */ authenticatorId?: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -503,6 +588,10 @@ export interface RevokeOptions { * The user's issued refresh token */ refreshToken: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -514,6 +603,10 @@ export interface UserInfoOptions { * The user's access token */ token: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; } /** @@ -528,6 +621,10 @@ export interface ResetPasswordOptions { * The name of the database connection of the user */ connection: string; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; } @@ -575,6 +672,10 @@ export interface CreateUserOptions { * Additional information that will be stored in `user_metadata` */ metadata?: object; + /** + * (Optional) Custom headers to include in the request. + */ + headers?: Record; [key: string]: any; }