@equinor/fusion-framework-module-msal 5.1.2 → 6.0.0-next.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 (144) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +237 -40
  3. package/dist/esm/MsalClient.interface.js +2 -0
  4. package/dist/esm/MsalClient.interface.js.map +1 -0
  5. package/dist/esm/MsalClient.js +215 -0
  6. package/dist/esm/MsalClient.js.map +1 -0
  7. package/dist/esm/MsalConfigurator.js +248 -0
  8. package/dist/esm/MsalConfigurator.js.map +1 -0
  9. package/dist/esm/MsalProvider.interface.js +2 -0
  10. package/dist/esm/MsalProvider.interface.js.map +1 -0
  11. package/dist/esm/MsalProvider.js +525 -0
  12. package/dist/esm/MsalProvider.js.map +1 -0
  13. package/dist/esm/MsalProxyProvider.interface.js +2 -0
  14. package/dist/esm/MsalProxyProvider.interface.js.map +1 -0
  15. package/dist/esm/__tests__/versioning/resolve-version.test.js +29 -38
  16. package/dist/esm/__tests__/versioning/resolve-version.test.js.map +1 -1
  17. package/dist/esm/create-client-log-callback.js +87 -0
  18. package/dist/esm/create-client-log-callback.js.map +1 -0
  19. package/dist/esm/create-proxy-provider.js +84 -0
  20. package/dist/esm/create-proxy-provider.js.map +1 -0
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/module.js +64 -16
  24. package/dist/esm/module.js.map +1 -1
  25. package/dist/esm/static.js +32 -2
  26. package/dist/esm/static.js.map +1 -1
  27. package/dist/esm/types.js +9 -0
  28. package/dist/esm/types.js.map +1 -1
  29. package/dist/esm/util/compare-origin.js +11 -0
  30. package/dist/esm/util/compare-origin.js.map +1 -0
  31. package/dist/esm/{v2/client/util/url.js → util/normalize-uri.js} +1 -10
  32. package/dist/esm/util/normalize-uri.js.map +1 -0
  33. package/dist/esm/{v2/client/util/browser.js → util/redirect.js} +1 -1
  34. package/dist/esm/util/redirect.js.map +1 -0
  35. package/dist/esm/v2/IAuthClient.interface.js +2 -0
  36. package/dist/esm/v2/IAuthClient.interface.js.map +1 -0
  37. package/dist/esm/v2/IPublicClientApplication.interface.js +2 -0
  38. package/dist/esm/v2/IPublicClientApplication.interface.js.map +1 -0
  39. package/dist/esm/v2/MsalProvider.interface.js +2 -0
  40. package/dist/esm/v2/MsalProvider.interface.js.map +1 -0
  41. package/dist/esm/v2/create-proxy-client.js +155 -0
  42. package/dist/esm/v2/create-proxy-client.js.map +1 -0
  43. package/dist/esm/v2/create-proxy-provider.js +140 -0
  44. package/dist/esm/v2/create-proxy-provider.js.map +1 -0
  45. package/dist/esm/v2/map-account-info.js +18 -0
  46. package/dist/esm/v2/map-account-info.js.map +1 -0
  47. package/dist/esm/v2/map-authentication-result.js +22 -0
  48. package/dist/esm/v2/map-authentication-result.js.map +1 -0
  49. package/dist/esm/version.js +1 -1
  50. package/dist/esm/version.js.map +1 -1
  51. package/dist/esm/versioning/resolve-version.js +28 -16
  52. package/dist/esm/versioning/resolve-version.js.map +1 -1
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/dist/types/MsalClient.d.ts +141 -0
  55. package/dist/types/MsalClient.interface.d.ts +103 -0
  56. package/dist/types/MsalConfigurator.d.ts +147 -0
  57. package/dist/types/MsalProvider.d.ts +291 -0
  58. package/dist/types/MsalProvider.interface.d.ts +159 -0
  59. package/dist/types/MsalProxyProvider.interface.d.ts +52 -0
  60. package/dist/types/create-client-log-callback.d.ts +38 -0
  61. package/dist/types/create-proxy-provider.d.ts +19 -0
  62. package/dist/types/index.d.ts +5 -4
  63. package/dist/types/module.d.ts +70 -4
  64. package/dist/types/static.d.ts +32 -1
  65. package/dist/types/types.d.ts +14 -6
  66. package/dist/types/util/redirect.d.ts +1 -0
  67. package/dist/types/v2/IAuthClient.interface.d.ts +68 -0
  68. package/dist/types/v2/IPublicClientApplication.interface.d.ts +68 -0
  69. package/dist/types/v2/MsalProvider.interface.d.ts +85 -0
  70. package/dist/types/v2/create-proxy-client.d.ts +22 -0
  71. package/dist/types/v2/create-proxy-provider.d.ts +24 -0
  72. package/dist/types/v2/map-account-info.d.ts +9 -0
  73. package/dist/types/v2/map-authentication-result.d.ts +9 -0
  74. package/dist/types/v2/types.d.ts +12 -0
  75. package/dist/types/version.d.ts +1 -1
  76. package/dist/types/versioning/resolve-version.d.ts +1 -1
  77. package/package.json +11 -6
  78. package/src/MsalClient.interface.ts +121 -0
  79. package/src/MsalClient.ts +274 -0
  80. package/src/MsalConfigurator.ts +289 -0
  81. package/src/MsalProvider.interface.ts +175 -0
  82. package/src/MsalProvider.ts +597 -0
  83. package/src/MsalProxyProvider.interface.ts +71 -0
  84. package/src/__tests__/versioning/resolve-version.test.ts +29 -42
  85. package/src/create-client-log-callback.ts +101 -0
  86. package/src/create-proxy-provider.ts +89 -0
  87. package/src/index.ts +6 -7
  88. package/src/module.ts +88 -20
  89. package/src/static.ts +32 -3
  90. package/src/types.ts +15 -7
  91. package/src/util/compare-origin.ts +11 -0
  92. package/src/{v2/client/util/url.ts → util/normalize-uri.ts} +0 -10
  93. package/src/v2/IAuthClient.interface.ts +91 -0
  94. package/src/v2/IPublicClientApplication.interface.ts +71 -0
  95. package/src/v2/MsalProvider.interface.ts +92 -0
  96. package/src/v2/create-proxy-client.ts +186 -0
  97. package/src/v2/create-proxy-provider.ts +156 -0
  98. package/src/v2/map-account-info.ts +20 -0
  99. package/src/v2/map-authentication-result.ts +24 -0
  100. package/src/v2/types.ts +12 -0
  101. package/src/version.ts +1 -1
  102. package/src/versioning/resolve-version.ts +35 -28
  103. package/tsconfig.json +3 -0
  104. package/dist/esm/v2/client/behavior.js +0 -5
  105. package/dist/esm/v2/client/behavior.js.map +0 -1
  106. package/dist/esm/v2/client/client.js +0 -142
  107. package/dist/esm/v2/client/client.js.map +0 -1
  108. package/dist/esm/v2/client/create-auth-client.js +0 -36
  109. package/dist/esm/v2/client/create-auth-client.js.map +0 -1
  110. package/dist/esm/v2/client/index.js +0 -5
  111. package/dist/esm/v2/client/index.js.map +0 -1
  112. package/dist/esm/v2/client/log/console.js +0 -45
  113. package/dist/esm/v2/client/log/console.js.map +0 -1
  114. package/dist/esm/v2/client/request.js +0 -2
  115. package/dist/esm/v2/client/request.js.map +0 -1
  116. package/dist/esm/v2/client/util/browser.js.map +0 -1
  117. package/dist/esm/v2/client/util/url.js.map +0 -1
  118. package/dist/esm/v2/configurator.js +0 -42
  119. package/dist/esm/v2/configurator.js.map +0 -1
  120. package/dist/esm/v2/index.js +0 -3
  121. package/dist/esm/v2/index.js.map +0 -1
  122. package/dist/esm/v2/provider.js +0 -115
  123. package/dist/esm/v2/provider.js.map +0 -1
  124. package/dist/types/v2/client/behavior.d.ts +0 -13
  125. package/dist/types/v2/client/client.d.ts +0 -89
  126. package/dist/types/v2/client/create-auth-client.d.ts +0 -27
  127. package/dist/types/v2/client/index.d.ts +0 -5
  128. package/dist/types/v2/client/log/console.d.ts +0 -28
  129. package/dist/types/v2/client/request.d.ts +0 -65
  130. package/dist/types/v2/configurator.d.ts +0 -32
  131. package/dist/types/v2/index.d.ts +0 -2
  132. package/dist/types/v2/provider.d.ts +0 -59
  133. package/src/v2/client/behavior.ts +0 -14
  134. package/src/v2/client/client.ts +0 -180
  135. package/src/v2/client/create-auth-client.ts +0 -48
  136. package/src/v2/client/index.ts +0 -8
  137. package/src/v2/client/log/console.ts +0 -58
  138. package/src/v2/client/request.ts +0 -66
  139. package/src/v2/configurator.ts +0 -58
  140. package/src/v2/index.ts +0 -2
  141. package/src/v2/provider.ts +0 -178
  142. /package/dist/types/{v2/client/util/browser.d.ts → util/compare-origin.d.ts} +0 -0
  143. /package/dist/types/{v2/client/util/url.d.ts → util/normalize-uri.d.ts} +0 -0
  144. /package/src/{v2/client/util/browser.ts → util/redirect.ts} +0 -0
@@ -0,0 +1,597 @@
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
+ AcquireTokenResult,
17
+ IMsalClient,
18
+ LoginOptions,
19
+ LoginResult,
20
+ LogoutOptions,
21
+ } from './MsalClient.interface';
22
+
23
+ import type { AccountInfo, AuthenticationResult } from './types';
24
+ import { resolveVersion } from './versioning/resolve-version';
25
+ import { version } from './version';
26
+ import type { MsalModuleVersion } from './static';
27
+
28
+ export type { IMsalProvider };
29
+
30
+ /**
31
+ * MSAL v4 compatible authentication provider for Fusion Framework.
32
+ *
33
+ * This provider wraps the MSAL v4 PublicClientApplication and provides
34
+ * a simplified interface for authentication operations while maintaining
35
+ * compatibility with the Fusion Framework module system.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const provider = new MsalProvider({
40
+ * clientId: 'your-client-id',
41
+ * tenantId: 'your-tenant-id',
42
+ * redirectUri: 'https://your-app.com/callback'
43
+ * });
44
+ *
45
+ * // Login user
46
+ * await provider.login({ scopes: ['User.Read'] });
47
+ *
48
+ * // Acquire token
49
+ * const token = await provider.acquireAccessToken({
50
+ * scopes: ['https://graph.microsoft.com/.default']
51
+ * });
52
+ * ```
53
+ */
54
+ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsalProvider {
55
+ #client: IMsalClient;
56
+ #telemetry: {
57
+ provider?: ITelemetryProvider;
58
+ metadata: Record<string, unknown>;
59
+ scope: string[];
60
+ };
61
+ #requiresAuth?: boolean;
62
+
63
+ /**
64
+ * The MSAL module version enum value indicating the API compatibility level.
65
+ *
66
+ * This getter resolves the current version string to its corresponding enum value,
67
+ * determining which MSAL version's API surface this provider implements. This is used
68
+ * for version-specific behavior and proxy provider creation.
69
+ *
70
+ * @returns The MSAL module version enum (V2, V4, etc.)
71
+ */
72
+ get msalVersion(): MsalModuleVersion {
73
+ return resolveVersion(version).enumVersion;
74
+ }
75
+
76
+ /**
77
+ * The MSAL client instance.
78
+ *
79
+ * Provides access to the underlying MSAL PublicClientApplication for advanced use cases.
80
+ * Prefer using provider methods for standard authentication operations.
81
+ */
82
+ get client(): IMsalClient {
83
+ return this.#client;
84
+ }
85
+
86
+ /**
87
+ * The currently authenticated account.
88
+ *
89
+ * Returns the active account if a user is authenticated, or null if no user is logged in.
90
+ * This is a shorthand for `client.getActiveAccount()`.
91
+ */
92
+ get account(): AccountInfo | null {
93
+ return this.#client.getActiveAccount();
94
+ }
95
+
96
+ /**
97
+ * Creates a new MSAL provider instance.
98
+ *
99
+ * @param config - Complete MSAL configuration including client, telemetry, and auth requirements
100
+ * @throws {Error} If client is not provided in configuration
101
+ */
102
+ constructor(config: MsalConfig) {
103
+ super({
104
+ version,
105
+ config,
106
+ });
107
+ this.#requiresAuth = config.requiresAuth;
108
+ this.#telemetry = config.telemetry;
109
+
110
+ // Validate required client configuration
111
+ if (!config.client) {
112
+ const error = new Error(
113
+ 'Client is required, please provide a valid client in the configuration',
114
+ );
115
+ this._trackException('constructor.client-required', TelemetryLevel.Error, {
116
+ exception: error,
117
+ });
118
+ throw error;
119
+ }
120
+ this.#client = config.client;
121
+ }
122
+
123
+ /**
124
+ * Initializes the MSAL provider and sets up authentication state.
125
+ *
126
+ * This method must be called before using any authentication operations. It performs:
127
+ * - Client initialization
128
+ * - Redirect result handling (if returning from auth flow)
129
+ * - Automatic login attempt if requiresAuth is enabled and no valid session exists
130
+ *
131
+ * @returns Promise that resolves when initialization is complete
132
+ *
133
+ * @remarks
134
+ * The provider will attempt automatic login with empty scopes if requiresAuth is true.
135
+ * Apps should call acquireToken with actual scopes after initialization completes.
136
+ */
137
+ async initialize(): Promise<void> {
138
+ const measurement = this._trackMeasurement('initialize', TelemetryLevel.Debug);
139
+ // Initialize the underlying MSAL client first
140
+ await this.#client.initialize();
141
+
142
+ // Only attempt authentication if this provider requires it
143
+ if (this.#requiresAuth) {
144
+ // Priority 1: Check if returning from redirect-based authentication
145
+ // This handles cases where user just completed a login/acquireToken via redirect
146
+ const handleRedirectResult = await this.handleRedirect();
147
+ if (handleRedirectResult?.account) {
148
+ // Successfully authenticated via redirect - set as active account
149
+ // This means the user was redirected to Microsoft and came back authenticated
150
+ this.#client.setActiveAccount(handleRedirectResult.account);
151
+ this._trackEvent('initialize.active-account-set-by-callback', TelemetryLevel.Information, {
152
+ properties: {
153
+ username: handleRedirectResult.account.username,
154
+ },
155
+ });
156
+ } else if (!this.#client.hasValidClaims) {
157
+ // Priority 2: No valid session found - attempt automatic login
158
+ // This handles first-time app load when no authentication state exists
159
+ // Note: Using empty scopes here as we don't know what scopes the app needs yet
160
+ // App should call acquireToken with actual scopes after initialization
161
+ const loginResult = await this.login({ request: { scopes: [] } });
162
+ if (loginResult?.account) {
163
+ // Automatic login successful - set as active account
164
+ this.#client.setActiveAccount(loginResult.account);
165
+ this._trackEvent('initialize.active-account-set-by-login', TelemetryLevel.Information, {
166
+ properties: {
167
+ username: loginResult.account.username,
168
+ },
169
+ });
170
+ }
171
+ }
172
+ // Priority 3: If hasValidClaims is true, user is already authenticated - no action needed
173
+ }
174
+ measurement.measure();
175
+ }
176
+
177
+ /**
178
+ * Acquire an access token string for the specified scopes
179
+ *
180
+ * @param options - Token acquisition options (same as acquireToken)
181
+ * @returns Promise resolving to access token string, or undefined if acquisition fails
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const token = await msalProvider.acquireAccessToken({
186
+ * request: { scopes: ['api.read'] }
187
+ * });
188
+ * if (token) {
189
+ * // Use token for API calls
190
+ * fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
191
+ * }
192
+ * ```
193
+ */
194
+ async acquireAccessToken(options: AcquireTokenOptionsLegacy): Promise<string | undefined> {
195
+ const { accessToken } = (await this.acquireToken(options)) ?? {};
196
+ return accessToken;
197
+ }
198
+
199
+ /**
200
+ * Acquire full authentication result for the specified scopes
201
+ *
202
+ * @param options - Token acquisition options including scopes, behavior, and silent mode
203
+ * @param options.request.scopes - Array of OAuth scopes to request access for
204
+ * @param options.scopes - Legacy scopes format (deprecated, use request.scopes)
205
+ * @param options.behavior - Authentication behavior ('redirect' or 'popup')
206
+ * @param options.silent - Whether to attempt silent token acquisition first
207
+ * @returns Promise resolving to authentication result containing access token and account info
208
+ *
209
+ * @remark Empty scopes are currently tracked as telemetry exceptions but execution continues for monitoring purposes.
210
+ * This behavior will be changed to throw exceptions once sufficient metrics are collected.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * // Modern API format
215
+ * const result = await msalProvider.acquireToken({
216
+ * request: { scopes: ['user.read', 'api.write'] },
217
+ * behavior: 'redirect',
218
+ * silent: true
219
+ * });
220
+ *
221
+ * // Legacy format (deprecated)
222
+ * const result = await msalProvider.acquireToken({
223
+ * scopes: ['user.read'],
224
+ * silent: false
225
+ * });
226
+ * ```
227
+ */
228
+ async acquireToken(options: AcquireTokenOptionsLegacy): Promise<AcquireTokenResult> {
229
+ const { behavior = 'redirect', silent = true, request } = options;
230
+ const account = request.account ?? this.account ?? undefined;
231
+ // Extract scopes from either new format (request.scopes) or legacy format (scopes)
232
+ const scopes = options.request?.scopes ?? options?.scopes ?? [];
233
+
234
+ const telemetryProperties = { behavior, silent, scopes };
235
+
236
+ // Track usage of deprecated legacy scopes format for migration monitoring
237
+ if (options.scopes) {
238
+ this._trackEvent('acquireToken.legacy-scopes-provided', TelemetryLevel.Warning, {
239
+ properties: telemetryProperties,
240
+ });
241
+ }
242
+
243
+ // Handle empty scopes - currently monitoring for telemetry, will throw in future
244
+ if (scopes.length === 0) {
245
+ const exception = new Error('Empty scopes provided, not allowed');
246
+ this._trackException('acquireToken.missing-scope', TelemetryLevel.Warning, {
247
+ exception,
248
+ properties: telemetryProperties,
249
+ });
250
+ // TODO: throw exception when sufficient metrics are collected
251
+ // This allows us to monitor how often empty scopes are provided before enforcing validation
252
+ }
253
+
254
+ try {
255
+ const measurement = this._trackMeasurement('acquireToken', TelemetryLevel.Information, {
256
+ properties: telemetryProperties,
257
+ });
258
+ // Merge account, original request options, and resolved scopes
259
+ // Account ensures context awareness, request preserves custom options, scopes uses resolved value
260
+ const result = await this.#client.acquireToken({
261
+ behavior,
262
+ silent,
263
+ request: { ...options.request, account, scopes },
264
+ });
265
+ measurement?.measure();
266
+ return result;
267
+ } catch (error) {
268
+ this._trackException('acquireToken-failed', TelemetryLevel.Error, {
269
+ exception: error as Error,
270
+ properties: telemetryProperties,
271
+ });
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Authenticates a user using Microsoft Authentication Library.
278
+ *
279
+ * This method implements a sophisticated login flow that **attempts silent authentication
280
+ * first by default** (`silent: true`) and falls back to interactive authentication based on the specified
281
+ * behavior. The flow prioritizes user experience by minimizing unnecessary popups or redirects.
282
+ *
283
+ * **Authentication Flow:**
284
+ * 1. **Silent Login Attempt** (default behavior):
285
+ * - Attempts SSO silent authentication using existing session
286
+ * - Requires `loginHint` to be provided (uses active account's username if available)
287
+ * - Falls back to interactive login if silent attempt fails
288
+ *
289
+ * 2. **Interactive Login Fallback** (based on `behavior`):
290
+ * - `popup`: Opens authentication popup window (default)
291
+ * - `redirect`: Redirects current window to authentication page
292
+ *
293
+ * **Default Behavior:**
294
+ * - Attempts silent authentication first (`silent: true`)
295
+ * - Falls back to popup authentication (`behavior: 'popup'`)
296
+ * - Uses active account's username as login hint if not provided
297
+ * - Warns if no scopes are specified (uses empty array)
298
+ *
299
+ * @param options - Login configuration options
300
+ * @param options.request - Authentication request parameters (scopes, loginHint, etc.)
301
+ * @param options.behavior - Authentication method: 'popup' (default) or 'redirect'
302
+ * @param options.silent - Whether to attempt silent authentication first (**default: true**)
303
+ *
304
+ * @returns Promise resolving to authentication result or undefined
305
+ *
306
+ * @throws {Error} When authentication fails or invalid parameters provided
307
+ *
308
+ * @example
309
+ * ```typescript
310
+ * // Basic login (silent first, popup fallback - DEFAULT BEHAVIOR)
311
+ * const result = await provider.login({
312
+ * request: { scopes: ['User.Read'] }
313
+ * });
314
+ *
315
+ * // Skip silent, go straight to redirect
316
+ * await provider.login({
317
+ * request: { scopes: ['User.Read'] },
318
+ * silent: false,
319
+ * behavior: 'redirect'
320
+ * });
321
+ * ```
322
+ */
323
+ async login(options: LoginOptions): Promise<LoginResult> {
324
+ const { behavior = 'redirect', silent = true, request } = options;
325
+
326
+ // Determine if silent login is possible based on available account/hint information
327
+ // Silent login requires either an account object or a loginHint to work
328
+ const canLoginSilently = silent && (request.account || request.loginHint);
329
+
330
+ const telemetryProperties = { behavior, silent, canLoginSilently, scopes: request.scopes };
331
+
332
+ // Default to active account if no account/hint provided in request
333
+ // This allows silent login to work automatically with existing authentication state
334
+ request.account ??= this.account ?? undefined;
335
+
336
+ // Default to empty scopes if none provided
337
+ // Empty scopes are tracked for monitoring but allowed for compatibility
338
+ if (!request.scopes) {
339
+ request.scopes = [];
340
+ this._trackEvent('login.missing-scope', TelemetryLevel.Warning, {
341
+ properties: telemetryProperties,
342
+ });
343
+ }
344
+
345
+ this._trackEvent('login', TelemetryLevel.Information, {
346
+ properties: telemetryProperties,
347
+ });
348
+
349
+ // Attempt silent authentication first if conditions are met
350
+ // This provides better UX by avoiding unnecessary popups/redirects
351
+ if (canLoginSilently) {
352
+ try {
353
+ return await this.#client.ssoSilent(request);
354
+ } catch (error) {
355
+ // Silent login failed - track for monitoring but continue to interactive flow
356
+ this._trackException('login.silent-failed', TelemetryLevel.Warning, {
357
+ exception: error as Error,
358
+ properties: telemetryProperties,
359
+ });
360
+ // Fall through to interactive authentication
361
+ }
362
+ }
363
+
364
+ switch (behavior) {
365
+ case 'popup':
366
+ return await this.#client.loginPopup(request);
367
+ case 'redirect':
368
+ await this.#client.loginRedirect(request);
369
+ break;
370
+ default:
371
+ throw new Error(
372
+ `Invalid behavior provided: ${behavior}, please provide a valid behavior, see options.behavior for more information.`,
373
+ );
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Logs out the current user and clears authentication state.
379
+ *
380
+ * This method initiates a logout flow using redirect, which navigates to Microsoft's
381
+ * logout endpoint to clear session cookies and tokens. The method returns true on
382
+ * successful logout initiation, or false if logout fails.
383
+ *
384
+ * @param options - Optional logout configuration
385
+ * @param options.account - Account to log out (defaults to active account)
386
+ * @param options.redirectUri - URI to redirect to after logout completes
387
+ * @returns Promise resolving to true on success, false on failure
388
+ *
389
+ * @remarks
390
+ * - Logout always uses redirect flow (more reliable than popup)
391
+ * - Returns false on error instead of throwing to prevent breaking app flow
392
+ * - Browser will navigate away during logout process
393
+ *
394
+ * @example
395
+ * ```typescript
396
+ * // Basic logout
397
+ * const success = await provider.logout();
398
+ *
399
+ * // Logout with custom redirect
400
+ * await provider.logout({ redirectUri: 'https://app.com/logout' });
401
+ * ```
402
+ */
403
+ async logout(options?: LogoutOptions): Promise<boolean> {
404
+ this._trackEvent('logout', TelemetryLevel.Information, {
405
+ properties: {
406
+ redirectUri: options?.redirectUri,
407
+ },
408
+ });
409
+
410
+ try {
411
+ // Logout the specific account (or current account if none specified)
412
+ await this.#client.logout({ account: this.account ?? undefined, ...options });
413
+ return true; // Success
414
+ } catch (error) {
415
+ // Logout failed - track error but don't throw to avoid breaking app flow
416
+ this._trackException('logout.failed', TelemetryLevel.Error, {
417
+ exception: error as Error,
418
+ });
419
+ }
420
+ return false; // Failed
421
+ }
422
+
423
+ /**
424
+ * Processes any pending authentication redirect after browser navigation.
425
+ *
426
+ * This method must be called on app initialization to handle tokens and account information
427
+ * returned by Microsoft's identity provider after redirect-based authentication flows.
428
+ *
429
+ * @returns Promise resolving to authentication result or null if no redirect pending
430
+ *
431
+ * @remarks
432
+ * - Should be called once on app startup before other authentication operations
433
+ * - Only returns a result if user just completed redirect-based login/acquireToken
434
+ * - Safe to call even when no redirect is pending (returns null)
435
+ *
436
+ * @example
437
+ * ```typescript
438
+ * // Call on app startup
439
+ * const result = await provider.handleRedirect();
440
+ * if (result?.account) {
441
+ * console.log(`Authenticated as: ${result.account.username}`);
442
+ * provider.client.setActiveAccount(result.account);
443
+ * }
444
+ * ```
445
+ */
446
+ async handleRedirect(): Promise<AuthenticationResult | null> {
447
+ // Process any pending redirect from authentication flow
448
+ const result = await this.client.handleRedirectPromise();
449
+ if (result) {
450
+ // Track successful redirect completion for monitoring
451
+ this._trackEvent('handleRedirect.success', TelemetryLevel.Information, {
452
+ properties: {
453
+ username: result.account?.username,
454
+ },
455
+ });
456
+ }
457
+ return result;
458
+ }
459
+
460
+ /**
461
+ * Creates a proxy provider for version compatibility.
462
+ *
463
+ * This method creates a version-specific proxy wrapper around this provider to maintain
464
+ * backward compatibility with different MSAL versions while using the latest v4 implementation.
465
+ *
466
+ * @param version - Target version string (e.g., '2.0.0', '4.0.0', 'v2', 'v4')
467
+ * @returns Proxy provider covariant with the specified version
468
+ *
469
+ * @remarks
470
+ * - Proxies adapt the v4 API to match older version signatures
471
+ * - Useful for gradual migration scenarios
472
+ * - Version compatibility is tracked via telemetry
473
+ * - Throws error if unsupported version is requested
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * // Create v2-compatible proxy
478
+ * const v2Proxy = provider.createProxyProvider('2.0.0');
479
+ * await v2Proxy.login(); // Uses v2-compatible signature
480
+ * ```
481
+ */
482
+ createProxyProvider<T = IMsalProvider>(version: string): T {
483
+ // Track proxy provider creation for compatibility monitoring
484
+ this._trackEvent('createProxyProvider', TelemetryLevel.Debug, {
485
+ properties: {
486
+ version: version,
487
+ },
488
+ });
489
+
490
+ // Parse and validate the requested version string
491
+ const resolvedVersion = resolveVersion(version);
492
+
493
+ this._trackEvent('createProxyProvider.version-resolved', TelemetryLevel.Information, {
494
+ properties: resolvedVersion,
495
+ });
496
+
497
+ // Warn if using outdated version - helps track migration progress
498
+ if (!resolvedVersion.satisfiesLatest) {
499
+ this._trackEvent('createProxyProvider.outdated-version', TelemetryLevel.Warning, {
500
+ properties: resolvedVersion,
501
+ });
502
+ }
503
+
504
+ try {
505
+ // create the proxy provider
506
+ return createProxyProvider(this, version);
507
+ } catch (error) {
508
+ this._trackException('createProxyProvider.failed', TelemetryLevel.Error, {
509
+ exception: error as Error,
510
+ properties: resolvedVersion,
511
+ });
512
+ throw error;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Tracks a telemetry event with MSAL module-specific naming and metadata.
518
+ *
519
+ * This protected method provides a standardized way to track events within the MSAL module.
520
+ * It automatically prefixes the event name with 'module-msal.' and includes the module's
521
+ * configured scope and metadata.
522
+ *
523
+ * @param name - The event name (will be prefixed with 'module-msal.')
524
+ * @param level - The telemetry level for the event
525
+ * @param options - Additional telemetry options (excluding type, name, level, scope, metadata)
526
+ */
527
+ protected _trackEvent(
528
+ name: string,
529
+ level: TelemetryLevel,
530
+ options?: Omit<TelemetryItem, 'type' | 'name' | 'level' | 'scope' | 'metadata'>,
531
+ ): void {
532
+ this.#telemetry.provider?.trackEvent({
533
+ name: `module-msal.${name}`,
534
+ level,
535
+ scope: this.#telemetry.scope,
536
+ metadata: this.#telemetry.metadata,
537
+ ...options,
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Starts a telemetry measurement with MSAL module-specific naming and metadata.
543
+ *
544
+ * This protected method provides a standardized way to measure performance within the MSAL module.
545
+ * It automatically prefixes the measurement name with 'module-msal.' and includes the module's
546
+ * configured scope and metadata. Returns a measurement object with a measure function.
547
+ *
548
+ * If no telemetry provider is available, returns a no-op measurement that returns -1.
549
+ *
550
+ * @param name - The measurement name (will be prefixed with 'module-msal.')
551
+ * @param level - The telemetry level for the measurement
552
+ * @param options - Additional telemetry options (excluding type, name, level, scope, metadata)
553
+ * @returns A measurement object with a measure function, or a no-op measurement if no provider
554
+ */
555
+ protected _trackMeasurement(
556
+ name: string,
557
+ level: TelemetryLevel,
558
+ options?: Omit<TelemetryItem, 'type' | 'name' | 'level' | 'scope' | 'metadata'>,
559
+ ): Pick<IMeasurement, 'measure'> {
560
+ return (
561
+ this.#telemetry.provider?.measure({
562
+ name: `module-msal.${name}`,
563
+ level,
564
+ scope: this.#telemetry.scope,
565
+ metadata: this.#telemetry.metadata,
566
+ ...options,
567
+ }) ?? {
568
+ measure: () => -1,
569
+ }
570
+ );
571
+ }
572
+
573
+ /**
574
+ * Tracks a telemetry exception with MSAL module-specific naming and metadata.
575
+ *
576
+ * This protected method provides a standardized way to track exceptions within the MSAL module.
577
+ * It automatically prefixes the exception name with 'module-msal.' and includes the module's
578
+ * configured scope and metadata.
579
+ *
580
+ * @param name - The exception name (will be prefixed with 'module-msal.')
581
+ * @param level - The telemetry level for the exception
582
+ * @param options - Additional telemetry options (excluding type, name, level, scope, metadata)
583
+ */
584
+ protected _trackException(
585
+ name: string,
586
+ level: TelemetryLevel,
587
+ options: Omit<TelemetryException, 'type' | 'name' | 'level' | 'scope' | 'metadata'>,
588
+ ): void {
589
+ this.#telemetry.provider?.trackException({
590
+ name: `module-msal.${name}`,
591
+ level,
592
+ scope: this.#telemetry.scope,
593
+ metadata: this.#telemetry.metadata,
594
+ ...options,
595
+ });
596
+ }
597
+ }
@@ -0,0 +1,71 @@
1
+ import type { SemVer } from 'semver';
2
+ import type { MsalModuleVersion } from './static';
3
+
4
+ import type { IMsalProvider } from './MsalProvider.interface';
5
+ import type { IMsalProvider as IMsalProvider_v2 } from './v2/MsalProvider.interface';
6
+
7
+ /**
8
+ * Type mapping between MSAL module versions and their corresponding provider interfaces.
9
+ *
10
+ * This mapping ensures type-safe creation of proxy providers for different MSAL versions.
11
+ * Each version maps to its appropriate provider interface type.
12
+ *
13
+ * @internal
14
+ */
15
+ type ProxyProviderMap = {
16
+ [MsalModuleVersion.V2]: IMsalProvider_v2;
17
+ [MsalModuleVersion.V4]: IMsalProvider;
18
+ };
19
+
20
+ /**
21
+ * Interface for providers that can create version-compatible proxy providers.
22
+ *
23
+ * This interface enables backward compatibility by allowing providers to create
24
+ * proxies that adapt their API to match different MSAL version signatures. The proxy
25
+ * wraps the v4 implementation and exposes it through older version interfaces.
26
+ *
27
+ * @remarks
28
+ * This interface should ideally be defined in the @equinor/fusion-framework-module package
29
+ * for broader framework compatibility.
30
+ *
31
+ * @property version - The semantic version of the provider
32
+ * @property msalVersion - The MSAL module version enum value
33
+ * @property createProxyProvider - Method to create a version-specific proxy provider
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const provider: IMsalProvider = new MsalProvider(config);
38
+ *
39
+ * // Create a v2-compatible proxy
40
+ * const v2Proxy = provider.createProxyProvider('2.0.0');
41
+ * // v2Proxy now has v2-compatible method signatures
42
+ * ```
43
+ */
44
+ export interface IProxyProvider {
45
+ /**
46
+ * The semantic version of the provider.
47
+ *
48
+ * This represents the actual version number of the MSAL implementation,
49
+ * following semantic versioning (semver) standards.
50
+ */
51
+ readonly version: string | SemVer;
52
+
53
+ /**
54
+ * The MSAL module version enum value indicating the API compatibility level.
55
+ *
56
+ * This property specifies which MSAL version's API surface this provider implements,
57
+ * allowing for version-specific behavior and proxy provider creation.
58
+ */
59
+ msalVersion: MsalModuleVersion;
60
+
61
+ /**
62
+ * Creates a proxy provider compatible with the specified MSAL version.
63
+ *
64
+ * The proxy adapts the provider's v4 API to match the requested version's interface,
65
+ * enabling backward compatibility during migration scenarios.
66
+ *
67
+ * @param version - Target version key (V2, V4, or Latest)
68
+ * @returns Proxy provider with version-specific type
69
+ */
70
+ createProxyProvider<T extends keyof ProxyProviderMap>(version: T): ProxyProviderMap[T];
71
+ }