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;
}