@azure/msal-angular 3.0.0-beta.1 → 3.0.0

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