@equinor/fusion-framework-module-msal 11.0.0 → 11.0.1

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 (60) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/tsconfig.tsbuildinfo +1 -1
  3. package/dist/types/version.d.ts +1 -1
  4. package/package.json +8 -5
  5. package/CHANGELOG.md +0 -1212
  6. package/docs/api-reference.md +0 -85
  7. package/docs/auth-code-flow.md +0 -86
  8. package/docs/migration-v2-to-v4.md +0 -115
  9. package/docs/testing.md +0 -191
  10. package/docs/troubleshooting.md +0 -17
  11. package/docs/version-management.md +0 -67
  12. package/src/MsalClient.interface.ts +0 -139
  13. package/src/MsalClient.ts +0 -326
  14. package/src/MsalConfigurator.ts +0 -486
  15. package/src/MsalProvider.interface.ts +0 -179
  16. package/src/MsalProvider.ts +0 -776
  17. package/src/MsalProxyProvider.interface.ts +0 -72
  18. package/src/__tests__/MsalConfigurator.test.ts +0 -222
  19. package/src/__tests__/MsalProvider.test.ts +0 -74
  20. package/src/__tests__/create-proxy-provider.test.ts +0 -77
  21. package/src/__tests__/mock/create-mock-user-from-token.test.ts +0 -46
  22. package/src/__tests__/mock/msal-mock.test.ts +0 -613
  23. package/src/__tests__/versioning/resolve-version.test.ts +0 -161
  24. package/src/create-client-log-callback.ts +0 -102
  25. package/src/create-proxy-provider.ts +0 -97
  26. package/src/index.ts +0 -48
  27. package/src/mock/MsalMockClient.ts +0 -618
  28. package/src/mock/MsalMockConfigurator.ts +0 -305
  29. package/src/mock/create-mock-token.ts +0 -92
  30. package/src/mock/create-mock-user-from-token.ts +0 -46
  31. package/src/mock/create-msal-mock-client.ts +0 -25
  32. package/src/mock/decode-jwt-segment.ts +0 -22
  33. package/src/mock/index.ts +0 -30
  34. package/src/mock/module.ts +0 -54
  35. package/src/module.ts +0 -142
  36. package/src/msal-config-schema.ts +0 -81
  37. package/src/static.ts +0 -38
  38. package/src/telemetry-config-schema.ts +0 -25
  39. package/src/types.ts +0 -16
  40. package/src/util/compare-origin.ts +0 -18
  41. package/src/util/normalize-uri.ts +0 -24
  42. package/src/util/redirect.ts +0 -19
  43. package/src/v2/IAuthClient.interface.ts +0 -114
  44. package/src/v2/Logger.ts +0 -204
  45. package/src/v2/MsalProvider.interface.ts +0 -102
  46. package/src/v2/create-proxy-client.ts +0 -195
  47. package/src/v2/create-proxy-provider.ts +0 -177
  48. package/src/v2/map-account-info.ts +0 -23
  49. package/src/v2/map-authentication-result.ts +0 -28
  50. package/src/v2/types.ts +0 -674
  51. package/src/v4/create-proxy-provider.ts +0 -75
  52. package/src/v4/index.ts +0 -13
  53. package/src/v4/types.ts +0 -727
  54. package/src/version.ts +0 -2
  55. package/src/versioning/VersionError.ts +0 -64
  56. package/src/versioning/index.ts +0 -29
  57. package/src/versioning/resolve-version.ts +0 -154
  58. package/src/versioning/types.ts +0 -60
  59. package/tsconfig.json +0 -18
  60. package/vitest.config.ts +0 -11
@@ -1,776 +0,0 @@
1
- import type {
2
- ITelemetryProvider,
3
- TelemetryItem,
4
- TelemetryException,
5
- IMeasurement,
6
- } from '@equinor/fusion-framework-module-telemetry';
7
-
8
- import { TelemetryLevel } from '@equinor/fusion-framework-module-telemetry';
9
-
10
- import { BaseModuleProvider } from '@equinor/fusion-framework-module/provider';
11
-
12
- import type { MsalConfig } from './MsalConfigurator';
13
- import type { AcquireTokenOptionsLegacy, IMsalProvider } from './MsalProvider.interface';
14
- import { createProxyProvider } from './create-proxy-provider';
15
- import type {
16
- AcquireTokenOptions,
17
- AcquireTokenResult,
18
- IMsalClient,
19
- LoginOptions,
20
- LoginResult,
21
- LogoutOptions,
22
- } from './MsalClient.interface';
23
-
24
- import type { AccountInfo, AuthenticationResult } from './types';
25
- import { resolveVersion } from './versioning/resolve-version';
26
- import { version } from './version';
27
- import type { MsalModuleVersion } from './static';
28
-
29
- export type { IMsalProvider };
30
-
31
- /**
32
- * MSAL v4 compatible authentication provider for Fusion Framework.
33
- *
34
- * This provider wraps the MSAL v4 PublicClientApplication and provides
35
- * a simplified interface for authentication operations while maintaining
36
- * compatibility with the Fusion Framework module system.
37
- *
38
- * @example
39
- * ```typescript
40
- * const provider = new MsalProvider({
41
- * clientId: 'your-client-id',
42
- * tenantId: 'your-tenant-id',
43
- * redirectUri: 'https://your-app.com/callback'
44
- * });
45
- *
46
- * // Login user
47
- * await provider.login({ request: { scopes: ['User.Read'] } });
48
- *
49
- * // Acquire token
50
- * const token = await provider.acquireAccessToken({
51
- * request: { scopes: ['https://graph.microsoft.com/.default'] }
52
- * });
53
- * ```
54
- */
55
- export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsalProvider {
56
- #client: IMsalClient;
57
- #telemetry: {
58
- provider?: ITelemetryProvider;
59
- metadata: Record<string, unknown>;
60
- scope: string[];
61
- };
62
- #requiresAuth?: boolean;
63
- #authCode?: string;
64
- #loginHint?: string;
65
-
66
- /**
67
- * Default OAuth scopes used when the caller provides no scopes.
68
- *
69
- * Resolves to the app's Entra ID configured permissions via the `/.default` scope.
70
- *
71
- * @returns The default OAuth scopes derived from the configured client ID.
72
- */
73
- get defaultScopes(): string[] {
74
- const clientId = this.#client.clientId;
75
- return clientId ? [`${clientId}/.default`] : [];
76
- }
77
-
78
- /**
79
- * The MSAL module version enum value indicating the API compatibility level.
80
- *
81
- * This getter resolves the current version string to its corresponding enum value,
82
- * determining which MSAL version's API surface this provider implements. This is used
83
- * for version-specific behavior and proxy provider creation.
84
- *
85
- * @returns The MSAL module version enum (V2, V4, etc.)
86
- */
87
- get msalVersion(): MsalModuleVersion {
88
- return resolveVersion(version).enumVersion;
89
- }
90
-
91
- /**
92
- * The MSAL client instance.
93
- *
94
- * Provides access to the underlying MSAL PublicClientApplication for advanced use cases.
95
- * Prefer using provider methods for standard authentication operations.
96
- *
97
- * @returns The underlying MSAL client instance.
98
- */
99
- get client(): IMsalClient {
100
- return this.#client;
101
- }
102
-
103
- /**
104
- * The currently authenticated account.
105
- *
106
- * Returns the active account if a user is authenticated, or null if no user is logged in.
107
- * This is a shorthand for `client.getActiveAccount()`.
108
- *
109
- * @returns The currently authenticated account, or `null` if no user is logged in.
110
- */
111
- get account(): AccountInfo | null {
112
- return this.#client.getActiveAccount();
113
- }
114
-
115
- /**
116
- * @deprecated Use account instead
117
- * @returns The currently authenticated account or undefined if no user is logged in.
118
- */
119
- get defaultAccount(): AccountInfo | undefined {
120
- this._trackException('MsalPrvider.defaultAccount.deprecated', TelemetryLevel.Warning, {
121
- exception: new Error(
122
- 'defaultAccount is deprecated, use account instead. This will be removed in the next major version.',
123
- ),
124
- properties: {
125
- message:
126
- 'defaultAccount is deprecated, use account instead. This will be removed in the next major version.',
127
- reason:
128
- 'This is most likely due to accessing the framework directly from application code, instead of using the application hooks.',
129
- },
130
- });
131
- return this.account ?? undefined;
132
- }
133
-
134
- /**
135
- * Creates a new MSAL provider instance.
136
- *
137
- * @param config - Complete MSAL configuration including client, telemetry, and auth requirements
138
- * @throws {Error} If client is not provided in configuration
139
- */
140
- constructor(config: MsalConfig) {
141
- super({
142
- version,
143
- config,
144
- });
145
- this.#requiresAuth = config.requiresAuth;
146
- this.#telemetry = config.telemetry;
147
- this.#loginHint = config.loginHint;
148
-
149
- // Extract auth code from config if present
150
- // This will be used during initialize to exchange for tokens
151
- this.#authCode = config.authCode?.trim() || undefined;
152
-
153
- // Validate required client configuration
154
- if (!config.client) {
155
- const error = new Error(
156
- 'Client is required, please provide a valid client in the configuration',
157
- );
158
- this._trackException('constructor.client-required', TelemetryLevel.Error, {
159
- exception: error,
160
- });
161
- throw error;
162
- }
163
- this.#client = config.client;
164
- }
165
-
166
- /**
167
- * Initializes the MSAL provider and sets up authentication state.
168
- *
169
- * This method must be called before using any authentication operations. It performs:
170
- * - Client initialization
171
- * - Auth code exchange (if backend-issued code provided)
172
- * - Redirect result handling (if returning from auth flow)
173
- * - Automatic login attempt if requiresAuth is enabled and no valid session exists
174
- *
175
- * @returns Promise that resolves when initialization is complete
176
- *
177
- * @remarks
178
- * Auth code exchange happens before the requiresAuth check, allowing automatic sign-in
179
- * without user interaction when a valid backend-issued code is provided. If exchange fails,
180
- * the provider falls back to standard MSAL authentication flows.
181
- *
182
- * The provider will attempt automatic login with empty scopes if requiresAuth is true.
183
- * Apps should call acquireToken with actual scopes after initialization completes.
184
- *
185
- * @throws {Error} If auth code exchange requires a client ID but none is configured.
186
- */
187
- async initialize(): Promise<void> {
188
- // Guard: skip authentication when running inside MSAL's hidden iframe.
189
- // MSAL uses a hidden iframe for silent token renewal (acquireTokenSilent).
190
- // The iframe loads the app URL, which would re-initialize MSAL and attempt
191
- // loginRedirect(), causing the "block_iframe_reload" error. Detect this
192
- // by checking if we're in an iframe and the URL contains MSAL's hash params.
193
- if (typeof window !== 'undefined' && window !== window.parent) {
194
- // Running inside an iframe — let the parent handle authentication.
195
- // Still initialize the client so handleRedirectPromise can process
196
- // the auth response and post it back to the parent frame.
197
- await this.#client.initialize();
198
- await this.#client.handleRedirectPromise();
199
- return;
200
- }
201
-
202
- const measurement = this._trackMeasurement('initialize', TelemetryLevel.Debug);
203
- // Initialize the underlying MSAL client first
204
- await this.#client.initialize();
205
-
206
- // Priority 0: Exchange auth code if provided by backend
207
- // This must happen before the requiresAuth check so tokens are cached
208
- if (this.#authCode) {
209
- try {
210
- this._trackEvent('initialize.exchanging-auth-code', TelemetryLevel.Information);
211
-
212
- // Use MSAL's acquireTokenByCode to exchange backend auth code for tokens
213
- // This follows Microsoft's standard SPA Auth Code Flow pattern
214
- const clientId = this.#client.clientId;
215
- // A client ID is required to build the default scope used for the auth code exchange
216
- if (!clientId) {
217
- throw new Error('Client ID is required for auth code exchange');
218
- }
219
-
220
- // Exchange the auth code for tokens using the client ID's default scope.
221
- // The `/.default` scope represents all permissions configured for this app in Entra ID,
222
- // ensuring the exchanged tokens have the correct app-level permissions without requiring
223
- // the caller to specify scopes. This follows MSAL's recommended SPA auth code pattern.
224
- // This method is inherited from PublicClientApplication (MSAL Browser v4+)
225
- const result = await this.#client.acquireTokenByCode({
226
- code: this.#authCode,
227
- scopes: [`${clientId}/.default`],
228
- });
229
-
230
- // Successfully exchanged auth code - set active account
231
- if (result.account) {
232
- this.#client.setActiveAccount(result.account);
233
- this._trackEvent('initialize.auth-code-exchanged-account', TelemetryLevel.Information, {
234
- properties: {
235
- username: result.account.username,
236
- },
237
- });
238
- }
239
- } catch (error) {
240
- // Auth code exchange failed - log and fall back to standard flows
241
- this._trackException('initialize.auth-code-exchange-failed', TelemetryLevel.Warning, {
242
- exception: error instanceof Error ? error : new Error(String(error)),
243
- properties: {
244
- message: error instanceof Error ? error.message : String(error),
245
- reason: 'Auth code exchange failed, falling back to standard authentication flows',
246
- },
247
- });
248
- // Continue to requiresAuth check - will trigger standard login if needed
249
- } finally {
250
- // Clear auth code to avoid repeated attempts
251
- this.#authCode = undefined;
252
- }
253
- }
254
-
255
- // Only attempt authentication if this provider requires it
256
- if (this.#requiresAuth) {
257
- // Priority 1: Check if returning from redirect-based authentication
258
- // This handles cases where user just completed a login/acquireToken via redirect
259
- const handleRedirectResult = await this.handleRedirect();
260
- // A returned account means the user just completed a redirect-based login
261
- if (handleRedirectResult?.account) {
262
- // Successfully authenticated via redirect - set as active account
263
- // This means the user was redirected to Microsoft and came back authenticated
264
- this.#client.setActiveAccount(handleRedirectResult.account);
265
- this._trackEvent('initialize.active-account-set-by-callback', TelemetryLevel.Information, {
266
- properties: {
267
- username: handleRedirectResult.account.username,
268
- },
269
- });
270
- } else if (!this.#client.hasValidClaims) {
271
- // Priority 2: No valid session found - attempt automatic login
272
- // This handles first-time app load when no authentication state exists
273
- // Note: Using default scopes here as we don't know what scopes the app needs yet
274
- // App should call acquireToken with actual scopes after initialization
275
- const loginResult = await this.login({ request: { scopes: this.defaultScopes } });
276
- // Only set the active account when the automatic login actually returned one
277
- if (loginResult?.account) {
278
- // Automatic login successful - set as active account
279
- this.#client.setActiveAccount(loginResult.account);
280
- this._trackEvent('initialize.active-account-set-by-login', TelemetryLevel.Information, {
281
- properties: {
282
- username: loginResult.account.username,
283
- },
284
- });
285
- }
286
- }
287
- // Priority 3: If hasValidClaims is true, user is already authenticated - no action needed
288
- }
289
- measurement.measure();
290
- }
291
-
292
- /**
293
- * Acquire an access token string for the specified scopes
294
- *
295
- * @param options - Token acquisition options (same as acquireToken)
296
- * @returns Promise resolving to access token string, or undefined if acquisition fails
297
- *
298
- * @example
299
- * ```typescript
300
- * const token = await msalProvider.acquireAccessToken({
301
- * request: { scopes: ['api.read'] }
302
- * });
303
- * if (token) {
304
- * // Use token for API calls
305
- * fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
306
- * }
307
- * ```
308
- */
309
- async acquireAccessToken(
310
- options?: AcquireTokenOptions | AcquireTokenOptionsLegacy,
311
- ): Promise<string | undefined> {
312
- const { accessToken } = (await this.acquireToken(options)) ?? {};
313
- return accessToken;
314
- }
315
-
316
- /**
317
- * Acquire full authentication result for the specified scopes
318
- *
319
- * @param options - Token acquisition options including scopes, behavior, and silent mode
320
- * @param options.request.scopes - Array of OAuth scopes to request access for
321
- * @param options.scopes - Legacy scopes format (deprecated, use request.scopes)
322
- * @param options.behavior - Authentication behavior ('redirect' or 'popup')
323
- * @param options.silent - Whether to attempt silent token acquisition first
324
- * @returns Promise resolving to authentication result containing access token and account info
325
- *
326
- * @remark Empty scopes are currently tracked as telemetry exceptions but execution continues for monitoring purposes.
327
- * This behavior will be changed to throw exceptions once sufficient metrics are collected.
328
- *
329
- * @throws {Error} Re-throws any error encountered during token acquisition after tracking it via telemetry.
330
- *
331
- * @example
332
- * ```typescript
333
- * // Modern API format
334
- * const result = await msalProvider.acquireToken({
335
- * request: { scopes: ['user.read', 'api.write'] },
336
- * behavior: 'redirect',
337
- * silent: true
338
- * });
339
- *
340
- * // Legacy format (deprecated)
341
- * const result = await msalProvider.acquireToken({
342
- * scopes: ['user.read'],
343
- * silent: false
344
- * });
345
- * ```
346
- */
347
- async acquireToken(
348
- options?: AcquireTokenOptions | AcquireTokenOptionsLegacy,
349
- ): Promise<AcquireTokenResult> {
350
- // Guard: when running inside MSAL's hidden iframe, only attempt silent
351
- // acquisition. Interactive flows (redirect/popup) must never be triggered
352
- // from an iframe — MSAL will throw "block_iframe_reload".
353
- const inIframe = typeof window !== 'undefined' && window !== window.parent;
354
-
355
- // Determine behavior and silent options, with defaults (redirect and true respectively)
356
- const behavior = inIframe ? 'redirect' : (options?.behavior ?? 'redirect');
357
-
358
- // When in an iframe, force silent-only to prevent interactive fallback
359
- const silent = inIframe ? true : (options?.silent ?? true);
360
-
361
- const defaultScopes = this.defaultScopes;
362
-
363
- const inputRequest = options?.request;
364
-
365
- // Determine the account to use for token acquisition, prioritizing request-specific account, then active account
366
- const account = inputRequest?.account ?? this.account ?? undefined;
367
-
368
- // Extract caller-provided scopes from either new format (request.scopes) or legacy format (scopes)
369
- const candidateScopes =
370
- inputRequest?.scopes ?? (options as AcquireTokenOptionsLegacy)?.scopes ?? [];
371
-
372
- const scopes =
373
- candidateScopes.length > 0 ? candidateScopes : defaultScopes.length > 0 ? defaultScopes : [];
374
-
375
- // Prepare telemetry properties for this token acquisition attempt
376
- const telemetryProperties = { behavior, silent, scopes };
377
-
378
- // Track usage of deprecated legacy scopes format for migration monitoring
379
- if ((options as AcquireTokenOptionsLegacy)?.scopes) {
380
- this._trackEvent('acquireToken.legacy-scopes-provided', TelemetryLevel.Warning, {
381
- properties: telemetryProperties,
382
- });
383
- }
384
-
385
- // Handle empty scopes - currently monitoring for telemetry, will throw in future
386
- if (candidateScopes.length === 0) {
387
- // Fall back to the client-id-derived default scope when one is available
388
- if (defaultScopes.length > 0) {
389
- this._trackEvent('acquireToken.missing-scope.defaulted', TelemetryLevel.Warning, {
390
- properties: { ...telemetryProperties, defaultScopes },
391
- });
392
- } else {
393
- const exception = new Error(
394
- 'Empty scopes provided and clientId is missing for default scope',
395
- );
396
- this._trackException('acquireToken.missing-scope', TelemetryLevel.Warning, {
397
- exception,
398
- properties: telemetryProperties,
399
- });
400
- // TODO(#5113): throw exception when sufficient metrics are collected
401
- // This allows us to monitor how often empty scopes are provided before enforcing validation
402
- }
403
- }
404
-
405
- try {
406
- const measurement = this._trackMeasurement('acquireToken', TelemetryLevel.Information, {
407
- properties: telemetryProperties,
408
- });
409
- // Merge account, original request options, and resolved scopes.
410
- // Account ensures context awareness, request preserves custom options, scopes uses resolved value.
411
- const result = await this.#client.acquireToken({
412
- behavior,
413
- silent,
414
- request: { ...inputRequest, account, scopes },
415
- });
416
- measurement?.measure();
417
- return result;
418
- } catch (error) {
419
- // Inside MSAL's hidden iframe, silent acquisition may fail and the client
420
- // would fall back to acquireTokenRedirect which throws "block_iframe_reload".
421
- // Suppress this — the parent frame handles interactive auth.
422
- if (inIframe) {
423
- this._trackEvent('acquireToken.suppressed-in-iframe', TelemetryLevel.Debug, {
424
- properties: telemetryProperties,
425
- });
426
- return undefined;
427
- }
428
- this._trackException('acquireToken-failed', TelemetryLevel.Error, {
429
- exception: error as Error,
430
- properties: telemetryProperties,
431
- });
432
- throw error;
433
- }
434
- }
435
-
436
- /**
437
- * Authenticates a user using Microsoft Authentication Library.
438
- *
439
- * This method implements a sophisticated login flow that **attempts silent authentication
440
- * first by default** (`silent: true`) and falls back to interactive authentication based on the specified
441
- * behavior. The flow prioritizes user experience by minimizing unnecessary popups or redirects.
442
- *
443
- * **Authentication Flow:**
444
- * 1. **Silent Login Attempt** (default behavior):
445
- * - Attempts SSO silent authentication using existing session
446
- * - Requires `loginHint` to be provided (uses active account's username if available)
447
- * - Falls back to interactive login if silent attempt fails
448
- *
449
- * 2. **Interactive Login Fallback** (based on `behavior`):
450
- * - `popup`: Opens authentication popup window (default)
451
- * - `redirect`: Redirects current window to authentication page
452
- *
453
- * **Default Behavior:**
454
- * - Attempts silent authentication first (`silent: true`)
455
- * - Falls back to popup authentication (`behavior: 'popup'`)
456
- * - Uses active account's username as login hint if not provided
457
- * - Warns if no scopes are specified (uses empty array)
458
- *
459
- * @param options - Login configuration options
460
- * @param options.request - Authentication request parameters (scopes, loginHint, etc.)
461
- * @param options.behavior - Authentication method: 'popup' (default) or 'redirect'
462
- * @param options.silent - Whether to attempt silent authentication first (**default: true**)
463
- *
464
- * @returns Promise resolving to authentication result or undefined
465
- *
466
- * @throws {Error} When authentication fails or invalid parameters provided
467
- *
468
- * @example
469
- * ```typescript
470
- * // Basic login (silent first, popup fallback - DEFAULT BEHAVIOR)
471
- * const result = await provider.login({
472
- * request: { scopes: ['User.Read'] }
473
- * });
474
- *
475
- * // Skip silent, go straight to redirect
476
- * await provider.login({
477
- * request: { scopes: ['User.Read'] },
478
- * silent: false,
479
- * behavior: 'redirect'
480
- * });
481
- * ```
482
- */
483
- async login(options: LoginOptions): Promise<LoginResult> {
484
- // Guard: never attempt interactive login inside MSAL's hidden iframe.
485
- if (typeof window !== 'undefined' && window !== window.parent) {
486
- return undefined;
487
- }
488
-
489
- const { behavior = 'redirect', silent = true, request } = options;
490
-
491
- request.loginHint ??=
492
- this.#loginHint ?? this.account?.username ?? this.account?.loginHint ?? undefined;
493
-
494
- const defaultScopes = this.defaultScopes;
495
-
496
- // Fallback to app default scope when possible; empty scopes tracked for monitoring
497
- if (!request.scopes || request.scopes.length === 0) {
498
- request.scopes = defaultScopes.length > 0 ? defaultScopes : [];
499
- }
500
-
501
- // Determine if silent login is possible based on available account/hint information
502
- // Silent login requires either an account object or a loginHint to work
503
- const canLoginSilently = silent && (request.account || request.loginHint);
504
-
505
- const telemetryProperties = { behavior, silent, canLoginSilently, scopes: request.scopes };
506
-
507
- // Default to active account if no account/hint provided in request
508
- // This allows silent login to work automatically with existing authentication state
509
- request.account ??= this.account ?? undefined;
510
-
511
- // If scopes are still empty here, we couldn't derive a default scope (e.g. missing clientId).
512
- // Track for monitoring; behavior will be enforced once we have sufficient metrics.
513
- if (request.scopes.length === 0) {
514
- this._trackEvent('login.missing-scope', TelemetryLevel.Warning, {
515
- properties: telemetryProperties,
516
- });
517
- }
518
-
519
- this._trackEvent('login', TelemetryLevel.Information, {
520
- properties: telemetryProperties,
521
- });
522
-
523
- // Attempt silent authentication first if conditions are met
524
- // This provides better UX by avoiding unnecessary popups/redirects
525
- if (canLoginSilently) {
526
- try {
527
- return await this.#client.ssoSilent(request);
528
- } catch (error) {
529
- // Silent login failed - track for monitoring but continue to interactive flow
530
- this._trackException('login.silent-failed', TelemetryLevel.Warning, {
531
- exception: error as Error,
532
- properties: telemetryProperties,
533
- });
534
- // Fall through to interactive authentication
535
- }
536
- }
537
-
538
- // Perform interactive authentication based on specified behavior
539
- switch (behavior) {
540
- case 'popup':
541
- return await this.#client.loginPopup(request);
542
- case 'redirect':
543
- await this.#client.loginRedirect(request);
544
- break;
545
- default:
546
- throw new Error(
547
- `Invalid behavior provided: ${behavior}, please provide a valid behavior, see options.behavior for more information.`,
548
- );
549
- }
550
- }
551
-
552
- /**
553
- * Logs out the current user and clears authentication state.
554
- *
555
- * This method initiates a logout flow using redirect, which navigates to Microsoft's
556
- * logout endpoint to clear session cookies and tokens. The method returns true on
557
- * successful logout initiation, or false if logout fails.
558
- *
559
- * @param options - Optional logout configuration
560
- * @param options.account - Account to log out (defaults to active account)
561
- * @param options.redirectUri - URI to redirect to after logout completes
562
- * @returns Promise resolving to true on success, false on failure
563
- *
564
- * @remarks
565
- * - Logout always uses redirect flow (more reliable than popup)
566
- * - Returns false on error instead of throwing to prevent breaking app flow
567
- * - Browser will navigate away during logout process
568
- *
569
- * @example
570
- * ```typescript
571
- * // Basic logout
572
- * const success = await provider.logout();
573
- *
574
- * // Logout with custom redirect
575
- * await provider.logout({ redirectUri: 'https://app.com/logout' });
576
- * ```
577
- */
578
- async logout(options?: LogoutOptions): Promise<boolean> {
579
- this._trackEvent('logout', TelemetryLevel.Information, {
580
- properties: {
581
- redirectUri: options?.redirectUri,
582
- },
583
- });
584
-
585
- try {
586
- // Logout the specific account (or current account if none specified)
587
- await this.#client.logout({ account: this.account ?? undefined, ...options });
588
- return true; // Success
589
- } catch (error) {
590
- // Logout failed - track error but don't throw to avoid breaking app flow
591
- this._trackException('logout.failed', TelemetryLevel.Error, {
592
- exception: error as Error,
593
- });
594
- }
595
- return false; // Failed
596
- }
597
-
598
- /**
599
- * Processes any pending authentication redirect after browser navigation.
600
- *
601
- * This method must be called on app initialization to handle tokens and account information
602
- * returned by Microsoft's identity provider after redirect-based authentication flows.
603
- *
604
- * @returns Promise resolving to authentication result or null if no redirect pending
605
- *
606
- * @remarks
607
- * - Should be called once on app startup before other authentication operations
608
- * - Only returns a result if user just completed redirect-based login/acquireToken
609
- * - Safe to call even when no redirect is pending (returns null)
610
- *
611
- * @example
612
- * ```typescript
613
- * // Call on app startup
614
- * const result = await provider.handleRedirect();
615
- * if (result?.account) {
616
- * console.log(`Authenticated as: ${result.account.username}`);
617
- * provider.client.setActiveAccount(result.account);
618
- * }
619
- * ```
620
- */
621
- async handleRedirect(): Promise<AuthenticationResult | null> {
622
- // Process any pending redirect from authentication flow
623
- const result = await this.client.handleRedirectPromise();
624
- // Only track/log when a redirect result is actually returned
625
- if (result) {
626
- // Track successful redirect completion for monitoring
627
- this._trackEvent('handleRedirect.success', TelemetryLevel.Information, {
628
- properties: {
629
- username: result.account?.username,
630
- },
631
- });
632
- }
633
- return result;
634
- }
635
-
636
- /**
637
- * Creates a proxy provider for version compatibility.
638
- *
639
- * This method creates a version-specific proxy wrapper around this provider to maintain
640
- * backward compatibility with different MSAL versions while using the latest v4 implementation.
641
- *
642
- * @param version - Target version string (e.g., '2.0.0', '4.0.0', 'v2', 'v4')
643
- * @returns Proxy provider covariant with the specified version
644
- *
645
- * @remarks
646
- * - Proxies adapt the v4 API to match older version signatures
647
- * - Useful for gradual migration scenarios
648
- * - Version compatibility is tracked via telemetry
649
- * - Throws error if unsupported version is requested
650
- *
651
- * @template T - The provider interface type expected by the target version.
652
- * @throws {Error} If the requested version cannot be resolved or the proxy fails to create.
653
- *
654
- * @example
655
- * ```typescript
656
- * // Create v2-compatible proxy
657
- * const v2Proxy = provider.createProxyProvider('2.0.0');
658
- * await v2Proxy.login(); // Uses v2-compatible signature
659
- * ```
660
- */
661
- createProxyProvider<T = IMsalProvider>(version: string): T {
662
- // Track proxy provider creation for compatibility monitoring
663
- this._trackEvent('createProxyProvider', TelemetryLevel.Debug, {
664
- properties: {
665
- version: version,
666
- },
667
- });
668
-
669
- // Parse and validate the requested version string
670
- const resolvedVersion = resolveVersion(version);
671
-
672
- this._trackEvent('createProxyProvider.version-resolved', TelemetryLevel.Information, {
673
- properties: resolvedVersion,
674
- });
675
-
676
- // Warn if using outdated version - helps track migration progress
677
- if (!resolvedVersion.satisfiesLatest) {
678
- this._trackEvent('createProxyProvider.outdated-version', TelemetryLevel.Warning, {
679
- properties: resolvedVersion,
680
- });
681
- }
682
-
683
- try {
684
- // create the proxy provider
685
- return createProxyProvider(this, version);
686
- } catch (error) {
687
- this._trackException('createProxyProvider.failed', TelemetryLevel.Error, {
688
- exception: error as Error,
689
- properties: resolvedVersion,
690
- });
691
- throw error;
692
- }
693
- }
694
-
695
- /**
696
- * Tracks a telemetry event with MSAL module-specific naming and metadata.
697
- *
698
- * This protected method provides a standardized way to track events within the MSAL module.
699
- * It automatically prefixes the event name with 'module-msal.' and includes the module's
700
- * configured scope and metadata.
701
- *
702
- * @param name - The event name (will be prefixed with 'module-msal.')
703
- * @param level - The telemetry level for the event
704
- * @param options - Additional telemetry options (excluding type, name, level, scope, metadata)
705
- */
706
- protected _trackEvent(
707
- name: string,
708
- level: TelemetryLevel,
709
- options?: Omit<TelemetryItem, 'type' | 'name' | 'level' | 'scope' | 'metadata'>,
710
- ): void {
711
- this.#telemetry.provider?.trackEvent({
712
- name: `module-msal.${name}`,
713
- level,
714
- scope: this.#telemetry.scope,
715
- metadata: this.#telemetry.metadata,
716
- ...options,
717
- });
718
- }
719
-
720
- /**
721
- * Starts a telemetry measurement with MSAL module-specific naming and metadata.
722
- *
723
- * This protected method provides a standardized way to measure performance within the MSAL module.
724
- * It automatically prefixes the measurement name with 'module-msal.' and includes the module's
725
- * configured scope and metadata. Returns a measurement object with a measure function.
726
- *
727
- * If no telemetry provider is available, returns a no-op measurement that returns -1.
728
- *
729
- * @param name - The measurement name (will be prefixed with 'module-msal.')
730
- * @param level - The telemetry level for the measurement
731
- * @param options - Additional telemetry options (excluding type, name, level, scope, metadata)
732
- * @returns A measurement object with a measure function, or a no-op measurement if no provider
733
- */
734
- protected _trackMeasurement(
735
- name: string,
736
- level: TelemetryLevel,
737
- options?: Omit<TelemetryItem, 'type' | 'name' | 'level' | 'scope' | 'metadata'>,
738
- ): Pick<IMeasurement, 'measure'> {
739
- return (
740
- this.#telemetry.provider?.measure({
741
- name: `module-msal.${name}`,
742
- level,
743
- scope: this.#telemetry.scope,
744
- metadata: this.#telemetry.metadata,
745
- ...options,
746
- }) ?? {
747
- measure: () => -1,
748
- }
749
- );
750
- }
751
-
752
- /**
753
- * Tracks a telemetry exception with MSAL module-specific naming and metadata.
754
- *
755
- * This protected method provides a standardized way to track exceptions within the MSAL module.
756
- * It automatically prefixes the exception name with 'module-msal.' and includes the module's
757
- * configured scope and metadata.
758
- *
759
- * @param name - The exception name (will be prefixed with 'module-msal.')
760
- * @param level - The telemetry level for the exception
761
- * @param options - Additional telemetry options (excluding type, name, level, scope, metadata)
762
- */
763
- protected _trackException(
764
- name: string,
765
- level: TelemetryLevel,
766
- options: Omit<TelemetryException, 'type' | 'name' | 'level' | 'scope' | 'metadata'>,
767
- ): void {
768
- this.#telemetry.provider?.trackException({
769
- name: `module-msal.${name}`,
770
- level,
771
- scope: this.#telemetry.scope,
772
- metadata: this.#telemetry.metadata,
773
- ...options,
774
- });
775
- }
776
- }