Skip to content

Commit 99e0895

Browse files
shenjCopilot
andauthored
Custom Auth: add requestInterceptor for custom x-* request headers (#8587)
## Summary Adds a new `requestInterceptor` option to `CustomAuthOptions` that lets applications attach additional `x-*` headers to every custom-auth backend request (sign-in, sign-up, reset-password, register). The primary use case is integrating with third-party fraud / bot-detection SDKs that require vendor-supplied headers on the auth endpoints. ## API ```ts import { CustomAuthRequestInterceptor } from "@azure/msal-browser/custom-auth"; const interceptor: CustomAuthRequestInterceptor = { addAdditionalHeaderFields(requestUrl: URL) { if (requestUrl.pathname.includes("/oauth2/v2.0/initiate")) { return { value_1: "customer_header_1", // Ignored: no "x-" prefix "x-client-header": "customer_header_2", // Ignored: reserved prefix "X-my-custom-header": "my data", // Sent }; } return null; }, }; const config: CustomAuthConfiguration = { auth: { clientId, authority }, customAuth: { authApiProxyUrl, requestInterceptor: interceptor, }, }; ``` The interceptor may return either a synchronous record or a `Promise`. ## Filter rules (same as iOS doc) * Names must start with `x-` (case-insensitive); others are dropped. * Reserved prefixes are dropped: `x-client-`, `x-ms-`, `x-broker-`, `x-app-`. * User headers that pass the filter take precedence over MSAL's common headers. * Exceptions thrown by the interceptor are captured and logged via `Logger.warningPii`; the request still goes through so user-supplied code cannot break authentication. ## Scope * Filtering applied only to custom-auth backend requests (anything routed through `BaseApiClient.request`). * Wiring: `CustomAuthStandardController` → `CustomAuthApiClient` → each sub-client (`SignInApiClient`, `SignupApiClient`, `ResetPasswordApiClient`, `RegisterApiClient`) → `BaseApiClient`. * No cache schema impact. ## Tests * `CustomHeaderUtils.spec.ts` — 16 cases covering each filter rule, casing, and edge cases. * `BaseApiClientInterceptor.spec.ts` — 8 cases covering: no-interceptor, URL passed to interceptor, filtering, precedence, sync return, async return, null return, sync throw, async rejection (all swallowed). * `CustomAuthApiClient.spec.ts` extended with 2 construction cases. * Full `lib/msal-browser` suite: **1643 tests pass**; lint, format, build, and api-extractor all clean. ## Changefile `change/@azure-msal-browser-ec2f3299-….json` — `minor` (new public option). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ce50f41 commit 99e0895

15 files changed

Lines changed: 900 additions & 15 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Custom Auth: add `requestInterceptor` configuration option that lets apps attach additional `x-*` headers to custom-auth backend requests (e.g., for fraud/bot-detection vendors). Headers without the `x-` prefix and headers starting with reserved prefixes (`x-client-`, `x-ms-`, `x-broker-`, `x-app-`) are filtered out. [#8587](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8587)",
4+
"packageName": "@azure/msal-browser",
5+
"email": "shen.jian@live.com",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/src/custom_auth/CustomAuthConstants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ export const HttpHeaderKeys = {
3333
X_MS_REQUEST_ID: "x-ms-request-id",
3434
} as const;
3535

36+
export const CustomHeaderConstants = {
37+
REQUIRED_PREFIX: "x-",
38+
RESERVED_PREFIXES: [
39+
"x-client-",
40+
"x-ms-",
41+
"x-broker-",
42+
"x-app-",
43+
] as ReadonlyArray<string>,
44+
} as const;
45+
3646
export const DefaultPackageInfo = {
3747
SKU: "msal.browser",
3848
VERSION: version,

lib/msal-browser/src/custom_auth/configuration/CustomAuthConfiguration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {
77
BrowserConfiguration,
88
Configuration,
99
} from "../../config/Configuration.js";
10+
import { CustomAuthRequestInterceptor } from "./CustomAuthRequestInterceptor.js";
1011

1112
export type CustomAuthOptions = {
1213
challengeTypes?: Array<string>;
1314
authApiProxyUrl: string;
1415
customAuthApiQueryParams?: Record<string, string>;
1516
capabilities?: Array<string>;
17+
requestInterceptor?: CustomAuthRequestInterceptor;
1618
};
1719

1820
export type CustomAuthConfiguration = Configuration & {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
/**
7+
* Result type returned by {@link CustomAuthRequestInterceptor.addAdditionalHeaderFields}.
8+
*
9+
* Implementations may return either a synchronous value or a `Promise` resolving to one of
10+
* the following:
11+
* - A `Record<string, string>` of additional headers to add to the outgoing request.
12+
* - `null` (or `undefined`) when no additional headers should be added for the request.
13+
*/
14+
export type CustomAuthAdditionalHeaderFieldsResult =
15+
| Record<string, string>
16+
| null
17+
| undefined;
18+
19+
/**
20+
* Interface for intercepting custom auth network requests in order to attach additional
21+
* headers to outgoing requests.
22+
*
23+
* Implementations are invoked by MSAL before each backend request used by custom auth
24+
* (sign-in, sign-up, reset-password, and register endpoints). Use this hook to integrate
25+
* with third-party fraud and bot-detection SDKs that require custom `x-*` headers.
26+
*
27+
* MSAL applies the following rules when evaluating the headers you provide:
28+
* - Headers must start with `x-` (case-insensitive). Headers that don't start with `x-`
29+
* are ignored.
30+
* - Headers that start with any of the following reserved prefixes are ignored:
31+
* `x-client-`, `x-ms-`, `x-broker-`, `x-app-`.
32+
* - Headers that pass both rules are added to the network request. If a header you
33+
* provide has the same name as one of MSAL's own internal headers, your value takes
34+
* precedence.
35+
*/
36+
export interface CustomAuthRequestInterceptor {
37+
/**
38+
* Returns additional headers to add to a custom-auth network request.
39+
*
40+
* Scope your headers to specific endpoints by inspecting `requestUrl`. Sending
41+
* headers to unrelated endpoints can degrade signal quality and increase false
42+
* positives for fraud/bot-detection vendors.
43+
*
44+
* @param requestUrl - The full URL of the outgoing custom-auth request.
45+
* @returns A record of headers to add (or `null`/`undefined` if no extra headers
46+
* are needed for the request). May be returned synchronously or as a
47+
* `Promise`.
48+
*/
49+
addAdditionalHeaderFields(
50+
requestUrl: URL
51+
):
52+
| CustomAuthAdditionalHeaderFieldsResult
53+
| Promise<CustomAuthAdditionalHeaderFieldsResult>;
54+
}

lib/msal-browser/src/custom_auth/controller/CustomAuthStandardController.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ export class CustomAuthStandardController
125125
this.customAuthConfig.auth.clientId,
126126
new FetchHttpClient(this.logger),
127127
this.customAuthConfig.customAuth?.capabilities?.join(" "),
128-
this.customAuthConfig.customAuth?.customAuthApiQueryParams
128+
this.customAuthConfig.customAuth?.customAuthApiQueryParams,
129+
this.customAuthConfig.customAuth?.requestInterceptor,
130+
this.logger
129131
),
130132
this.authority
131133
);

lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/BaseApiClient.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ import {
1717
} from "../../error/CustomAuthApiError.js";
1818
import {
1919
AADServerParamKeys,
20+
Logger,
2021
ServerTelemetryManager,
2122
} from "@azure/msal-common/browser";
2223
import { ApiErrorResponse } from "./types/ApiErrorResponseTypes.js";
24+
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
25+
import { filterCustomHeaders } from "../../utils/CustomHeaderUtils.js";
2326

2427
export abstract class BaseApiClient {
2528
private readonly baseRequestUrl: URL;
@@ -28,7 +31,9 @@ export abstract class BaseApiClient {
2831
baseUrl: string,
2932
private readonly clientId: string,
3033
private httpClient: IHttpClient,
31-
private customAuthApiQueryParams?: Record<string, string>
34+
private customAuthApiQueryParams?: Record<string, string>,
35+
private requestInterceptor?: CustomAuthRequestInterceptor,
36+
private logger?: Logger
3237
) {
3338
this.baseRequestUrl = parseUrl(
3439
!baseUrl.endsWith("/") ? `${baseUrl}/` : baseUrl
@@ -45,13 +50,23 @@ export abstract class BaseApiClient {
4550
client_id: this.clientId,
4651
...data,
4752
});
48-
const headers = this.getCommonHeaders(correlationId, telemetryManager);
53+
const commonHeaders = this.getCommonHeaders(
54+
correlationId,
55+
telemetryManager
56+
);
4957
const url = buildUrl(
5058
this.baseRequestUrl.href,
5159
endpoint,
5260
this.customAuthApiQueryParams
5361
);
5462

63+
const additionalHeaders = await this.getAdditionalHeaders(
64+
url,
65+
correlationId
66+
);
67+
68+
const headers = { ...commonHeaders, ...additionalHeaders };
69+
5570
let response: Response;
5671

5772
try {
@@ -173,4 +188,28 @@ export abstract class BaseApiClient {
173188
responseError.timestamp
174189
);
175190
}
191+
192+
private async getAdditionalHeaders(
193+
url: URL,
194+
correlationId: string
195+
): Promise<Record<string, string>> {
196+
if (!this.requestInterceptor) {
197+
return {};
198+
}
199+
200+
try {
201+
const result = await Promise.resolve(
202+
this.requestInterceptor.addAdditionalHeaderFields(url)
203+
);
204+
205+
return filterCustomHeaders(result, this.logger, correlationId);
206+
} catch (e) {
207+
this.logger?.warningPii(
208+
`CustomAuthRequestInterceptor.addAdditionalHeaderFields threw an error; continuing without additional headers: ${e}`,
209+
correlationId
210+
);
211+
212+
return {};
213+
}
214+
}
176215
}

lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { SignInApiClient } from "./SignInApiClient.js";
99
import { RegisterApiClient } from "./RegisterApiClient.js";
1010
import { ICustomAuthApiClient } from "./ICustomAuthApiClient.js";
1111
import { IHttpClient } from "../http_client/IHttpClient.js";
12+
import { Logger } from "@azure/msal-common/browser";
13+
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
1214

1315
export class CustomAuthApiClient implements ICustomAuthApiClient {
1416
signInApi: SignInApiClient;
@@ -21,34 +23,44 @@ export class CustomAuthApiClient implements ICustomAuthApiClient {
2123
clientId: string,
2224
httpClient: IHttpClient,
2325
capabilities?: string,
24-
customAuthApiQueryParams?: Record<string, string>
26+
customAuthApiQueryParams?: Record<string, string>,
27+
requestInterceptor?: CustomAuthRequestInterceptor,
28+
logger?: Logger
2529
) {
2630
this.signInApi = new SignInApiClient(
2731
customAuthApiBaseUrl,
2832
clientId,
2933
httpClient,
3034
capabilities,
31-
customAuthApiQueryParams
35+
customAuthApiQueryParams,
36+
requestInterceptor,
37+
logger
3238
);
3339
this.signUpApi = new SignupApiClient(
3440
customAuthApiBaseUrl,
3541
clientId,
3642
httpClient,
3743
capabilities,
38-
customAuthApiQueryParams
44+
customAuthApiQueryParams,
45+
requestInterceptor,
46+
logger
3947
);
4048
this.resetPasswordApi = new ResetPasswordApiClient(
4149
customAuthApiBaseUrl,
4250
clientId,
4351
httpClient,
4452
capabilities,
45-
customAuthApiQueryParams
53+
customAuthApiQueryParams,
54+
requestInterceptor,
55+
logger
4656
);
4757
this.registerApi = new RegisterApiClient(
4858
customAuthApiBaseUrl,
4959
clientId,
5060
httpClient,
51-
customAuthApiQueryParams
61+
customAuthApiQueryParams,
62+
requestInterceptor,
63+
logger
5264
);
5365
}
5466
}

lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ResetPasswordApiClient.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { CustomAuthApiError } from "../../error/CustomAuthApiError.js";
1111
import { BaseApiClient } from "./BaseApiClient.js";
1212
import { IHttpClient } from "../http_client/IHttpClient.js";
13+
import { Logger } from "@azure/msal-common/browser";
1314
import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js";
1415
import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js";
1516
import {
@@ -26,6 +27,7 @@ import {
2627
ResetPasswordStartResponse,
2728
ResetPasswordSubmitResponse,
2829
} from "./types/ApiResponseTypes.js";
30+
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
2931

3032
export class ResetPasswordApiClient extends BaseApiClient {
3133
private readonly capabilities?: string;
@@ -35,13 +37,17 @@ export class ResetPasswordApiClient extends BaseApiClient {
3537
clientId: string,
3638
httpClient: IHttpClient,
3739
capabilities?: string,
38-
customAuthApiQueryParams?: Record<string, string>
40+
customAuthApiQueryParams?: Record<string, string>,
41+
requestInterceptor?: CustomAuthRequestInterceptor,
42+
logger?: Logger
3943
) {
4044
super(
4145
customAuthApiBaseUrl,
4246
clientId,
4347
httpClient,
44-
customAuthApiQueryParams
48+
customAuthApiQueryParams,
49+
requestInterceptor,
50+
logger
4551
);
4652
this.capabilities = capabilities;
4753
}

lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignInApiClient.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { ServerTelemetryManager } from "@azure/msal-common/browser";
6+
import { Logger, ServerTelemetryManager } from "@azure/msal-common/browser";
77
import { GrantType } from "../../../CustomAuthConstants.js";
88
import { CustomAuthApiError } from "../../error/CustomAuthApiError.js";
99
import { BaseApiClient } from "./BaseApiClient.js";
@@ -24,6 +24,7 @@ import {
2424
SignInIntrospectResponse,
2525
SignInTokenResponse,
2626
} from "./types/ApiResponseTypes.js";
27+
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
2728

2829
export class SignInApiClient extends BaseApiClient {
2930
private readonly capabilities?: string;
@@ -33,13 +34,17 @@ export class SignInApiClient extends BaseApiClient {
3334
clientId: string,
3435
httpClient: IHttpClient,
3536
capabilities?: string,
36-
customAuthApiQueryParams?: Record<string, string>
37+
customAuthApiQueryParams?: Record<string, string>,
38+
requestInterceptor?: CustomAuthRequestInterceptor,
39+
logger?: Logger
3740
) {
3841
super(
3942
customAuthApiBaseUrl,
4043
clientId,
4144
httpClient,
42-
customAuthApiQueryParams
45+
customAuthApiQueryParams,
46+
requestInterceptor,
47+
logger
4348
);
4449
this.capabilities = capabilities;
4550
}

lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignupApiClient.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { GrantType } from "../../../CustomAuthConstants.js";
77
import { BaseApiClient } from "./BaseApiClient.js";
88
import { IHttpClient } from "../http_client/IHttpClient.js";
9+
import { Logger } from "@azure/msal-common/browser";
910
import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js";
1011
import {
1112
SignUpChallengeRequest,
@@ -19,6 +20,7 @@ import {
1920
SignUpContinueResponse,
2021
SignUpStartResponse,
2122
} from "./types/ApiResponseTypes.js";
23+
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
2224

2325
export class SignupApiClient extends BaseApiClient {
2426
private readonly capabilities?: string;
@@ -28,13 +30,17 @@ export class SignupApiClient extends BaseApiClient {
2830
clientId: string,
2931
httpClient: IHttpClient,
3032
capabilities?: string,
31-
customAuthApiQueryParams?: Record<string, string>
33+
customAuthApiQueryParams?: Record<string, string>,
34+
requestInterceptor?: CustomAuthRequestInterceptor,
35+
logger?: Logger
3236
) {
3337
super(
3438
customAuthApiBaseUrl,
3539
clientId,
3640
httpClient,
37-
customAuthApiQueryParams
41+
customAuthApiQueryParams,
42+
requestInterceptor,
43+
logger
3844
);
3945
this.capabilities = capabilities;
4046
}

0 commit comments

Comments
 (0)