@azure/msal-angular 4.0.12 → 4.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.beachballrc +3 -0
  2. package/.editorconfig +16 -0
  3. package/.eslintrc.json +3 -0
  4. package/CHANGELOG.json +2315 -0
  5. package/CHANGELOG.md +869 -0
  6. package/FAQ.md +182 -0
  7. package/angular.json +51 -0
  8. package/docs/angular-universal.md +64 -0
  9. package/docs/configuration.md +609 -0
  10. package/docs/errors.md +89 -0
  11. package/docs/events.md +262 -0
  12. package/docs/initialization.md +206 -0
  13. package/docs/known-issues.md +19 -0
  14. package/docs/logging.md +47 -0
  15. package/docs/msal-guard.md +208 -0
  16. package/docs/msal-interceptor.md +160 -0
  17. package/docs/multi-tenant.md +90 -0
  18. package/docs/performance.md +50 -0
  19. package/docs/public-apis.md +29 -0
  20. package/docs/redirects.md +257 -0
  21. package/docs/security.md +4 -0
  22. package/docs/ssosilent.md +84 -0
  23. package/docs/v0-v1-upgrade-guide.md +61 -0
  24. package/docs/v1-v2-upgrade-guide.md +70 -0
  25. package/docs/v2-v3-upgrade-guide.md +52 -0
  26. package/docs/v3-v4-upgrade-guide.md +36 -0
  27. package/karma.conf.js +44 -0
  28. package/ng-package.json +7 -0
  29. package/package.json +56 -26
  30. package/src/IMsalService.ts +34 -0
  31. package/src/constants.ts +20 -0
  32. package/src/msal.broadcast.config.ts +8 -0
  33. package/src/msal.broadcast.service.spec.ts +471 -0
  34. package/src/msal.broadcast.service.ts +84 -0
  35. package/src/msal.guard.config.ts +27 -0
  36. package/src/msal.guard.spec.ts +525 -0
  37. package/src/msal.guard.ts +290 -0
  38. package/src/msal.interceptor.config.ts +39 -0
  39. package/src/msal.interceptor.spec.ts +1168 -0
  40. package/src/msal.interceptor.ts +388 -0
  41. package/src/msal.module.ts +51 -0
  42. package/src/msal.navigation.client.spec.ts +96 -0
  43. package/src/msal.navigation.client.ts +59 -0
  44. package/src/msal.redirect.component.spec.ts +64 -0
  45. package/src/msal.redirect.component.ts +26 -0
  46. package/src/msal.service.spec.ts +468 -0
  47. package/src/msal.service.ts +104 -0
  48. package/src/packageMetadata.ts +3 -0
  49. package/{public-api.d.ts → src/public-api.ts} +35 -17
  50. package/src/test.ts +19 -0
  51. package/tsconfig.json +25 -0
  52. package/tsconfig.lib.json +27 -0
  53. package/tsconfig.lib.prod.json +10 -0
  54. package/tsconfig.spec.json +17 -0
  55. package/typedoc.json +10 -0
  56. package/IMsalService.d.ts +0 -17
  57. package/constants.d.ts +0 -5
  58. package/esm2020/IMsalService.mjs +0 -6
  59. package/esm2020/azure-msal-angular.mjs +0 -5
  60. package/esm2020/constants.mjs +0 -10
  61. package/esm2020/msal.broadcast.config.mjs +0 -6
  62. package/esm2020/msal.broadcast.service.mjs +0 -66
  63. package/esm2020/msal.guard.config.mjs +0 -6
  64. package/esm2020/msal.guard.mjs +0 -218
  65. package/esm2020/msal.interceptor.config.mjs +0 -6
  66. package/esm2020/msal.interceptor.mjs +0 -270
  67. package/esm2020/msal.module.mjs +0 -46
  68. package/esm2020/msal.navigation.client.mjs +0 -53
  69. package/esm2020/msal.redirect.component.mjs +0 -31
  70. package/esm2020/msal.service.mjs +0 -88
  71. package/esm2020/packageMetadata.mjs +0 -4
  72. package/esm2020/public-api.mjs +0 -18
  73. package/fesm2015/azure-msal-angular.mjs +0 -762
  74. package/fesm2015/azure-msal-angular.mjs.map +0 -1
  75. package/fesm2020/azure-msal-angular.mjs +0 -758
  76. package/fesm2020/azure-msal-angular.mjs.map +0 -1
  77. package/index.d.ts +0 -5
  78. package/msal.broadcast.config.d.ts +0 -3
  79. package/msal.broadcast.service.d.ts +0 -19
  80. package/msal.guard.config.d.ts +0 -9
  81. package/msal.guard.d.ts +0 -43
  82. package/msal.interceptor.config.d.ts +0 -13
  83. package/msal.interceptor.d.ts +0 -70
  84. package/msal.module.d.ts +0 -13
  85. package/msal.navigation.client.d.ts +0 -19
  86. package/msal.redirect.component.d.ts +0 -15
  87. package/msal.service.d.ts +0 -33
  88. package/packageMetadata.d.ts +0 -2
@@ -0,0 +1,388 @@
1
+ /*
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { Injectable, Inject } from "@angular/core";
7
+ import { Location, DOCUMENT } from "@angular/common";
8
+ import {
9
+ HttpRequest,
10
+ HttpHandler,
11
+ HttpEvent,
12
+ HttpInterceptor,
13
+ } from "@angular/common/http"; // eslint-disable-line import/no-unresolved
14
+ import {
15
+ AccountInfo,
16
+ AuthenticationResult,
17
+ BrowserConfigurationAuthError,
18
+ InteractionStatus,
19
+ InteractionType,
20
+ StringUtils,
21
+ } from "@azure/msal-browser";
22
+ import { Observable, EMPTY, of } from "rxjs";
23
+ import { switchMap, catchError, take, filter } from "rxjs/operators";
24
+ import { MsalService } from "./msal.service";
25
+ import {
26
+ MsalInterceptorAuthRequest,
27
+ MsalInterceptorConfiguration,
28
+ ProtectedResourceScopes,
29
+ } from "./msal.interceptor.config";
30
+ import { MsalBroadcastService } from "./msal.broadcast.service";
31
+ import { MSAL_INTERCEPTOR_CONFIG } from "./constants";
32
+
33
+ @Injectable()
34
+ export class MsalInterceptor implements HttpInterceptor {
35
+ private _document?: Document;
36
+
37
+ constructor(
38
+ @Inject(MSAL_INTERCEPTOR_CONFIG)
39
+ private msalInterceptorConfig: MsalInterceptorConfiguration,
40
+ private authService: MsalService,
41
+ private location: Location,
42
+ private msalBroadcastService: MsalBroadcastService,
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
44
+ @Inject(DOCUMENT) document?: any
45
+ ) {
46
+ this._document = document as Document;
47
+ }
48
+
49
+ intercept(
50
+ req: HttpRequest<any>, // eslint-disable-line @typescript-eslint/no-explicit-any
51
+ next: HttpHandler
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ ): Observable<HttpEvent<any>> {
54
+ if (
55
+ this.msalInterceptorConfig.interactionType !== InteractionType.Popup &&
56
+ this.msalInterceptorConfig.interactionType !== InteractionType.Redirect
57
+ ) {
58
+ throw new BrowserConfigurationAuthError(
59
+ "invalid_interaction_type",
60
+ "Invalid interaction type provided to MSAL Interceptor. InteractionType.Popup, InteractionType.Redirect must be provided in the msalInterceptorConfiguration"
61
+ );
62
+ }
63
+
64
+ this.authService.getLogger().verbose("MSAL Interceptor activated");
65
+ const scopes = this.getScopesForEndpoint(req.url, req.method);
66
+
67
+ // If no scopes for endpoint, does not acquire token
68
+ if (!scopes || scopes.length === 0) {
69
+ this.authService
70
+ .getLogger()
71
+ .verbose("Interceptor - no scopes for endpoint");
72
+ return next.handle(req);
73
+ }
74
+
75
+ // Sets account as active account or first account
76
+ let account: AccountInfo;
77
+ if (!!this.authService.instance.getActiveAccount()) {
78
+ this.authService
79
+ .getLogger()
80
+ .verbose("Interceptor - active account selected");
81
+ account = this.authService.instance.getActiveAccount();
82
+ } else {
83
+ this.authService
84
+ .getLogger()
85
+ .verbose("Interceptor - no active account, fallback to first account");
86
+ account = this.authService.instance.getAllAccounts()[0];
87
+ }
88
+
89
+ const authRequest =
90
+ typeof this.msalInterceptorConfig.authRequest === "function"
91
+ ? this.msalInterceptorConfig.authRequest(this.authService, req, {
92
+ account: account,
93
+ })
94
+ : { ...this.msalInterceptorConfig.authRequest, account };
95
+
96
+ this.authService
97
+ .getLogger()
98
+ .info(`Interceptor - ${scopes.length} scopes found for endpoint`);
99
+ this.authService
100
+ .getLogger()
101
+ .infoPii(`Interceptor - [${scopes}] scopes found for ${req.url}`);
102
+
103
+ return this.acquireToken(authRequest, scopes, account).pipe(
104
+ switchMap((result: AuthenticationResult) => {
105
+ this.authService
106
+ .getLogger()
107
+ .verbose("Interceptor - setting authorization headers");
108
+ const headers = req.headers.set(
109
+ "Authorization",
110
+ `Bearer ${result.accessToken}`
111
+ );
112
+
113
+ const requestClone = req.clone({ headers });
114
+ return next.handle(requestClone);
115
+ })
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Try to acquire token silently. Invoke interaction if acquireTokenSilent rejected with error or resolved with null access token
121
+ * @param authRequest Request
122
+ * @param scopes Array of scopes for the request
123
+ * @param account Account
124
+ * @returns Authentication result
125
+ */
126
+ private acquireToken(
127
+ authRequest: MsalInterceptorAuthRequest,
128
+ scopes: string[],
129
+ account: AccountInfo
130
+ ): Observable<AuthenticationResult> {
131
+ // Note: For MSA accounts, include openid scope when calling acquireTokenSilent to return idToken
132
+ return this.authService
133
+ .acquireTokenSilent({ ...authRequest, scopes, account })
134
+ .pipe(
135
+ catchError(() => {
136
+ this.authService
137
+ .getLogger()
138
+ .error(
139
+ "Interceptor - acquireTokenSilent rejected with error. Invoking interaction to resolve."
140
+ );
141
+ return this.msalBroadcastService.inProgress$.pipe(
142
+ take(1),
143
+ switchMap((status: InteractionStatus) => {
144
+ if (status === InteractionStatus.None) {
145
+ return this.acquireTokenInteractively(authRequest, scopes);
146
+ }
147
+
148
+ return this.msalBroadcastService.inProgress$.pipe(
149
+ filter(
150
+ (status: InteractionStatus) =>
151
+ status === InteractionStatus.None
152
+ ),
153
+ take(1),
154
+ switchMap(() => this.acquireToken(authRequest, scopes, account))
155
+ );
156
+ })
157
+ );
158
+ }),
159
+ switchMap((result: AuthenticationResult) => {
160
+ if (!result.accessToken) {
161
+ this.authService
162
+ .getLogger()
163
+ .error(
164
+ "Interceptor - acquireTokenSilent resolved with null access token. Known issue with B2C tenants, invoking interaction to resolve."
165
+ );
166
+ return this.msalBroadcastService.inProgress$.pipe(
167
+ filter(
168
+ (status: InteractionStatus) => status === InteractionStatus.None
169
+ ),
170
+ take(1),
171
+ switchMap(() =>
172
+ this.acquireTokenInteractively(authRequest, scopes)
173
+ )
174
+ );
175
+ }
176
+ return of(result);
177
+ })
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Invoke interaction for the given set of scopes
183
+ * @param authRequest Request
184
+ * @param scopes Array of scopes for the request
185
+ * @returns Result from the interactive request
186
+ */
187
+ private acquireTokenInteractively(
188
+ authRequest: MsalInterceptorAuthRequest,
189
+ scopes: string[]
190
+ ): Observable<AuthenticationResult> {
191
+ if (this.msalInterceptorConfig.interactionType === InteractionType.Popup) {
192
+ this.authService
193
+ .getLogger()
194
+ .verbose(
195
+ "Interceptor - error acquiring token silently, acquiring by popup"
196
+ );
197
+ return this.authService.acquireTokenPopup({ ...authRequest, scopes });
198
+ }
199
+ this.authService
200
+ .getLogger()
201
+ .verbose(
202
+ "Interceptor - error acquiring token silently, acquiring by redirect"
203
+ );
204
+ const redirectStartPage = window.location.href;
205
+ this.authService.acquireTokenRedirect({
206
+ ...authRequest,
207
+ scopes,
208
+ redirectStartPage,
209
+ });
210
+ return EMPTY;
211
+ }
212
+
213
+ /**
214
+ * Looks up the scopes for the given endpoint from the protectedResourceMap
215
+ * @param endpoint Url of the request
216
+ * @param httpMethod Http method of the request
217
+ * @returns Array of scopes, or null if not found
218
+ *
219
+ */
220
+ private getScopesForEndpoint(
221
+ endpoint: string,
222
+ httpMethod: string
223
+ ): Array<string> | null {
224
+ this.authService
225
+ .getLogger()
226
+ .verbose("Interceptor - getting scopes for endpoint");
227
+
228
+ // Ensures endpoints and protected resources compared are normalized
229
+ const normalizedEndpoint = this.location.normalize(endpoint);
230
+
231
+ const protectedResourcesArray = Array.from(
232
+ this.msalInterceptorConfig.protectedResourceMap.keys()
233
+ );
234
+
235
+ const matchingProtectedResources = this.matchResourcesToEndpoint(
236
+ protectedResourcesArray,
237
+ normalizedEndpoint
238
+ );
239
+
240
+ if (matchingProtectedResources.length > 0) {
241
+ return this.matchScopesToEndpoint(
242
+ this.msalInterceptorConfig.protectedResourceMap,
243
+ matchingProtectedResources,
244
+ httpMethod
245
+ );
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * Finds resource endpoints that match request endpoint
253
+ * @param protectedResourcesEndpoints
254
+ * @param endpoint
255
+ * @returns
256
+ */
257
+ private matchResourcesToEndpoint(
258
+ protectedResourcesEndpoints: string[],
259
+ endpoint: string
260
+ ): Array<string> {
261
+ const matchingResources: Array<string> = [];
262
+
263
+ protectedResourcesEndpoints.forEach((key) => {
264
+ const normalizedKey = this.location.normalize(key);
265
+
266
+ // Get url components
267
+ const absoluteKey = this.getAbsoluteUrl(normalizedKey);
268
+ const keyComponents = new URL(absoluteKey);
269
+ const absoluteEndpoint = this.getAbsoluteUrl(endpoint);
270
+ const endpointComponents = new URL(absoluteEndpoint);
271
+
272
+ if (this.checkUrlComponents(keyComponents, endpointComponents)) {
273
+ matchingResources.push(key);
274
+ }
275
+ });
276
+
277
+ return matchingResources;
278
+ }
279
+
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
+
307
+ /**
308
+ * Transforms relative urls to absolute urls
309
+ * @param url
310
+ * @returns
311
+ */
312
+ private getAbsoluteUrl(url: string): string {
313
+ const link = this._document.createElement("a");
314
+ link.href = url;
315
+ return link.href;
316
+ }
317
+
318
+ /**
319
+ * Finds scopes from first matching endpoint with HTTP method that matches request
320
+ * @param protectedResourceMap Protected resource map
321
+ * @param endpointArray Array of resources that match request endpoint
322
+ * @param httpMethod Http method of the request
323
+ * @returns
324
+ */
325
+ private matchScopesToEndpoint(
326
+ protectedResourceMap: Map<
327
+ string,
328
+ Array<string | ProtectedResourceScopes> | null
329
+ >,
330
+ endpointArray: string[],
331
+ httpMethod: string
332
+ ): Array<string> | null {
333
+ const allMatchedScopes = [];
334
+
335
+ // Check each matched endpoint for matching HttpMethod and scopes
336
+ endpointArray.forEach((matchedEndpoint) => {
337
+ const scopesForEndpoint = [];
338
+ const methodAndScopesArray = protectedResourceMap.get(matchedEndpoint);
339
+
340
+ // Return if resource is unprotected
341
+ if (methodAndScopesArray === null) {
342
+ allMatchedScopes.push(null);
343
+ return;
344
+ }
345
+
346
+ methodAndScopesArray.forEach((entry) => {
347
+ // Entry is either array of scopes or ProtectedResourceScopes object
348
+ if (typeof entry === "string") {
349
+ scopesForEndpoint.push(entry);
350
+ } else {
351
+ // Ensure methods being compared are normalized
352
+ const normalizedRequestMethod = httpMethod.toLowerCase();
353
+ const normalizedResourceMethod = entry.httpMethod.toLowerCase();
354
+ // Method in protectedResourceMap matches request http method
355
+ if (normalizedResourceMethod === normalizedRequestMethod) {
356
+ // Validate if scopes comes null to unprotect the resource in a certain http method
357
+ if (entry.scopes === null) {
358
+ allMatchedScopes.push(null);
359
+ } else {
360
+ entry.scopes.forEach((scope) => {
361
+ scopesForEndpoint.push(scope);
362
+ });
363
+ }
364
+ }
365
+ }
366
+ });
367
+
368
+ // Only add to all scopes if scopes for endpoint and method is found
369
+ if (scopesForEndpoint.length > 0) {
370
+ allMatchedScopes.push(scopesForEndpoint);
371
+ }
372
+ });
373
+
374
+ if (allMatchedScopes.length > 0) {
375
+ if (allMatchedScopes.length > 1) {
376
+ this.authService
377
+ .getLogger()
378
+ .warning(
379
+ "Interceptor - More than 1 matching scopes for endpoint found."
380
+ );
381
+ }
382
+ // Returns scopes for first matching endpoint
383
+ return allMatchedScopes[0];
384
+ }
385
+
386
+ return null;
387
+ }
388
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ModuleWithProviders, NgModule } from "@angular/core";
7
+ import { CommonModule } from "@angular/common";
8
+ import { IPublicClientApplication } from "@azure/msal-browser";
9
+ import { MsalBroadcastService } from "./msal.broadcast.service";
10
+ import { MsalGuard } from "./msal.guard";
11
+ import { MsalGuardConfiguration } from "./msal.guard.config";
12
+ import { MsalInterceptorConfiguration } from "./msal.interceptor.config";
13
+ import { MsalRedirectComponent } from "./msal.redirect.component";
14
+ import { MsalService } from "./msal.service";
15
+ import {
16
+ MSAL_INSTANCE,
17
+ MSAL_GUARD_CONFIG,
18
+ MSAL_INTERCEPTOR_CONFIG,
19
+ } from "./constants";
20
+
21
+ @NgModule({
22
+ declarations: [MsalRedirectComponent],
23
+ imports: [CommonModule],
24
+ providers: [MsalGuard, MsalBroadcastService],
25
+ })
26
+ export class MsalModule {
27
+ static forRoot(
28
+ msalInstance: IPublicClientApplication,
29
+ guardConfig: MsalGuardConfiguration,
30
+ interceptorConfig: MsalInterceptorConfiguration
31
+ ): ModuleWithProviders<MsalModule> {
32
+ return {
33
+ ngModule: MsalModule,
34
+ providers: [
35
+ {
36
+ provide: MSAL_INSTANCE,
37
+ useValue: msalInstance,
38
+ },
39
+ {
40
+ provide: MSAL_GUARD_CONFIG,
41
+ useValue: guardConfig,
42
+ },
43
+ {
44
+ provide: MSAL_INTERCEPTOR_CONFIG,
45
+ useValue: interceptorConfig,
46
+ },
47
+ MsalService,
48
+ ],
49
+ };
50
+ }
51
+ }
@@ -0,0 +1,96 @@
1
+ import { TestBed } from "@angular/core/testing";
2
+ import { Location } from "@angular/common";
3
+ import { Router } from "@angular/router";
4
+ import {
5
+ InteractionType,
6
+ NavigationClient,
7
+ NavigationOptions,
8
+ PublicClientApplication,
9
+ } from "@azure/msal-browser";
10
+ import { MsalBroadcastService } from "./msal.broadcast.service";
11
+ import { MsalGuard } from "./msal.guard";
12
+ import { MsalCustomNavigationClient } from "./msal.navigation.client";
13
+ import { MsalModule, MsalService } from "./public-api";
14
+
15
+ let authService: MsalService;
16
+ let navigationClient: MsalCustomNavigationClient;
17
+ let routerMock = {
18
+ navigateByUrl: jasmine.createSpy("navigateByUrl").and.resolveTo(),
19
+ };
20
+
21
+ const msalInstance = new PublicClientApplication({
22
+ auth: {
23
+ clientId: "b5c2e510-4a17-4feb-b219-e55aa5b74144",
24
+ redirectUri: "http://localhost:4200",
25
+ },
26
+ });
27
+
28
+ describe("MsalCustomNaviationClient", () => {
29
+ beforeEach(() => {
30
+ TestBed.resetTestingModule();
31
+
32
+ TestBed.configureTestingModule({
33
+ imports: [
34
+ MsalModule.forRoot(msalInstance, null, {
35
+ interactionType: InteractionType.Popup,
36
+ protectedResourceMap: new Map(),
37
+ }),
38
+ ],
39
+ providers: [
40
+ MsalBroadcastService,
41
+ { provide: Router, useValue: routerMock },
42
+ Location,
43
+ MsalCustomNavigationClient,
44
+ MsalService,
45
+ MsalGuard,
46
+ ],
47
+ teardown: { destroyAfterEach: false },
48
+ });
49
+ authService = TestBed.inject(MsalService);
50
+ navigationClient = TestBed.inject(MsalCustomNavigationClient);
51
+ });
52
+
53
+ describe("NavigateInternal Unit tests", () => {
54
+ it("is created", () => {
55
+ expect(navigationClient).toBeTruthy();
56
+ });
57
+
58
+ it("NavigateInternal (noHistory false)", (done) => {
59
+ const url = "http://localhost:4200/profile";
60
+ const normalizedAbsoluteUrl = "/profile";
61
+
62
+ const options = {
63
+ noHistory: false,
64
+ } as NavigationOptions;
65
+
66
+ const navigateByUrlOptions = {
67
+ replaceUrl: false,
68
+ };
69
+
70
+ navigationClient.navigateInternal(url, options).then(() => {
71
+ expect(routerMock.navigateByUrl).toHaveBeenCalledWith(
72
+ normalizedAbsoluteUrl,
73
+ navigateByUrlOptions
74
+ );
75
+ done();
76
+ });
77
+ });
78
+
79
+ it("NavigateInternal (noHistory true)", (done) => {
80
+ const url = "http://localhost:4200/profile";
81
+
82
+ const options = {
83
+ noHistory: true,
84
+ } as NavigationOptions;
85
+
86
+ const windowLocationReplaceSpy = spyOn(
87
+ NavigationClient.prototype,
88
+ "navigateInternal"
89
+ );
90
+ navigationClient.navigateInternal(url, options).then(() => {
91
+ expect(windowLocationReplaceSpy).toHaveBeenCalledWith(url, options);
92
+ done();
93
+ });
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { Injectable } from "@angular/core";
7
+ import { Location } from "@angular/common";
8
+ import { Router } from "@angular/router";
9
+ import {
10
+ NavigationClient,
11
+ NavigationOptions,
12
+ UrlString,
13
+ } from "@azure/msal-browser";
14
+ import { MsalService } from "./msal.service";
15
+
16
+ /**
17
+ * Custom navigation used for Angular client-side navigation.
18
+ * See performance doc for details:
19
+ * https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-angular/docs/performance.md
20
+ */
21
+ @Injectable()
22
+ export class MsalCustomNavigationClient extends NavigationClient {
23
+ constructor(
24
+ private authService: MsalService,
25
+ private router: Router,
26
+ private location: Location
27
+ ) {
28
+ super();
29
+ }
30
+
31
+ async navigateInternal(
32
+ url: string,
33
+ options: NavigationOptions
34
+ ): Promise<boolean> {
35
+ this.authService.getLogger().trace("MsalCustomNavigationClient called");
36
+
37
+ this.authService
38
+ .getLogger()
39
+ .verbose("MsalCustomNavigationClient - navigating");
40
+ this.authService
41
+ .getLogger()
42
+ .verbosePii(`MsalCustomNavigationClient - navigating to url: ${url}`);
43
+
44
+ // Prevent hash clearing from causing an issue with Client-side navigation after redirect is handled
45
+ if (options.noHistory) {
46
+ return super.navigateInternal(url, options);
47
+ } else {
48
+ // Normalizing newUrl if no query string
49
+ const urlComponents = new UrlString(url).getUrlComponents();
50
+ const newUrl = urlComponents.QueryString
51
+ ? `${urlComponents.AbsolutePath}?${urlComponents.QueryString}`
52
+ : this.location.normalize(urlComponents.AbsolutePath);
53
+ await this.router.navigateByUrl(newUrl, {
54
+ replaceUrl: options.noHistory,
55
+ });
56
+ }
57
+ return Promise.resolve(options.noHistory);
58
+ }
59
+ }
@@ -0,0 +1,64 @@
1
+ import { TestBed } from "@angular/core/testing";
2
+ import {
3
+ IPublicClientApplication,
4
+ PublicClientApplication,
5
+ } from "@azure/msal-browser";
6
+ import { MsalBroadcastService } from "./msal.broadcast.service";
7
+ import { MsalModule } from "./msal.module";
8
+ import { MsalRedirectComponent } from "./msal.redirect.component";
9
+ import { MsalService } from "./msal.service";
10
+
11
+ let authService: MsalService;
12
+ let broadcastService: MsalBroadcastService;
13
+
14
+ function MSALInstanceFactory(): IPublicClientApplication {
15
+ return new PublicClientApplication({
16
+ auth: {
17
+ clientId: "b5c2e510-4a17-4feb-b219-e55aa5b74144",
18
+ redirectUri: "http://localhost:4200",
19
+ },
20
+ });
21
+ }
22
+
23
+ function initializeMsal() {
24
+ TestBed.resetTestingModule();
25
+
26
+ TestBed.configureTestingModule({
27
+ declarations: [MsalRedirectComponent],
28
+ imports: [MsalModule.forRoot(MSALInstanceFactory(), null, null)],
29
+ providers: [],
30
+ teardown: { destroyAfterEach: false },
31
+ });
32
+
33
+ authService = TestBed.inject(MsalService);
34
+ broadcastService = TestBed.inject(MsalBroadcastService);
35
+ }
36
+
37
+ describe("MsalRedirectComponent", () => {
38
+ beforeEach(initializeMsal);
39
+
40
+ it("calls handleRedirectObservable on ngInit", (done) => {
41
+ const sampleAccessToken = {
42
+ accessToken: "123abc",
43
+ };
44
+
45
+ spyOn(
46
+ PublicClientApplication.prototype,
47
+ "handleRedirectPromise"
48
+ ).and.callFake(() => {
49
+ return new Promise((resolve) => {
50
+ console.log("Spy called");
51
+ //@ts-ignore
52
+ resolve(sampleAccessToken);
53
+
54
+ expect(
55
+ PublicClientApplication.prototype.handleRedirectPromise
56
+ ).toHaveBeenCalled();
57
+ done();
58
+ });
59
+ });
60
+
61
+ const component = new MsalRedirectComponent(authService);
62
+ component.ngOnInit();
63
+ });
64
+ });
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ /**
7
+ * This is a dedicated redirect component to be added to Angular apps to
8
+ * handle redirects when using @azure/msal-angular.
9
+ * Import this component to use redirects in your app.
10
+ */
11
+
12
+ import { Component, OnInit } from "@angular/core";
13
+ import { MsalService } from "./msal.service";
14
+
15
+ @Component({
16
+ selector: "app-redirect",
17
+ template: "",
18
+ })
19
+ export class MsalRedirectComponent implements OnInit {
20
+ constructor(private authService: MsalService) {}
21
+
22
+ ngOnInit(): void {
23
+ this.authService.getLogger().verbose("MsalRedirectComponent activated");
24
+ this.authService.handleRedirectObservable().subscribe();
25
+ }
26
+ }