Skip to content

Commit 1d8e35f

Browse files
jo-arroyoCopilot
andauthored
Update docs and add warning for strictMatching (#8388)
This PR adds `strictMatching` runtime warning and documentation for `@azure/msal-angular`. When `strictMatching` is not explicitly configured on `MsalInterceptorConfiguration`, `MsalInterceptor` now emits a one-time warning via the MSAL logger. This helps developers discover silent 401 failures caused by misconfigured `protectedResourceMap` keys under the v5 default strict matching behaviour. #### Code Changes - **`msal.interceptor.ts`** — Inline check in the constructor emits a logger warning when `strictMatching === undefined`, linking to the strict matching docs. - **`msal.interceptor.spec.ts`** — 3 new warning tests (`undefined` → warns, `true`/`false` → no warn). Existing tests updated to pass `strictMatching: true` to suppress the warning. #### Documentation Changes - **`msal-interceptor.md`** — Common failure patterns table, environment-driven configuration guidance, troubleshooting section with fix options, and runtime warning explanation. - **`v4-v5-upgrade-guide.md`** — Quick checklist, v5 minor upgrade callout, and environment-driven `protectedResourceMap` code sample with `// TODO` migration comment. - **`known-issues.md`** — Concise entry linking to strict matching docs. - **`configuration.md`** — `strictMatching: false` with comments in dynamic config samples; guidance comments in static config samples. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7955a28 commit 1d8e35f

7 files changed

Lines changed: 217 additions & 7 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Add strictMatching runtime warning when strictMatching is not explicitly configured [#8388](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8388)",
4+
"packageName": "@azure/msal-angular",
5+
"email": "joarroyo@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-angular/docs/configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export function MSALInstanceFactory(): IPublicClientApplication {
115115

116116
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
117117
const protectedResourceMap = new Map<string, Array<string>>();
118+
// Use exact paths or wildcards so strict matching (the v5 default) works correctly
118119
protectedResourceMap.set("https://graph.microsoft.com/v1.0/me", ["user.read"]);
119120

120121
return {
@@ -238,6 +239,10 @@ fetch("/assets/configuration.json")
238239
useValue: {
239240
interactionType: json.interceptor.interactionType,
240241
protectedResourceMap: new Map(json.interceptor.protectedResourceMap),
242+
// Dynamic JSON configurations may use base URLs without wildcards.
243+
// Set strictMatching: false if keys are not guaranteed to be exact
244+
// paths or wildcard patterns. Remove once keys are migrated.
245+
strictMatching: false,
241246
} as MsalInterceptorConfiguration,
242247
},
243248
])
@@ -373,6 +378,10 @@ export function MSALInterceptorConfigFactory(config: ConfigService): MsalInterce
373378
return {
374379
interactionType: config.getSettings("interceptor").interactionType,
375380
protectedResourceMap,
381+
// Dynamic configurations may use base URLs without wildcards.
382+
// Set strictMatching: false if keys are not guaranteed to be exact
383+
// paths or wildcard patterns. Remove once keys are migrated.
384+
strictMatching: false,
376385
};
377386
}
378387

@@ -548,6 +557,7 @@ export function MSALInstanceFactory(): IPublicClientApplication {
548557

549558
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
550559
const protectedResourceMap = new Map<string, Array<string>>();
560+
// Use exact paths or wildcards so strict matching (the v5 default) works correctly
551561
protectedResourceMap.set("https://graph.microsoft.com/v1.0/me", ["user.read"]);
552562

553563
return {

lib/msal-angular/docs/known-issues.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Known issues for MSAL Angular
22

3+
## 5.x
4+
5+
### Strict matching may silently drop `Authorization` headers
6+
7+
`protectedResourceMap` uses strict URL matching by default in v5, which can silently prevent the `Authorization` header from being attached (resulting in **401 Unauthorized** errors). See the [strict matching documentation](./msal-interceptor.md#strict-matching-strictmatching) for details, common failure patterns, and fix options.
8+
39
## 2.0.0
410
* MSAL Guard's `CanLoad` does not interactively prompt for login. This will be addressed in a future release.
511

lib/msal-angular/docs/msal-interceptor.md

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ Other things to note regarding the `protectedResourceMap`:
151151

152152
In msal-angular v5, URL component pattern matching for `protectedResourceMap` entries uses strict matching semantics by default. The `strictMatching` field on `MsalInterceptorConfiguration` controls this behaviour.
153153

154+
> **⚠️ Important:** If your application sets `protectedResourceMap` keys dynamically (e.g. from environment files, `APP_INITIALIZER`, or JSON configuration) and those keys are base URLs without sub-paths or wildcards, strict matching can silently prevent the `Authorization` header from being attached, resulting in **401 errors with no build-time error and no per-request warning — only a one-time initialization warning if `strictMatching` is left unconfigured**. See [Troubleshooting strict matching](#troubleshooting-strict-matching) below.
155+
154156
#### What strict matching changes
155157

156158
| Behaviour | Legacy (`strictMatching: false`) | Strict (default in v5) |
@@ -165,6 +167,16 @@ With strict matching (the v5 default):
165167
- A pattern like `*.contoso.com` matches `app.contoso.com` but **not** `a.b.contoso.com` (wildcard cannot span dot separators).
166168
- A pattern like `https://graph.microsoft.com/v1.0/me` matches only that exact URL.
167169

170+
#### Common failure patterns
171+
172+
The following `protectedResourceMap` key patterns work under legacy matching but silently fail with strict matching:
173+
174+
| Key pattern | Outgoing request URL | Result under strict matching | Fix |
175+
|---|---|---|---|
176+
| `https://api.example.com` | `https://api.example.com/v1/users` | ❌ No match — key resolves to path `/`, request has path `/v1/users` | `https://api.example.com/*` |
177+
| `https://api.example.com/` | `https://api.example.com/v1/users` | ❌ No match — trailing slash anchors the pattern to exactly `/` | `https://api.example.com/*` |
178+
| `environment.apiConfig.uri` (e.g. `https://api.example.com`) | `https://api.example.com/v1/users` | ❌ No match — same as above | `` `${environment.apiConfig.uri}/*` `` |
179+
168180
#### Default behaviour in v5 (no configuration needed)
169181

170182
Strict matching is enabled by default. No additional configuration is required:
@@ -173,7 +185,8 @@ Strict matching is enabled by default. No additional configuration is required:
173185
{
174186
interactionType: InteractionType.Redirect,
175187
protectedResourceMap: new Map([
176-
["https://*.contoso.com/api", ["contoso.scope"]],
188+
// Use wildcards or exact paths so strict matching works correctly
189+
["https://*.contoso.com/api/*", ["contoso.scope"]],
177190
["https://graph.microsoft.com/v1.0/me", ["user.read"]]
178191
])
179192
// strictMatching defaults to true in v5
@@ -197,6 +210,84 @@ If your patterns rely on the looser matching from v4, you can set `strictMatchin
197210
}
198211
```
199212

213+
#### Guidance for environment-driven configurations
214+
215+
If your `protectedResourceMap` keys reference Angular `environment` values (e.g. `environment.apiConfig.uri`), check whether those values are **exact paths** (e.g. `https://graph.microsoft.com/v1.0/me`) or **bare base URLs** (e.g. `https://api.example.com`). Exact paths work correctly with strict matching and need no special handling:
216+
217+
```javascript
218+
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
219+
const protectedResourceMap = new Map<string, Array<string>>();
220+
// environment.apiConfig.uri is an exact path (e.g. "https://graph.microsoft.com/v1.0/me")
221+
// — strict matching works correctly
222+
protectedResourceMap.set(environment.apiConfig.uri, environment.apiConfig.scopes);
223+
224+
return {
225+
interactionType: InteractionType.Redirect,
226+
protectedResourceMap,
227+
};
228+
}
229+
```
230+
231+
If the environment value is a bare base URL and you need to match sub-paths, append a `/*` wildcard:
232+
233+
```javascript
234+
// environment.apiConfig.uri is a base URL (e.g. "https://api.example.com")
235+
// Append /* to match all sub-paths
236+
protectedResourceMap.set(`${environment.apiConfig.uri}/*`, environment.apiConfig.scopes);
237+
```
238+
239+
For truly **dynamic** configurations where the key shape is unknown at build time (e.g. `APP_INITIALIZER`, JSON loaded via `fetch`, or `platformBrowserDynamic`), set `strictMatching: false` as a temporary safe default. See [Fix options → Option B](#fix-options) below for a code example.
240+
241+
#### Troubleshooting strict matching
242+
243+
##### Symptoms
244+
245+
- API requests return **401 Unauthorized** after upgrading to `@azure/msal-angular` v5 (or between v5 minor versions such as 5.0.x → 5.1.x).
246+
- The `Authorization: Bearer <token>` header is **missing** from outgoing HTTP requests.
247+
- No build-time or runtime errors are reported — the failure is **silent**.
248+
- The issue may only appear in certain environments (e.g. staging/production) where the API base URL differs from development.
249+
250+
##### Fix options
251+
252+
**Option A: Update keys to work with strict matching (recommended)**
253+
254+
Update your `protectedResourceMap` keys to use exact paths or wildcards that match under strict matching rules. This is the preferred approach because strict matching is safer and more predictable:
255+
256+
```javascript
257+
{
258+
interactionType: InteractionType.Redirect,
259+
protectedResourceMap: new Map([
260+
// Exact path — matches only this URL
261+
["https://graph.microsoft.com/v1.0/me", ["user.read"]],
262+
// Wildcard — matches all sub-paths of the API
263+
["https://api.example.com/v1/*", ["api.scope"]]
264+
])
265+
// strictMatching defaults to true — no need to set it
266+
}
267+
```
268+
269+
**Option B: Set `strictMatching: false` (fallback for dynamic configurations)**
270+
271+
If your `protectedResourceMap` keys are loaded dynamically at runtime (e.g. from `APP_INITIALIZER`, JSON config, or `platformBrowserDynamic`) and you cannot guarantee they contain exact paths or wildcards, set `strictMatching: false` as a temporary safe default:
272+
273+
```javascript
274+
{
275+
interactionType: InteractionType.Redirect,
276+
protectedResourceMap: new Map([
277+
[config.apiUri, config.apiScopes]
278+
]),
279+
// Dynamic keys may be base URLs without wildcards.
280+
// Remove once keys are migrated to exact paths or wildcard patterns.
281+
strictMatching: false
282+
}
283+
```
284+
285+
> **Note:** Legacy matching (`strictMatching: false`) is provided for backwards compatibility and may be removed in a future major version.
286+
287+
##### Runtime warning
288+
289+
`MsalInterceptor` emits a **one-time runtime warning** via the MSAL logger during initialization when `strictMatching` is not explicitly configured. If you see this warning, follow the fix options above.
290+
200291
### Optional authRequest
201292
202293
For more information on the optional `authRequest` that can be set in the `MsalInterceptorConfiguration`, please see our [multi-tenant doc here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/multi-tenant.md#dynamic-auth-request).

lib/msal-angular/docs/v4-v5-upgrade-guide.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,35 @@ Please see the [MSAL Browser v4-v5 migration guide](https://github.com/AzureAD/m
1010

1111
In msal-angular v5, URL pattern matching for protectedResourceMap entries uses strict matching semantics by default. Strict matching treats pattern metacharacters as literals, anchors matches to the full URL component, and applies host wildcard rules that do not span dot separators. If your v4 configuration relied on looser matching behavior, update your protectedResourceMap patterns to align with strict matching, or set strictMatching to false to retain legacy behavior temporarily. See [MSAL Interceptor docs](./msal-interceptor.md#strict-matching-strictmatching) for more details.
1212

13+
> **⚠️ This change can also affect v5 minor upgrades.** If strict matching was not yet the default in the v5 minor version you originally adopted (e.g. 5.0.x), upgrading to a later v5 minor (e.g. 5.1.x) where strict matching is the default can silently break token attachment. The primary symptom is identical: **401 errors** — a runtime warning is now emitted when `strictMatching` is not explicitly configured, but the match failure itself remains silent and the `Authorization` header is no longer attached.
14+
15+
#### Quick checklist
16+
17+
1. **Review your `protectedResourceMap` keys.** Keys that are bare base URLs (e.g. `https://api.example.com`) without wildcards or sub-paths will no longer match requests to sub-paths of that URL. See [common failure patterns](./msal-interceptor.md#common-failure-patterns).
18+
2. **Update keys to use exact paths or wildcards.** Each key should either match the exact URL your application requests, or use a `/*` wildcard suffix to match sub-paths. See [fix options](./msal-interceptor.md#fix-options).
19+
3. **If your keys are loaded dynamically at runtime**, set `strictMatching: false` as a temporary safe default. See [guidance for environment-driven configurations](./msal-interceptor.md#guidance-for-environment-driven-configurations).
20+
21+
#### Environment-driven `protectedResourceMap`
22+
23+
If your `protectedResourceMap` keys come from Angular `environment` files, `APP_INITIALIZER`, JSON configuration, or `platformBrowserDynamic`, set `strictMatching: false` as a safe default during migration:
24+
25+
```javascript
26+
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
27+
const protectedResourceMap = new Map<string, Array<string>>();
28+
protectedResourceMap.set(environment.apiConfig.uri, environment.apiConfig.scopes);
29+
30+
return {
31+
interactionType: InteractionType.Redirect,
32+
protectedResourceMap,
33+
// TODO: Remove once protectedResourceMap keys are updated to use
34+
// exact paths or wildcard patterns (e.g. "https://api.example.com/*").
35+
strictMatching: false,
36+
};
37+
}
38+
```
39+
40+
Once all keys are migrated to exact paths or wildcards, remove `strictMatching: false` (or set it to `true`) to benefit from the stricter, safer matching behaviour. See [guidance for environment-driven configurations](./msal-interceptor.md#guidance-for-environment-driven-configurations) for more details.
41+
1342
## Other changes in `@azure/msal-angular@5`
1443

1544
### `inject(TOKEN)` syntax

lib/msal-angular/src/msal.interceptor.spec.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,65 @@ describe("MsalInterceptor", () => {
11761176
});
11771177
});
11781178

1179+
// ---------------------------------------------------------------------------
1180+
// strictMatching warning tests
1181+
// ---------------------------------------------------------------------------
1182+
1183+
describe("MsalInterceptor - strict matching warning", () => {
1184+
const emptyMap = new Map<
1185+
string,
1186+
Array<string | ProtectedResourceScopes> | null
1187+
>();
1188+
1189+
function initializeAndSpyOnWarning(strict?: boolean): jasmine.Spy {
1190+
TestBed.resetTestingModule();
1191+
TestBed.configureTestingModule({
1192+
imports: [
1193+
MsalModule.forRoot(
1194+
MSALInstanceFactory(),
1195+
null,
1196+
MSALStrictInterceptorFactory(emptyMap, strict)
1197+
),
1198+
],
1199+
providers: [
1200+
MsalInterceptor,
1201+
MsalService,
1202+
MsalBroadcastService,
1203+
{
1204+
provide: HTTP_INTERCEPTORS,
1205+
useClass: MsalInterceptor,
1206+
multi: true,
1207+
},
1208+
Location,
1209+
provideHttpClient(withInterceptorsFromDi()),
1210+
provideHttpClientTesting(),
1211+
provideRouter([]),
1212+
],
1213+
teardown: { destroyAfterEach: false },
1214+
});
1215+
1216+
const spy = spyOn(TestBed.inject(MsalService).getLogger(), "warning");
1217+
interceptor = TestBed.inject(MsalInterceptor);
1218+
return spy;
1219+
}
1220+
1221+
it("warns when strictMatching is undefined", () => {
1222+
const spy = initializeAndSpyOnWarning(undefined);
1223+
expect(spy).toHaveBeenCalledTimes(1);
1224+
expect(spy.calls.first().args[0]).toContain(
1225+
"[MSAL] strictMatching is enabled by default"
1226+
);
1227+
});
1228+
1229+
it("does not warn when strictMatching is true", () => {
1230+
expect(initializeAndSpyOnWarning(true)).not.toHaveBeenCalled();
1231+
});
1232+
1233+
it("does not warn when strictMatching is false", () => {
1234+
expect(initializeAndSpyOnWarning(false)).not.toHaveBeenCalled();
1235+
});
1236+
});
1237+
11791238
// ---------------------------------------------------------------------------
11801239
// matchPatternStrict / matchPattern helper tests
11811240
// ---------------------------------------------------------------------------
@@ -1245,7 +1304,7 @@ describe("matchPatternStrict unit tests", () => {
12451304
string,
12461305
Array<string | ProtectedResourceScopes> | null
12471306
>();
1248-
initializeMsalStrict(emptyMap);
1307+
initializeMsalStrict(emptyMap, true);
12491308
match = (interceptor as any).matchPatternStrict.bind(interceptor);
12501309
});
12511310

@@ -1412,7 +1471,7 @@ describe("msal.interceptor matchPatternStrict", () => {
14121471

14131472
describe("host wildcard matching", () => {
14141473
beforeEach(() => {
1415-
initializeMsalStrict(hostwildcardMap);
1474+
initializeMsalStrict(hostwildcardMap, true);
14161475
});
14171476

14181477
it("msal.interceptor matchPatternStrict: host wildcard matches a single-label subdomain", (done) => {
@@ -1501,7 +1560,7 @@ describe("msal.interceptor matchPatternStrict", () => {
15011560
>([["https://myapplication.com/user/*", ["customscope.read"]]]);
15021561

15031562
beforeEach(() => {
1504-
initializeMsalStrict(pathMap);
1563+
initializeMsalStrict(pathMap, true);
15051564
});
15061565

15071566
it("msal.interceptor matchPatternStrict: pathname wildcard matches expected segments", (done) => {
@@ -1630,8 +1689,7 @@ describe("MsalInterceptor - strictMatching option", () => {
16301689
]);
16311690

16321691
beforeEach(() => {
1633-
// No strictMatching field set — should default to strict (v5)
1634-
initializeMsalStrict(defaultStrictMap);
1692+
initializeMsalStrict(defaultStrictMap, true);
16351693
});
16361694

16371695
it("MsalInterceptor: attaches authorization header only when URL components match configured pattern", (done) => {
@@ -1719,7 +1777,7 @@ describe("MsalInterceptor - strictMatching option", () => {
17191777
>([["https://*.microsoft.com", ["microsoft.scope"]]]);
17201778

17211779
beforeEach(() => {
1722-
initializeMsalStrict(microsoftResourceMap);
1780+
initializeMsalStrict(microsoftResourceMap, true);
17231781
});
17241782

17251783
it("does not attach authorization header when the matched hostname appears only in the query string", (done) => {

lib/msal-angular/src/msal.interceptor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ export class MsalInterceptor implements HttpInterceptor {
4343
@Inject(DOCUMENT) document?: any
4444
) {
4545
this._document = document as Document;
46+
47+
if (this.msalInterceptorConfig.strictMatching === undefined) {
48+
this.authService
49+
.getLogger()
50+
.warning(
51+
`[MSAL] strictMatching is enabled by default. See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/docs/msal-interceptor.md#strict-matching-strictmatching`,
52+
""
53+
);
54+
}
4655
}
4756

4857
intercept(

0 commit comments

Comments
 (0)