@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.
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +8 -5
- package/CHANGELOG.md +0 -1212
- package/docs/api-reference.md +0 -85
- package/docs/auth-code-flow.md +0 -86
- package/docs/migration-v2-to-v4.md +0 -115
- package/docs/testing.md +0 -191
- package/docs/troubleshooting.md +0 -17
- package/docs/version-management.md +0 -67
- package/src/MsalClient.interface.ts +0 -139
- package/src/MsalClient.ts +0 -326
- package/src/MsalConfigurator.ts +0 -486
- package/src/MsalProvider.interface.ts +0 -179
- package/src/MsalProvider.ts +0 -776
- package/src/MsalProxyProvider.interface.ts +0 -72
- package/src/__tests__/MsalConfigurator.test.ts +0 -222
- package/src/__tests__/MsalProvider.test.ts +0 -74
- package/src/__tests__/create-proxy-provider.test.ts +0 -77
- package/src/__tests__/mock/create-mock-user-from-token.test.ts +0 -46
- package/src/__tests__/mock/msal-mock.test.ts +0 -613
- package/src/__tests__/versioning/resolve-version.test.ts +0 -161
- package/src/create-client-log-callback.ts +0 -102
- package/src/create-proxy-provider.ts +0 -97
- package/src/index.ts +0 -48
- package/src/mock/MsalMockClient.ts +0 -618
- package/src/mock/MsalMockConfigurator.ts +0 -305
- package/src/mock/create-mock-token.ts +0 -92
- package/src/mock/create-mock-user-from-token.ts +0 -46
- package/src/mock/create-msal-mock-client.ts +0 -25
- package/src/mock/decode-jwt-segment.ts +0 -22
- package/src/mock/index.ts +0 -30
- package/src/mock/module.ts +0 -54
- package/src/module.ts +0 -142
- package/src/msal-config-schema.ts +0 -81
- package/src/static.ts +0 -38
- package/src/telemetry-config-schema.ts +0 -25
- package/src/types.ts +0 -16
- package/src/util/compare-origin.ts +0 -18
- package/src/util/normalize-uri.ts +0 -24
- package/src/util/redirect.ts +0 -19
- package/src/v2/IAuthClient.interface.ts +0 -114
- package/src/v2/Logger.ts +0 -204
- package/src/v2/MsalProvider.interface.ts +0 -102
- package/src/v2/create-proxy-client.ts +0 -195
- package/src/v2/create-proxy-provider.ts +0 -177
- package/src/v2/map-account-info.ts +0 -23
- package/src/v2/map-authentication-result.ts +0 -28
- package/src/v2/types.ts +0 -674
- package/src/v4/create-proxy-provider.ts +0 -75
- package/src/v4/index.ts +0 -13
- package/src/v4/types.ts +0 -727
- package/src/version.ts +0 -2
- package/src/versioning/VersionError.ts +0 -64
- package/src/versioning/index.ts +0 -29
- package/src/versioning/resolve-version.ts +0 -154
- package/src/versioning/types.ts +0 -60
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
package/src/MsalProvider.ts
DELETED
|
@@ -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
|
-
}
|