@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.
- package/CHANGELOG.md +98 -0
- package/README.md +237 -40
- package/dist/esm/MsalClient.interface.js +2 -0
- package/dist/esm/MsalClient.interface.js.map +1 -0
- package/dist/esm/MsalClient.js +215 -0
- package/dist/esm/MsalClient.js.map +1 -0
- package/dist/esm/MsalConfigurator.js +248 -0
- package/dist/esm/MsalConfigurator.js.map +1 -0
- package/dist/esm/MsalProvider.interface.js +2 -0
- package/dist/esm/MsalProvider.interface.js.map +1 -0
- package/dist/esm/MsalProvider.js +525 -0
- package/dist/esm/MsalProvider.js.map +1 -0
- package/dist/esm/MsalProxyProvider.interface.js +2 -0
- package/dist/esm/MsalProxyProvider.interface.js.map +1 -0
- package/dist/esm/__tests__/versioning/resolve-version.test.js +29 -38
- package/dist/esm/__tests__/versioning/resolve-version.test.js.map +1 -1
- package/dist/esm/create-client-log-callback.js +87 -0
- package/dist/esm/create-client-log-callback.js.map +1 -0
- package/dist/esm/create-proxy-provider.js +84 -0
- package/dist/esm/create-proxy-provider.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/module.js +64 -16
- package/dist/esm/module.js.map +1 -1
- package/dist/esm/static.js +32 -2
- package/dist/esm/static.js.map +1 -1
- package/dist/esm/types.js +9 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/util/compare-origin.js +11 -0
- package/dist/esm/util/compare-origin.js.map +1 -0
- package/dist/esm/{v2/client/util/url.js → util/normalize-uri.js} +1 -10
- package/dist/esm/util/normalize-uri.js.map +1 -0
- package/dist/esm/{v2/client/util/browser.js → util/redirect.js} +1 -1
- package/dist/esm/util/redirect.js.map +1 -0
- package/dist/esm/v2/IAuthClient.interface.js +2 -0
- package/dist/esm/v2/IAuthClient.interface.js.map +1 -0
- package/dist/esm/v2/IPublicClientApplication.interface.js +2 -0
- package/dist/esm/v2/IPublicClientApplication.interface.js.map +1 -0
- package/dist/esm/v2/MsalProvider.interface.js +2 -0
- package/dist/esm/v2/MsalProvider.interface.js.map +1 -0
- package/dist/esm/v2/create-proxy-client.js +155 -0
- package/dist/esm/v2/create-proxy-client.js.map +1 -0
- package/dist/esm/v2/create-proxy-provider.js +140 -0
- package/dist/esm/v2/create-proxy-provider.js.map +1 -0
- package/dist/esm/v2/map-account-info.js +18 -0
- package/dist/esm/v2/map-account-info.js.map +1 -0
- package/dist/esm/v2/map-authentication-result.js +22 -0
- package/dist/esm/v2/map-authentication-result.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/esm/versioning/resolve-version.js +28 -16
- package/dist/esm/versioning/resolve-version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/MsalClient.d.ts +141 -0
- package/dist/types/MsalClient.interface.d.ts +103 -0
- package/dist/types/MsalConfigurator.d.ts +147 -0
- package/dist/types/MsalProvider.d.ts +291 -0
- package/dist/types/MsalProvider.interface.d.ts +159 -0
- package/dist/types/MsalProxyProvider.interface.d.ts +52 -0
- package/dist/types/create-client-log-callback.d.ts +38 -0
- package/dist/types/create-proxy-provider.d.ts +19 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/module.d.ts +70 -4
- package/dist/types/static.d.ts +32 -1
- package/dist/types/types.d.ts +14 -6
- package/dist/types/util/redirect.d.ts +1 -0
- package/dist/types/v2/IAuthClient.interface.d.ts +68 -0
- package/dist/types/v2/IPublicClientApplication.interface.d.ts +68 -0
- package/dist/types/v2/MsalProvider.interface.d.ts +85 -0
- package/dist/types/v2/create-proxy-client.d.ts +22 -0
- package/dist/types/v2/create-proxy-provider.d.ts +24 -0
- package/dist/types/v2/map-account-info.d.ts +9 -0
- package/dist/types/v2/map-authentication-result.d.ts +9 -0
- package/dist/types/v2/types.d.ts +12 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/versioning/resolve-version.d.ts +1 -1
- package/package.json +11 -6
- package/src/MsalClient.interface.ts +121 -0
- package/src/MsalClient.ts +274 -0
- package/src/MsalConfigurator.ts +289 -0
- package/src/MsalProvider.interface.ts +175 -0
- package/src/MsalProvider.ts +597 -0
- package/src/MsalProxyProvider.interface.ts +71 -0
- package/src/__tests__/versioning/resolve-version.test.ts +29 -42
- package/src/create-client-log-callback.ts +101 -0
- package/src/create-proxy-provider.ts +89 -0
- package/src/index.ts +6 -7
- package/src/module.ts +88 -20
- package/src/static.ts +32 -3
- package/src/types.ts +15 -7
- package/src/util/compare-origin.ts +11 -0
- package/src/{v2/client/util/url.ts → util/normalize-uri.ts} +0 -10
- package/src/v2/IAuthClient.interface.ts +91 -0
- package/src/v2/IPublicClientApplication.interface.ts +71 -0
- package/src/v2/MsalProvider.interface.ts +92 -0
- package/src/v2/create-proxy-client.ts +186 -0
- package/src/v2/create-proxy-provider.ts +156 -0
- package/src/v2/map-account-info.ts +20 -0
- package/src/v2/map-authentication-result.ts +24 -0
- package/src/v2/types.ts +12 -0
- package/src/version.ts +1 -1
- package/src/versioning/resolve-version.ts +35 -28
- package/tsconfig.json +3 -0
- package/dist/esm/v2/client/behavior.js +0 -5
- package/dist/esm/v2/client/behavior.js.map +0 -1
- package/dist/esm/v2/client/client.js +0 -142
- package/dist/esm/v2/client/client.js.map +0 -1
- package/dist/esm/v2/client/create-auth-client.js +0 -36
- package/dist/esm/v2/client/create-auth-client.js.map +0 -1
- package/dist/esm/v2/client/index.js +0 -5
- package/dist/esm/v2/client/index.js.map +0 -1
- package/dist/esm/v2/client/log/console.js +0 -45
- package/dist/esm/v2/client/log/console.js.map +0 -1
- package/dist/esm/v2/client/request.js +0 -2
- package/dist/esm/v2/client/request.js.map +0 -1
- package/dist/esm/v2/client/util/browser.js.map +0 -1
- package/dist/esm/v2/client/util/url.js.map +0 -1
- package/dist/esm/v2/configurator.js +0 -42
- package/dist/esm/v2/configurator.js.map +0 -1
- package/dist/esm/v2/index.js +0 -3
- package/dist/esm/v2/index.js.map +0 -1
- package/dist/esm/v2/provider.js +0 -115
- package/dist/esm/v2/provider.js.map +0 -1
- package/dist/types/v2/client/behavior.d.ts +0 -13
- package/dist/types/v2/client/client.d.ts +0 -89
- package/dist/types/v2/client/create-auth-client.d.ts +0 -27
- package/dist/types/v2/client/index.d.ts +0 -5
- package/dist/types/v2/client/log/console.d.ts +0 -28
- package/dist/types/v2/client/request.d.ts +0 -65
- package/dist/types/v2/configurator.d.ts +0 -32
- package/dist/types/v2/index.d.ts +0 -2
- package/dist/types/v2/provider.d.ts +0 -59
- package/src/v2/client/behavior.ts +0 -14
- package/src/v2/client/client.ts +0 -180
- package/src/v2/client/create-auth-client.ts +0 -48
- package/src/v2/client/index.ts +0 -8
- package/src/v2/client/log/console.ts +0 -58
- package/src/v2/client/request.ts +0 -66
- package/src/v2/configurator.ts +0 -58
- package/src/v2/index.ts +0 -2
- package/src/v2/provider.ts +0 -178
- /package/dist/types/{v2/client/util/browser.d.ts → util/compare-origin.d.ts} +0 -0
- /package/dist/types/{v2/client/util/url.d.ts → util/normalize-uri.d.ts} +0 -0
- /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
|
+
}
|