Skip to content

Commit 8e4b664

Browse files
authored
Fix MSAL Angular MsalInterceptor bug matching to query string (#7137)
This PR addresses a bug where the `MsalInterceptor` could match the protectedResource to the query string part of the URL instead of the host name and port part of the URL. It also refactors the code surrounding relative URLs. This addresses issue #7111
1 parent 69e58c3 commit 8e4b664

File tree

4 files changed

+89
-47
lines changed

4 files changed

+89
-47
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Fix query string instead of HostNameAndPort bug in MsalInterceptor #7137",
4+
"packageName": "@azure/msal-angular",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,3 @@ export type ProtectedResourceScopes = {
3737
httpMethod: string;
3838
scopes: Array<string> | null;
3939
};
40-
41-
export type MatchingResources = {
42-
absoluteResources: Array<string>;
43-
relativeResources: Array<string>;
44-
};

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ function MSALInterceptorFactory(): MsalInterceptorConfiguration {
5757
string,
5858
Array<string | ProtectedResourceScopes> | null
5959
>([
60+
["https://MY_API_SITE_2", ["api://MY_API_SITE_2/as_user"]],
61+
["https://MY_API_SITE_1", ["api://MY_API_SITE_1/as_user"]],
6062
["https://graph.microsoft.com/v1.0/me", ["user.read"]],
6163
["relative/me", ["relative.scope"]],
6264
["https://myapplication.com/user/*", ["customscope.read"]],
@@ -906,9 +908,9 @@ describe("MsalInterceptor", () => {
906908
sampleAccountInfo,
907909
]);
908910

909-
httpClient.get("http://site.com/relative/me").subscribe();
911+
httpClient.get("http://localhost:9876/relative/me").subscribe();
910912
setTimeout(() => {
911-
const request = httpMock.expectOne("http://site.com/relative/me");
913+
const request = httpMock.expectOne("http://localhost:9876/relative/me");
912914
request.flush({ data: "test" });
913915
expect(request.request.headers.get("Authorization")).toEqual(
914916
"Bearer access-token"
@@ -1122,4 +1124,44 @@ describe("MsalInterceptor", () => {
11221124
done();
11231125
}, 200);
11241126
});
1127+
1128+
it("attaches authorization header with access token when endpoint match is in HostNameAndPort instead of query string", (done) => {
1129+
const spy = spyOn(
1130+
PublicClientApplication.prototype,
1131+
"acquireTokenSilent"
1132+
).and.returnValue(
1133+
new Promise((resolve) => {
1134+
//@ts-ignore
1135+
resolve({
1136+
accessToken: "access-token",
1137+
});
1138+
})
1139+
);
1140+
1141+
spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
1142+
sampleAccountInfo,
1143+
]);
1144+
1145+
httpClient
1146+
.get(
1147+
"https://MY_API_SITE_1/api/sites?$filter=siteUrl eq 'https://MY_API_SITE_2'"
1148+
)
1149+
.subscribe();
1150+
1151+
setTimeout(() => {
1152+
const request = httpMock.expectOne(
1153+
"https://MY_API_SITE_1/api/sites?$filter=siteUrl eq 'https://MY_API_SITE_2'"
1154+
);
1155+
request.flush({ data: "test" });
1156+
expect(request.request.headers.get("Authorization")).toEqual(
1157+
"Bearer access-token"
1158+
);
1159+
expect(spy).toHaveBeenCalledWith({
1160+
account: sampleAccountInfo,
1161+
scopes: ["api://MY_API_SITE_1/as_user"],
1162+
});
1163+
httpMock.verify();
1164+
done();
1165+
}, 200);
1166+
});
11251167
});

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

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
InteractionStatus,
1919
InteractionType,
2020
StringUtils,
21-
UrlString,
2221
} from "@azure/msal-browser";
2322
import { Observable, EMPTY, of } from "rxjs";
2423
import { switchMap, catchError, take, filter } from "rxjs/operators";
@@ -27,7 +26,6 @@ import {
2726
MsalInterceptorAuthRequest,
2827
MsalInterceptorConfiguration,
2928
ProtectedResourceScopes,
30-
MatchingResources,
3129
} from "./msal.interceptor.config";
3230
import { MsalBroadcastService } from "./msal.broadcast.service";
3331
import { MSAL_INTERCEPTOR_CONFIG } from "./constants";
@@ -239,17 +237,10 @@ export class MsalInterceptor implements HttpInterceptor {
239237
normalizedEndpoint
240238
);
241239

242-
// Check absolute urls of resources first before checking relative to prevent incorrect matching where multiple resources have similar relative urls
243-
if (matchingProtectedResources.absoluteResources.length > 0) {
240+
if (matchingProtectedResources.length > 0) {
244241
return this.matchScopesToEndpoint(
245242
this.msalInterceptorConfig.protectedResourceMap,
246-
matchingProtectedResources.absoluteResources,
247-
httpMethod
248-
);
249-
} else if (matchingProtectedResources.relativeResources.length > 0) {
250-
return this.matchScopesToEndpoint(
251-
this.msalInterceptorConfig.protectedResourceMap,
252-
matchingProtectedResources.relativeResources,
243+
matchingProtectedResources,
253244
httpMethod
254245
);
255246
}
@@ -266,46 +257,53 @@ export class MsalInterceptor implements HttpInterceptor {
266257
private matchResourcesToEndpoint(
267258
protectedResourcesEndpoints: string[],
268259
endpoint: string
269-
): MatchingResources {
270-
const matchingResources: MatchingResources = {
271-
absoluteResources: [],
272-
relativeResources: [],
273-
};
260+
): Array<string> {
261+
const matchingResources: Array<string> = [];
274262

275263
protectedResourcesEndpoints.forEach((key) => {
276-
// Normalizes and adds resource to matchingResources.absoluteResources if key matches endpoint. StringUtils.matchPattern accounts for wildcards
277264
const normalizedKey = this.location.normalize(key);
278-
if (StringUtils.matchPattern(normalizedKey, endpoint)) {
279-
matchingResources.absoluteResources.push(key);
280-
}
281265

282-
// Get url components for relative urls
283-
const absoluteKey = this.getAbsoluteUrl(key);
284-
const keyComponents = new UrlString(absoluteKey).getUrlComponents();
266+
// Get url components
267+
const absoluteKey = this.getAbsoluteUrl(normalizedKey);
268+
const keyComponents = new URL(absoluteKey);
285269
const absoluteEndpoint = this.getAbsoluteUrl(endpoint);
286-
const endpointComponents = new UrlString(
287-
absoluteEndpoint
288-
).getUrlComponents();
289-
290-
// Normalized key should include query strings if applicable
291-
const relativeNormalizedKey = keyComponents.QueryString
292-
? `${keyComponents.AbsolutePath}?${keyComponents.QueryString}`
293-
: this.location.normalize(keyComponents.AbsolutePath);
294-
295-
// Add resource to matchingResources.relativeResources if same origin, relativeKey matches endpoint, and is not empty
296-
if (
297-
keyComponents.HostNameAndPort === endpointComponents.HostNameAndPort &&
298-
StringUtils.matchPattern(relativeNormalizedKey, absoluteEndpoint) &&
299-
relativeNormalizedKey !== "" &&
300-
relativeNormalizedKey !== "/*"
301-
) {
302-
matchingResources.relativeResources.push(key);
270+
const endpointComponents = new URL(absoluteEndpoint);
271+
272+
if (this.checkUrlComponents(keyComponents, endpointComponents)) {
273+
matchingResources.push(key);
303274
}
304275
});
305276

306277
return matchingResources;
307278
}
308279

280+
/**
281+
* Compares URL segments between key and endpoint
282+
* @param key
283+
* @param endpoint
284+
* @returns
285+
*/
286+
private checkUrlComponents(
287+
keyComponents: URL,
288+
endpointComponents: URL
289+
): boolean {
290+
// URL properties from https://developer.mozilla.org/en-US/docs/Web/API/URL
291+
const urlProperties = ["protocol", "host", "pathname", "search", "hash"];
292+
293+
for (const property of urlProperties) {
294+
if (keyComponents[property]) {
295+
const decodedInput = decodeURIComponent(keyComponents[property]);
296+
if (
297+
!StringUtils.matchPattern(decodedInput, endpointComponents[property])
298+
) {
299+
return false;
300+
}
301+
}
302+
}
303+
304+
return true;
305+
}
306+
309307
/**
310308
* Transforms relative urls to absolute urls
311309
* @param url

0 commit comments

Comments
 (0)