@azure/msal-angular 4.0.11 → 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,1168 @@
1
+ import { TestBed } from "@angular/core/testing";
2
+ import { Location } from "@angular/common";
3
+ import { HTTP_INTERCEPTORS, HttpClient } from "@angular/common/http";
4
+ import {
5
+ HttpClientTestingModule,
6
+ HttpTestingController,
7
+ } from "@angular/common/http/testing";
8
+ import { RouterTestingModule } from "@angular/router/testing";
9
+ import {
10
+ AccountInfo,
11
+ AuthError,
12
+ InteractionStatus,
13
+ InteractionType,
14
+ IPublicClientApplication,
15
+ PublicClientApplication,
16
+ SilentRequest,
17
+ } from "@azure/msal-browser";
18
+ import { BehaviorSubject } from "rxjs";
19
+ import {
20
+ MsalModule,
21
+ MsalService,
22
+ MsalInterceptor,
23
+ MsalBroadcastService,
24
+ MsalInterceptorConfiguration,
25
+ ProtectedResourceScopes,
26
+ } from "./public-api";
27
+
28
+ let interceptor: MsalInterceptor;
29
+ let httpMock: HttpTestingController;
30
+ let httpClient: HttpClient;
31
+ let testInteractionType: InteractionType;
32
+
33
+ let testInterceptorConfig: Partial<MsalInterceptorConfiguration> = {};
34
+
35
+ const sampleAccountInfo: AccountInfo = {
36
+ homeAccountId: "test",
37
+ localAccountId: "test",
38
+ environment: "test",
39
+ tenantId: "test-tenant",
40
+ username: "test",
41
+ };
42
+
43
+ function MSALInstanceFactory(): IPublicClientApplication {
44
+ return new PublicClientApplication({
45
+ auth: {
46
+ clientId: "b5c2e510-4a17-4feb-b219-e55aa5b74144",
47
+ redirectUri: "http://localhost:4200",
48
+ },
49
+ });
50
+ }
51
+
52
+ function MSALInterceptorFactory(): MsalInterceptorConfiguration {
53
+ return {
54
+ //@ts-ignore
55
+ interactionType: testInteractionType,
56
+ protectedResourceMap: new Map<
57
+ string,
58
+ Array<string | ProtectedResourceScopes> | null
59
+ >([
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"]],
62
+ ["https://graph.microsoft.com/v1.0/me", ["user.read"]],
63
+ ["relative/me", ["relative.scope"]],
64
+ ["https://myapplication.com/user/*", ["customscope.read"]],
65
+ ["https://*.myapplication.com/*", ["mail.read"]],
66
+ ["https://api.test.com", ["default.scope1"]],
67
+ ["https://*.test.com", ["default.scope2"]],
68
+ ["http://localhost:3000/unprotect", null],
69
+ [
70
+ "http://localhost:3000/unprotect/post",
71
+ [{ httpMethod: "POST", scopes: null }],
72
+ ],
73
+ ["http://localhost:3000/", ["base.scope"]],
74
+ ["http://localhost:9876/tenant?abc", ["query.scope"]],
75
+ ["http://applicationA/slash/", ["customA.scope"]],
76
+ ["http://applicationB/noSlash", ["customB.scope"]],
77
+ [
78
+ "http://applicationC.com",
79
+ [
80
+ {
81
+ httpMethod: "POST",
82
+ scopes: ["write.scope"],
83
+ },
84
+ ],
85
+ ],
86
+ [
87
+ "http://applicationD.com",
88
+ [
89
+ "all.scope",
90
+ {
91
+ httpMethod: "GET",
92
+ scopes: ["read.scope"],
93
+ },
94
+ {
95
+ httpMethod: "Post",
96
+ scopes: ["info.scope"],
97
+ },
98
+ ],
99
+ ],
100
+ ["http://applicationE.com/profile/", ["customE.scope"]],
101
+ ["http://applicationF.com/profile/", ["customF.scope"]],
102
+ ]),
103
+ authRequest: testInterceptorConfig.authRequest,
104
+ };
105
+ }
106
+
107
+ function initializeMsal() {
108
+ TestBed.resetTestingModule();
109
+
110
+ TestBed.configureTestingModule({
111
+ imports: [
112
+ HttpClientTestingModule,
113
+ RouterTestingModule,
114
+ MsalModule.forRoot(MSALInstanceFactory(), null, MSALInterceptorFactory()),
115
+ ],
116
+ providers: [
117
+ MsalInterceptor,
118
+ MsalService,
119
+ MsalBroadcastService,
120
+ {
121
+ provide: HTTP_INTERCEPTORS,
122
+ useClass: MsalInterceptor,
123
+ multi: true,
124
+ },
125
+ Location,
126
+ ],
127
+ teardown: { destroyAfterEach: false },
128
+ });
129
+
130
+ interceptor = TestBed.inject(MsalInterceptor);
131
+ httpMock = TestBed.inject(HttpTestingController);
132
+ httpClient = TestBed.inject(HttpClient);
133
+ }
134
+
135
+ describe("MsalInterceptor", () => {
136
+ beforeEach(() => {
137
+ testInteractionType = InteractionType.Popup;
138
+ testInterceptorConfig = {};
139
+ initializeMsal();
140
+ });
141
+
142
+ it("throws error if incorrect interaction type set in interceptor configuration", (done) => {
143
+ testInteractionType = InteractionType.Silent;
144
+ initializeMsal();
145
+
146
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe({
147
+ error: (error) => {
148
+ expect(error.errorCode).toBe("invalid_interaction_type");
149
+ expect(error.errorMessage).toBe(
150
+ "Invalid interaction type provided to MSAL Interceptor. InteractionType.Popup, InteractionType.Redirect must be provided in the msalInterceptorConfiguration"
151
+ );
152
+ testInteractionType = InteractionType.Popup;
153
+ done();
154
+ },
155
+ });
156
+ });
157
+
158
+ it("does not attach authorization header for unprotected resource", (done) => {
159
+ httpClient
160
+ .get("http://localhost/api")
161
+ .subscribe((response) => expect(response).toBeTruthy());
162
+
163
+ const request = httpMock.expectOne("http://localhost/api");
164
+ request.flush({ data: "test" });
165
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
166
+ httpMock.verify();
167
+ done();
168
+ });
169
+
170
+ it("does not attach authorization header for own domain", (done) => {
171
+ httpClient
172
+ .get("http://localhost:9876")
173
+ .subscribe((response) => expect(response).toBeTruthy());
174
+
175
+ const request = httpMock.expectOne("http://localhost:9876");
176
+ request.flush({ data: "test" });
177
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
178
+ httpMock.verify();
179
+ done();
180
+ });
181
+
182
+ it("attaches authorization header with access token for protected resource with exact match", (done) => {
183
+ const spy = spyOn(
184
+ PublicClientApplication.prototype,
185
+ "acquireTokenSilent"
186
+ ).and.returnValue(
187
+ new Promise((resolve) => {
188
+ //@ts-ignore
189
+ resolve({
190
+ accessToken: "access-token",
191
+ });
192
+ })
193
+ );
194
+
195
+ spyOn(
196
+ PublicClientApplication.prototype,
197
+ "getActiveAccount"
198
+ ).and.returnValue(sampleAccountInfo);
199
+
200
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe();
201
+ setTimeout(() => {
202
+ const request = httpMock.expectOne("https://graph.microsoft.com/v1.0/me");
203
+ request.flush({ data: "test" });
204
+ expect(request.request.headers.get("Authorization")).toEqual(
205
+ "Bearer access-token"
206
+ );
207
+ expect(spy).toHaveBeenCalledWith({
208
+ account: sampleAccountInfo,
209
+ scopes: ["user.read"],
210
+ });
211
+ httpMock.verify();
212
+ done();
213
+ }, 200);
214
+ });
215
+
216
+ it("attaches authorization header with access token via interaction if acquireTokenSilent returns null access token, interaction type is Popup and interaction invocation waits for interaction status becomes None", (done) => {
217
+ const _inProgress = new BehaviorSubject<InteractionStatus>(
218
+ InteractionStatus.Startup
219
+ );
220
+ const msalBroadcastService = TestBed.inject(MsalBroadcastService);
221
+
222
+ msalBroadcastService.inProgress$ = _inProgress.asObservable();
223
+
224
+ const spy1 = spyOn(
225
+ PublicClientApplication.prototype,
226
+ "acquireTokenSilent"
227
+ ).and.returnValue(
228
+ new Promise((resolve) => {
229
+ //@ts-ignore
230
+ resolve({
231
+ accessToken: null,
232
+ });
233
+ })
234
+ );
235
+
236
+ const spy2 = spyOn(
237
+ PublicClientApplication.prototype,
238
+ "acquireTokenPopup"
239
+ ).and.returnValue(
240
+ new Promise((resolve) => {
241
+ //@ts-ignore
242
+ resolve({
243
+ accessToken: "access-token",
244
+ });
245
+ })
246
+ );
247
+
248
+ spyOn(
249
+ PublicClientApplication.prototype,
250
+ "getActiveAccount"
251
+ ).and.returnValue(sampleAccountInfo);
252
+
253
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe();
254
+ setTimeout(() => {
255
+ const request = httpMock.expectNone(
256
+ "https://graph.microsoft.com/v1.0/me"
257
+ );
258
+ expect(request).toBeUndefined();
259
+ expect(spy1).toHaveBeenCalledWith({
260
+ account: sampleAccountInfo,
261
+ scopes: ["user.read"],
262
+ });
263
+ expect(spy2).not.toHaveBeenCalled();
264
+ httpMock.verify();
265
+
266
+ _inProgress.next(InteractionStatus.None);
267
+ }, 200);
268
+
269
+ setTimeout(() => {
270
+ const request = httpMock.expectOne("https://graph.microsoft.com/v1.0/me");
271
+ request.flush({ data: "test" });
272
+ expect(request.request.headers.get("Authorization")).toEqual(
273
+ "Bearer access-token"
274
+ );
275
+ expect(spy2).toHaveBeenCalledWith({
276
+ account: sampleAccountInfo,
277
+ scopes: ["user.read"],
278
+ });
279
+ httpMock.verify();
280
+ done();
281
+ }, 400);
282
+ });
283
+
284
+ it("attaches authorization header with access token for protected resource with wildcard", (done) => {
285
+ const spy = spyOn(
286
+ PublicClientApplication.prototype,
287
+ "acquireTokenSilent"
288
+ ).and.returnValue(
289
+ new Promise((resolve) => {
290
+ //@ts-ignore
291
+ resolve({
292
+ accessToken: "access-token",
293
+ });
294
+ })
295
+ );
296
+
297
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
298
+ sampleAccountInfo,
299
+ ]);
300
+
301
+ httpClient.get("https://myapplication.com/user/1").subscribe();
302
+ setTimeout(() => {
303
+ const request = httpMock.expectOne("https://myapplication.com/user/1");
304
+ request.flush({ data: "test" });
305
+ expect(request.request.headers.get("Authorization")).toEqual(
306
+ "Bearer access-token"
307
+ );
308
+ expect(spy).toHaveBeenCalledWith({
309
+ account: sampleAccountInfo,
310
+ scopes: ["customscope.read"],
311
+ });
312
+ httpMock.verify();
313
+ done();
314
+ }, 200);
315
+ });
316
+
317
+ it("attaches authorization header with access token to url for protected resource with wildcard, url has multiple slashes", (done) => {
318
+ const spy = spyOn(
319
+ PublicClientApplication.prototype,
320
+ "acquireTokenSilent"
321
+ ).and.returnValue(
322
+ new Promise((resolve) => {
323
+ //@ts-ignore
324
+ resolve({
325
+ accessToken: "access-token",
326
+ });
327
+ })
328
+ );
329
+
330
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
331
+ sampleAccountInfo,
332
+ ]);
333
+
334
+ httpClient.get("https://myapplication.com/user/1/2/3").subscribe();
335
+ setTimeout(() => {
336
+ const request = httpMock.expectOne(
337
+ "https://myapplication.com/user/1/2/3"
338
+ );
339
+ request.flush({ data: "test" });
340
+ expect(request.request.headers.get("Authorization")).toEqual(
341
+ "Bearer access-token"
342
+ );
343
+ expect(spy).toHaveBeenCalledWith({
344
+ account: sampleAccountInfo,
345
+ scopes: ["customscope.read"],
346
+ });
347
+ httpMock.verify();
348
+ done();
349
+ }, 200);
350
+ });
351
+
352
+ it("attaches authorization header with access token for protected resource with multiple wildcards", (done) => {
353
+ const spy = spyOn(
354
+ PublicClientApplication.prototype,
355
+ "acquireTokenSilent"
356
+ ).and.returnValue(
357
+ new Promise((resolve) => {
358
+ //@ts-ignore
359
+ resolve({
360
+ accessToken: "access-token",
361
+ });
362
+ })
363
+ );
364
+
365
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
366
+ sampleAccountInfo,
367
+ ]);
368
+
369
+ httpClient.get("https://mail.myapplication.com/me").subscribe();
370
+ setTimeout(() => {
371
+ const request = httpMock.expectOne("https://mail.myapplication.com/me");
372
+ request.flush({ data: "test" });
373
+ expect(request.request.headers.get("Authorization")).toEqual(
374
+ "Bearer access-token"
375
+ );
376
+ expect(spy).toHaveBeenCalledWith({
377
+ account: sampleAccountInfo,
378
+ scopes: ["mail.read"],
379
+ });
380
+ httpMock.verify();
381
+ done();
382
+ }, 200);
383
+ });
384
+
385
+ it("attaches authorization header with access token for base url as protected resource", (done) => {
386
+ const spy = spyOn(
387
+ PublicClientApplication.prototype,
388
+ "acquireTokenSilent"
389
+ ).and.returnValue(
390
+ new Promise((resolve) => {
391
+ //@ts-ignore
392
+ resolve({
393
+ accessToken: "access-token",
394
+ });
395
+ })
396
+ );
397
+
398
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
399
+ sampleAccountInfo,
400
+ ]);
401
+
402
+ httpClient.get("http://localhost:3000/base").subscribe();
403
+ setTimeout(() => {
404
+ const request = httpMock.expectOne("http://localhost:3000/base");
405
+ request.flush({ data: "test" });
406
+ expect(request.request.headers.get("Authorization")).toEqual(
407
+ "Bearer access-token"
408
+ );
409
+ expect(spy).toHaveBeenCalledWith({
410
+ account: sampleAccountInfo,
411
+ scopes: ["base.scope"],
412
+ });
413
+
414
+ httpMock.verify();
415
+ done();
416
+ }, 200);
417
+ });
418
+
419
+ it("attaches authorization header with access token for multiple matching entries in protected resource, scopes are for first matching entry", (done) => {
420
+ const spy = spyOn(
421
+ PublicClientApplication.prototype,
422
+ "acquireTokenSilent"
423
+ ).and.returnValue(
424
+ new Promise((resolve) => {
425
+ //@ts-ignore
426
+ resolve({
427
+ accessToken: "access-token",
428
+ });
429
+ })
430
+ );
431
+
432
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
433
+ sampleAccountInfo,
434
+ ]);
435
+
436
+ httpClient.get("https://api.test.com").subscribe();
437
+ setTimeout(() => {
438
+ const request = httpMock.expectOne("https://api.test.com");
439
+ request.flush({ data: "test" });
440
+ expect(request.request.headers.get("Authorization")).toEqual(
441
+ "Bearer access-token"
442
+ );
443
+ expect(spy).toHaveBeenCalledWith({
444
+ account: sampleAccountInfo,
445
+ scopes: ["default.scope1"],
446
+ });
447
+ httpMock.verify();
448
+ done();
449
+ }, 200);
450
+ });
451
+
452
+ it("does not attach authorization header when scopes set to null, and resource is before any base url or wildcards", (done) => {
453
+ httpClient
454
+ .get("http://localhost:3000/unprotect")
455
+ .subscribe((response) => expect(response).toBeTruthy());
456
+
457
+ const request = httpMock.expectOne("http://localhost:3000/unprotect");
458
+ request.flush({ data: "test" });
459
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
460
+ httpMock.verify();
461
+ done();
462
+ });
463
+
464
+ it("does not attach authorization header when scopes set to null on specific http method, and resource is before any base url or wildcards", (done) => {
465
+ httpClient
466
+ .post("http://localhost:3000/unprotect/post", {})
467
+ .subscribe((response) => expect(response).toBeTruthy());
468
+
469
+ const request = httpMock.expectOne("http://localhost:3000/unprotect/post");
470
+ request.flush({ data: "test" });
471
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
472
+ httpMock.verify();
473
+ done();
474
+ });
475
+
476
+ it("attaches authorization header with access token from acquireTokenPopup if acquireTokenSilent fails in interceptor, interaction type is Popup and interaction status is None", (done) => {
477
+ const _inProgress = new BehaviorSubject<InteractionStatus>(
478
+ InteractionStatus.None
479
+ );
480
+ const msalBroadcastService = TestBed.inject(MsalBroadcastService);
481
+
482
+ msalBroadcastService.inProgress$ = _inProgress.asObservable();
483
+
484
+ const sampleError = new AuthError("123", "message");
485
+ const sampleAccessToken = {
486
+ accessToken: "123abc",
487
+ };
488
+
489
+ const spy1 = spyOn(
490
+ PublicClientApplication.prototype,
491
+ "acquireTokenSilent"
492
+ ).and.returnValue(
493
+ new Promise((resolve, reject) => {
494
+ reject(sampleError);
495
+ })
496
+ );
497
+
498
+ const spy2 = spyOn(
499
+ PublicClientApplication.prototype,
500
+ "acquireTokenPopup"
501
+ ).and.returnValue(
502
+ new Promise((resolve) => {
503
+ //@ts-ignore
504
+ resolve(sampleAccessToken);
505
+ })
506
+ );
507
+
508
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
509
+ sampleAccountInfo,
510
+ ]);
511
+
512
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe();
513
+ setTimeout(() => {
514
+ const request = httpMock.expectOne("https://graph.microsoft.com/v1.0/me");
515
+ request.flush({ data: "test" });
516
+ expect(request.request.headers.get("Authorization")).toEqual(
517
+ "Bearer 123abc"
518
+ );
519
+ expect(spy1).toHaveBeenCalledWith({
520
+ account: sampleAccountInfo,
521
+ scopes: ["user.read"],
522
+ });
523
+ expect(spy2).toHaveBeenCalledWith({
524
+ account: sampleAccountInfo,
525
+ scopes: ["user.read"],
526
+ });
527
+ httpMock.verify();
528
+ done();
529
+ }, 200);
530
+ });
531
+
532
+ it("does not attach authorization header if acquireTokenSilent fails in interceptor, interaction type is Redirect and interaction status is None", (done) => {
533
+ testInteractionType = InteractionType.Redirect;
534
+ initializeMsal();
535
+
536
+ const _inProgress = new BehaviorSubject<InteractionStatus>(
537
+ InteractionStatus.None
538
+ );
539
+ const msalBroadcastService = TestBed.inject(MsalBroadcastService);
540
+
541
+ msalBroadcastService.inProgress$ = _inProgress.asObservable();
542
+
543
+ const sampleError = new AuthError("123", "message");
544
+
545
+ spyOn(
546
+ PublicClientApplication.prototype,
547
+ "acquireTokenSilent"
548
+ ).and.returnValue(
549
+ new Promise((resolve, reject) => {
550
+ reject(sampleError);
551
+ })
552
+ );
553
+
554
+ spyOn(
555
+ PublicClientApplication.prototype,
556
+ "acquireTokenRedirect"
557
+ ).and.returnValue(
558
+ new Promise((resolve) => {
559
+ //@ts-ignore
560
+ resolve();
561
+ })
562
+ );
563
+
564
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
565
+ sampleAccountInfo,
566
+ ]);
567
+
568
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe();
569
+ setTimeout(() => {
570
+ const request = httpMock.expectNone(
571
+ "https://graph.microsoft.com/v1.0/me"
572
+ );
573
+ expect(request).toBeUndefined();
574
+ httpMock.verify();
575
+ done();
576
+ }, 200);
577
+ });
578
+
579
+ it("does not invoke interaction if acquireTokenSilent fails in interceptor and interaction status other than None", (done) => {
580
+ const _inProgress = new BehaviorSubject<InteractionStatus>(
581
+ InteractionStatus.Startup
582
+ );
583
+ const msalBroadcastService = TestBed.inject(MsalBroadcastService);
584
+
585
+ msalBroadcastService.inProgress$ = _inProgress.asObservable();
586
+
587
+ const sampleError = new AuthError("123", "message");
588
+ const sampleAccessToken = {
589
+ accessToken: "123abc",
590
+ };
591
+
592
+ const spy1 = spyOn(
593
+ PublicClientApplication.prototype,
594
+ "acquireTokenSilent"
595
+ ).and.returnValue(
596
+ new Promise((resolve, reject) => {
597
+ reject(sampleError);
598
+ })
599
+ );
600
+
601
+ const spy2 = spyOn(
602
+ PublicClientApplication.prototype,
603
+ "acquireTokenPopup"
604
+ ).and.returnValue(
605
+ new Promise((resolve) => {
606
+ //@ts-ignore
607
+ resolve(sampleAccessToken);
608
+ })
609
+ );
610
+
611
+ const spy3 = spyOn(
612
+ PublicClientApplication.prototype,
613
+ "acquireTokenRedirect"
614
+ ).and.returnValue(
615
+ new Promise((resolve) => {
616
+ //@ts-ignore
617
+ resolve();
618
+ })
619
+ );
620
+
621
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
622
+ sampleAccountInfo,
623
+ ]);
624
+
625
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe();
626
+ setTimeout(() => {
627
+ const request = httpMock.expectNone(
628
+ "https://graph.microsoft.com/v1.0/me"
629
+ );
630
+ expect(request).toBeUndefined();
631
+ expect(spy1).toHaveBeenCalledWith({
632
+ account: sampleAccountInfo,
633
+ scopes: ["user.read"],
634
+ });
635
+ expect(spy2).not.toHaveBeenCalled();
636
+ expect(spy3).not.toHaveBeenCalled();
637
+ httpMock.verify();
638
+ done();
639
+ }, 200);
640
+ });
641
+
642
+ it("does not invoke interaction if acquireTokenSilent fails in interceptor and interaction status other than None and should invoke acquireTokenSilent when status became None", (done) => {
643
+ const _inProgress = new BehaviorSubject<InteractionStatus>(
644
+ InteractionStatus.Startup
645
+ );
646
+ const msalBroadcastService = TestBed.inject(MsalBroadcastService);
647
+
648
+ msalBroadcastService.inProgress$ = _inProgress.asObservable();
649
+
650
+ const sampleError = new AuthError("123", "message");
651
+ const sampleAccessToken = {
652
+ accessToken: "123abc",
653
+ };
654
+
655
+ const spy1 = spyOn(
656
+ PublicClientApplication.prototype,
657
+ "acquireTokenSilent"
658
+ ).and.returnValue(
659
+ new Promise((resolve, reject) => {
660
+ reject(sampleError);
661
+ })
662
+ );
663
+
664
+ const spy2 = spyOn(
665
+ PublicClientApplication.prototype,
666
+ "acquireTokenPopup"
667
+ ).and.returnValue(
668
+ new Promise((resolve) => {
669
+ //@ts-ignore
670
+ resolve(sampleAccessToken);
671
+ })
672
+ );
673
+
674
+ const spy3 = spyOn(
675
+ PublicClientApplication.prototype,
676
+ "acquireTokenRedirect"
677
+ ).and.returnValue(
678
+ new Promise((resolve) => {
679
+ //@ts-ignore
680
+ resolve();
681
+ })
682
+ );
683
+
684
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
685
+ sampleAccountInfo,
686
+ ]);
687
+
688
+ httpClient.get("https://graph.microsoft.com/v1.0/me").subscribe();
689
+ setTimeout(() => {
690
+ const request = httpMock.expectNone(
691
+ "https://graph.microsoft.com/v1.0/me"
692
+ );
693
+ expect(request).toBeUndefined();
694
+ expect(spy1).toHaveBeenCalledWith({
695
+ account: sampleAccountInfo,
696
+ scopes: ["user.read"],
697
+ });
698
+ expect(spy2).not.toHaveBeenCalled();
699
+ expect(spy3).not.toHaveBeenCalled();
700
+ httpMock.verify();
701
+
702
+ _inProgress.next(InteractionStatus.None);
703
+ }, 200);
704
+
705
+ setTimeout(() => {
706
+ expect(spy1.calls.mostRecent().args).toEqual([
707
+ { account: sampleAccountInfo, scopes: ["user.read"] },
708
+ ]);
709
+ expect(spy1).toHaveBeenCalledTimes(2);
710
+ done();
711
+ }, 400);
712
+ });
713
+
714
+ it("keeps original authority, https://login.microsoftonline.com/common", (done) => {
715
+ const originalAuthority = "https://login.microsoftonline.com/common";
716
+
717
+ testInterceptorConfig.authRequest = {
718
+ authority: originalAuthority,
719
+ };
720
+ initializeMsal();
721
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
722
+ sampleAccountInfo,
723
+ ]);
724
+ const spy = spyOn(
725
+ PublicClientApplication.prototype,
726
+ "acquireTokenSilent"
727
+ ).and.callFake(
728
+ (silentRequest: SilentRequest) =>
729
+ new Promise((resolve) => {
730
+ //@ts-ignore
731
+ resolve({
732
+ accessToken: `access-token-for-${silentRequest.authority}`,
733
+ });
734
+ })
735
+ );
736
+
737
+ httpClient.get("https://api.test.com").subscribe();
738
+ setTimeout(() => {
739
+ const request = httpMock.expectOne("https://api.test.com");
740
+ request.flush({ data: "test" });
741
+ expect(request.request.headers.get("Authorization")).toEqual(
742
+ "Bearer access-token-for-https://login.microsoftonline.com/common"
743
+ );
744
+ expect(spy).toHaveBeenCalledWith({
745
+ authority: originalAuthority,
746
+ account: sampleAccountInfo,
747
+ scopes: ["default.scope1"],
748
+ });
749
+ httpMock.verify();
750
+ done();
751
+ }, 200);
752
+ });
753
+
754
+ it("calls dynamic authority with account, authority override", (done) => {
755
+ testInterceptorConfig.authRequest = (msalService, httpReq, authRequest) => {
756
+ return {
757
+ ...authRequest,
758
+ authority: `https://login.microsoftonline.com/${authRequest.account.tenantId}`,
759
+ };
760
+ };
761
+ initializeMsal();
762
+ spyOn(
763
+ PublicClientApplication.prototype,
764
+ "getActiveAccount"
765
+ ).and.returnValue(sampleAccountInfo);
766
+ const spy = spyOn(
767
+ PublicClientApplication.prototype,
768
+ "acquireTokenSilent"
769
+ ).and.callFake(
770
+ (silentRequest: SilentRequest) =>
771
+ new Promise((resolve) => {
772
+ //@ts-ignore
773
+ resolve({
774
+ accessToken: `access-token-for-${silentRequest.authority}`,
775
+ });
776
+ })
777
+ );
778
+
779
+ httpClient.get("https://api.test.com").subscribe();
780
+ setTimeout(() => {
781
+ const request = httpMock.expectOne("https://api.test.com");
782
+ request.flush({ data: "test" });
783
+ expect(request.request.headers.get("Authorization")).toEqual(
784
+ "Bearer access-token-for-https://login.microsoftonline.com/test-tenant"
785
+ );
786
+ expect(spy).toHaveBeenCalledWith({
787
+ account: sampleAccountInfo,
788
+ authority: "https://login.microsoftonline.com/test-tenant",
789
+ scopes: ["default.scope1"],
790
+ });
791
+ httpMock.verify();
792
+ done();
793
+ }, 200);
794
+ });
795
+
796
+ it("attaches authorization header with access token for protected resource with queries", (done) => {
797
+ spyOn(
798
+ PublicClientApplication.prototype,
799
+ "getActiveAccount"
800
+ ).and.returnValue(sampleAccountInfo);
801
+ const spy = spyOn(
802
+ PublicClientApplication.prototype,
803
+ "acquireTokenSilent"
804
+ ).and.returnValue(
805
+ new Promise((resolve) => {
806
+ //@ts-ignore
807
+ resolve({
808
+ accessToken: "access-token",
809
+ });
810
+ })
811
+ );
812
+
813
+ httpClient.get("http://localhost:9876/tenant?abc").subscribe();
814
+ setTimeout(() => {
815
+ const request = httpMock.expectOne("http://localhost:9876/tenant?abc");
816
+ request.flush({ data: "test" });
817
+ expect(request.request.headers.get("Authorization")).toEqual(
818
+ "Bearer access-token"
819
+ );
820
+ expect(spy).toHaveBeenCalledWith({
821
+ account: sampleAccountInfo,
822
+ scopes: ["query.scope"],
823
+ });
824
+ httpMock.verify();
825
+ done();
826
+ }, 200);
827
+ });
828
+
829
+ it("attaches authorization header with access token for protected resource with trailing slash", (done) => {
830
+ const spy = spyOn(
831
+ PublicClientApplication.prototype,
832
+ "acquireTokenSilent"
833
+ ).and.returnValue(
834
+ new Promise((resolve) => {
835
+ //@ts-ignore
836
+ resolve({
837
+ accessToken: "access-token",
838
+ });
839
+ })
840
+ );
841
+
842
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
843
+ sampleAccountInfo,
844
+ ]);
845
+
846
+ httpClient.get("http://applicationA/slash").subscribe();
847
+ setTimeout(() => {
848
+ const request = httpMock.expectOne("http://applicationA/slash");
849
+ request.flush({ data: "test" });
850
+ expect(request.request.headers.get("Authorization")).toEqual(
851
+ "Bearer access-token"
852
+ );
853
+ expect(spy).toHaveBeenCalledWith({
854
+ account: sampleAccountInfo,
855
+ scopes: ["customA.scope"],
856
+ });
857
+ httpMock.verify();
858
+ done();
859
+ }, 200);
860
+ });
861
+
862
+ it("attaches authorization header with access token for endpoint with trailing slash", (done) => {
863
+ const spy = spyOn(
864
+ PublicClientApplication.prototype,
865
+ "acquireTokenSilent"
866
+ ).and.returnValue(
867
+ new Promise((resolve) => {
868
+ //@ts-ignore
869
+ resolve({
870
+ accessToken: "access-token",
871
+ });
872
+ })
873
+ );
874
+
875
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
876
+ sampleAccountInfo,
877
+ ]);
878
+
879
+ httpClient.get("http://applicationB/noSlash/").subscribe();
880
+ setTimeout(() => {
881
+ const request = httpMock.expectOne("http://applicationB/noSlash/");
882
+ request.flush({ data: "test" });
883
+ expect(request.request.headers.get("Authorization")).toEqual(
884
+ "Bearer access-token"
885
+ );
886
+ expect(spy).toHaveBeenCalledWith({
887
+ account: sampleAccountInfo,
888
+ scopes: ["customB.scope"],
889
+ });
890
+ httpMock.verify();
891
+ done();
892
+ }, 200);
893
+ });
894
+
895
+ it("attaches authorization header with access token for relative endpoint", (done) => {
896
+ const spy = spyOn(
897
+ PublicClientApplication.prototype,
898
+ "acquireTokenSilent"
899
+ ).and.returnValue(
900
+ new Promise((resolve) => {
901
+ //@ts-ignore
902
+ resolve({
903
+ accessToken: "access-token",
904
+ });
905
+ })
906
+ );
907
+
908
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
909
+ sampleAccountInfo,
910
+ ]);
911
+
912
+ httpClient.get("http://localhost:9876/relative/me").subscribe();
913
+ setTimeout(() => {
914
+ const request = httpMock.expectOne("http://localhost:9876/relative/me");
915
+ request.flush({ data: "test" });
916
+ expect(request.request.headers.get("Authorization")).toEqual(
917
+ "Bearer access-token"
918
+ );
919
+ expect(spy).toHaveBeenCalledWith({
920
+ account: sampleAccountInfo,
921
+ scopes: ["relative.scope"],
922
+ });
923
+ httpMock.verify();
924
+ done();
925
+ }, 200);
926
+ });
927
+
928
+ it("attaches authorization header with access token for relative endpoint which includes query", (done) => {
929
+ const spy = spyOn(
930
+ PublicClientApplication.prototype,
931
+ "acquireTokenSilent"
932
+ ).and.returnValue(
933
+ new Promise((resolve) => {
934
+ //@ts-ignore
935
+ resolve({
936
+ accessToken: "access-token",
937
+ });
938
+ })
939
+ );
940
+
941
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
942
+ sampleAccountInfo,
943
+ ]);
944
+
945
+ httpClient.get("/tenant?abc").subscribe();
946
+ setTimeout(() => {
947
+ const request = httpMock.expectOne("/tenant?abc");
948
+ request.flush({ data: "test" });
949
+ expect(request.request.headers.get("Authorization")).toEqual(
950
+ "Bearer access-token"
951
+ );
952
+ expect(spy).toHaveBeenCalledWith({
953
+ account: sampleAccountInfo,
954
+ scopes: ["query.scope"],
955
+ });
956
+ httpMock.verify();
957
+ done();
958
+ }, 200);
959
+ });
960
+
961
+ it("attaches authorization header with access token for endpoint with HTTP methods specified", (done) => {
962
+ const spy = spyOn(
963
+ PublicClientApplication.prototype,
964
+ "acquireTokenSilent"
965
+ ).and.returnValue(
966
+ new Promise((resolve) => {
967
+ //@ts-ignore
968
+ resolve({
969
+ accessToken: "access-token",
970
+ });
971
+ })
972
+ );
973
+
974
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
975
+ sampleAccountInfo,
976
+ ]);
977
+
978
+ httpClient.post("http://applicationC.com", {}).subscribe();
979
+ setTimeout(() => {
980
+ const request = httpMock.expectOne("http://applicationC.com");
981
+ request.flush({ data: "test" });
982
+ expect(request.request.headers.get("Authorization")).toEqual(
983
+ "Bearer access-token"
984
+ );
985
+ expect(spy).toHaveBeenCalledWith({
986
+ account: sampleAccountInfo,
987
+ scopes: ["write.scope"],
988
+ });
989
+ httpMock.verify();
990
+ done();
991
+ }, 200);
992
+ });
993
+
994
+ it("does not attach authorization header when request HTTP method is not in protectedResourceMap", (done) => {
995
+ httpClient
996
+ .get("http://applicationC.com")
997
+ .subscribe((response) => expect(response).toBeTruthy());
998
+
999
+ const request = httpMock.expectOne("http://applicationC.com");
1000
+ request.flush({ data: "test" });
1001
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
1002
+ httpMock.verify();
1003
+ done();
1004
+ });
1005
+
1006
+ it("attaches authorization header with access token for endpoint with scopes in string array and with HTTP methods specified", (done) => {
1007
+ const spy = spyOn(
1008
+ PublicClientApplication.prototype,
1009
+ "acquireTokenSilent"
1010
+ ).and.returnValue(
1011
+ new Promise((resolve) => {
1012
+ //@ts-ignore
1013
+ resolve({
1014
+ accessToken: "access-token",
1015
+ });
1016
+ })
1017
+ );
1018
+
1019
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
1020
+ sampleAccountInfo,
1021
+ ]);
1022
+
1023
+ httpClient.get("http://applicationD.com").subscribe();
1024
+ setTimeout(() => {
1025
+ const request = httpMock.expectOne("http://applicationD.com");
1026
+ request.flush({ data: "test" });
1027
+ expect(request.request.headers.get("Authorization")).toEqual(
1028
+ "Bearer access-token"
1029
+ );
1030
+ expect(spy).toHaveBeenCalledWith({
1031
+ account: sampleAccountInfo,
1032
+ scopes: ["all.scope", "read.scope"],
1033
+ });
1034
+ httpMock.verify();
1035
+ done();
1036
+ }, 200);
1037
+ });
1038
+
1039
+ it("does not attach authorization header if request HTTP method with scope is not in protectedResourceMap", (done) => {
1040
+ httpClient
1041
+ .get("http://applicationC.com")
1042
+ .subscribe((response) => expect(response).toBeTruthy());
1043
+
1044
+ const request = httpMock.expectOne("http://applicationC.com");
1045
+ request.flush({ data: "test" });
1046
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
1047
+ httpMock.verify();
1048
+ done();
1049
+ });
1050
+
1051
+ it("attaches authorization header with access token for endpoint with HTTP methods specified, regardless of casing of HTTP method", (done) => {
1052
+ const spy = spyOn(
1053
+ PublicClientApplication.prototype,
1054
+ "acquireTokenSilent"
1055
+ ).and.returnValue(
1056
+ new Promise((resolve) => {
1057
+ //@ts-ignore
1058
+ resolve({
1059
+ accessToken: "access-token",
1060
+ });
1061
+ })
1062
+ );
1063
+
1064
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
1065
+ sampleAccountInfo,
1066
+ ]);
1067
+
1068
+ httpClient.post("http://applicationD.com", {}).subscribe();
1069
+ setTimeout(() => {
1070
+ const request = httpMock.expectOne("http://applicationD.com");
1071
+ request.flush({ data: "test" });
1072
+ expect(request.request.headers.get("Authorization")).toEqual(
1073
+ "Bearer access-token"
1074
+ );
1075
+ expect(spy).toHaveBeenCalledWith({
1076
+ account: sampleAccountInfo,
1077
+ scopes: ["all.scope", "info.scope"],
1078
+ });
1079
+ httpMock.verify();
1080
+ done();
1081
+ }, 200);
1082
+ });
1083
+
1084
+ it("does not attach authorization header if relative endpoints match but absolute url does not match", (done) => {
1085
+ httpClient
1086
+ .get("http://applicationZ.com/noSlash")
1087
+ .subscribe((response) => expect(response).toBeTruthy());
1088
+
1089
+ const request = httpMock.expectOne("http://applicationZ.com/noSlash");
1090
+ request.flush({ data: "test" });
1091
+ expect(request.request.headers.get("Authorization")).toBeUndefined;
1092
+ httpMock.verify();
1093
+ done();
1094
+ });
1095
+
1096
+ it("attaches authorization header with access token for correct endpoint even though an earlier endpoint in the protectedResourceMap has a matching relative endpoint", (done) => {
1097
+ const spy = spyOn(
1098
+ PublicClientApplication.prototype,
1099
+ "acquireTokenSilent"
1100
+ ).and.returnValue(
1101
+ new Promise((resolve) => {
1102
+ //@ts-ignore
1103
+ resolve({
1104
+ accessToken: "access-token",
1105
+ });
1106
+ })
1107
+ );
1108
+
1109
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
1110
+ sampleAccountInfo,
1111
+ ]);
1112
+
1113
+ httpClient.post("http://applicationF.com/profile/", {}).subscribe();
1114
+ setTimeout(() => {
1115
+ const request = httpMock.expectOne("http://applicationF.com/profile/");
1116
+ request.flush({ data: "test" });
1117
+ expect(request.request.headers.get("Authorization")).toEqual(
1118
+ "Bearer access-token"
1119
+ );
1120
+ expect(spy).toHaveBeenCalledWith({
1121
+ account: sampleAccountInfo,
1122
+ scopes: ["customF.scope"],
1123
+ });
1124
+ httpMock.verify();
1125
+ done();
1126
+ }, 200);
1127
+ });
1128
+
1129
+ it("attaches authorization header with access token when endpoint match is in HostNameAndPort instead of query string", (done) => {
1130
+ const spy = spyOn(
1131
+ PublicClientApplication.prototype,
1132
+ "acquireTokenSilent"
1133
+ ).and.returnValue(
1134
+ new Promise((resolve) => {
1135
+ //@ts-ignore
1136
+ resolve({
1137
+ accessToken: "access-token",
1138
+ });
1139
+ })
1140
+ );
1141
+
1142
+ spyOn(PublicClientApplication.prototype, "getAllAccounts").and.returnValue([
1143
+ sampleAccountInfo,
1144
+ ]);
1145
+
1146
+ httpClient
1147
+ .get(
1148
+ "https://MY_API_SITE_1/api/sites?$filter=siteUrl eq 'https://MY_API_SITE_2'"
1149
+ )
1150
+ .subscribe();
1151
+
1152
+ setTimeout(() => {
1153
+ const request = httpMock.expectOne(
1154
+ "https://MY_API_SITE_1/api/sites?$filter=siteUrl eq 'https://MY_API_SITE_2'"
1155
+ );
1156
+ request.flush({ data: "test" });
1157
+ expect(request.request.headers.get("Authorization")).toEqual(
1158
+ "Bearer access-token"
1159
+ );
1160
+ expect(spy).toHaveBeenCalledWith({
1161
+ account: sampleAccountInfo,
1162
+ scopes: ["api://MY_API_SITE_1/as_user"],
1163
+ });
1164
+ httpMock.verify();
1165
+ done();
1166
+ }, 200);
1167
+ });
1168
+ });