@equinor/fusion-framework-module-msal 10.0.2 → 11.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 +163 -0
- package/README.md +17 -368
- package/dist/esm/MsalConfigurator.js +176 -88
- package/dist/esm/MsalConfigurator.js.map +1 -1
- package/dist/esm/__tests__/MsalConfigurator.test.js +75 -0
- package/dist/esm/__tests__/MsalConfigurator.test.js.map +1 -1
- package/dist/esm/__tests__/create-proxy-provider.test.js +53 -0
- package/dist/esm/__tests__/create-proxy-provider.test.js.map +1 -0
- package/dist/esm/__tests__/mock/msal-mock.test.js +399 -0
- package/dist/esm/__tests__/mock/msal-mock.test.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mock/MsalMockClient.js +467 -0
- package/dist/esm/mock/MsalMockClient.js.map +1 -0
- package/dist/esm/mock/MsalMockConfigurator.js +187 -0
- package/dist/esm/mock/MsalMockConfigurator.js.map +1 -0
- package/dist/esm/mock/create-mock-token.js +60 -0
- package/dist/esm/mock/create-mock-token.js.map +1 -0
- package/dist/esm/mock/create-msal-mock-client.js +20 -0
- package/dist/esm/mock/create-msal-mock-client.js.map +1 -0
- package/dist/esm/mock/decode-jwt-segment.js +22 -0
- package/dist/esm/mock/decode-jwt-segment.js.map +1 -0
- package/dist/esm/mock/index.js +30 -0
- package/dist/esm/mock/index.js.map +1 -0
- package/dist/esm/mock/module.js +38 -0
- package/dist/esm/mock/module.js.map +1 -0
- package/dist/esm/msal-config-schema.js +37 -0
- package/dist/esm/msal-config-schema.js.map +1 -0
- package/dist/esm/telemetry-config-schema.js +16 -0
- package/dist/esm/telemetry-config-schema.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 +0 -1
- package/dist/esm/versioning/resolve-version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/MsalConfigurator.d.ts +96 -20
- package/dist/types/__tests__/create-proxy-provider.test.d.ts +1 -0
- package/dist/types/__tests__/mock/msal-mock.test.d.ts +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/mock/MsalMockClient.d.ts +270 -0
- package/dist/types/mock/MsalMockConfigurator.d.ts +156 -0
- package/dist/types/mock/create-mock-token.d.ts +54 -0
- package/dist/types/mock/create-msal-mock-client.d.ts +14 -0
- package/dist/types/mock/decode-jwt-segment.d.ts +13 -0
- package/dist/types/mock/index.d.ts +29 -0
- package/dist/types/mock/module.d.ts +35 -0
- package/dist/types/msal-config-schema.d.ts +64 -0
- package/dist/types/telemetry-config-schema.d.ts +8 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/api-reference.md +85 -0
- package/docs/auth-code-flow.md +86 -0
- package/docs/migration-v2-to-v4.md +115 -0
- package/docs/testing.md +167 -0
- package/docs/troubleshooting.md +17 -0
- package/docs/version-management.md +67 -0
- package/package.json +12 -5
- package/src/MsalConfigurator.ts +202 -115
- package/src/__tests__/MsalConfigurator.test.ts +106 -0
- package/src/__tests__/create-proxy-provider.test.ts +77 -0
- package/src/__tests__/mock/msal-mock.test.ts +544 -0
- package/src/index.ts +6 -0
- package/src/mock/MsalMockClient.ts +599 -0
- package/src/mock/MsalMockConfigurator.ts +241 -0
- package/src/mock/create-mock-token.ts +92 -0
- package/src/mock/create-msal-mock-client.ts +25 -0
- package/src/mock/decode-jwt-segment.ts +22 -0
- package/src/mock/index.ts +29 -0
- package/src/mock/module.ts +54 -0
- package/src/msal-config-schema.ts +81 -0
- package/src/telemetry-config-schema.ts +25 -0
- package/src/version.ts +1 -1
- package/src/versioning/resolve-version.ts +0 -1
- package/vitest.config.ts +1 -1
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { BaseConfigBuilder, type ConfigBuilderCallbackArgs } from '@equinor/fusion-framework-module';
|
|
2
|
+
import { CacheLookupPolicy } from '@azure/msal-browser';
|
|
3
|
+
import type { ITelemetryProvider } from '@equinor/fusion-framework-module-telemetry';
|
|
3
4
|
import type { IMsalProvider } from './MsalProvider.interface';
|
|
4
|
-
import { type ITelemetryProvider } from '@equinor/fusion-framework-module-telemetry';
|
|
5
5
|
import { type MsalClientConfig, type IMsalClient } from './MsalClient';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* This configuration controls how authentication events are tracked and logged
|
|
11
|
-
* through the framework's telemetry system.
|
|
12
|
-
*/
|
|
13
|
-
export type TelemetryConfig = z.infer<typeof TelemetryConfigSchema>;
|
|
14
|
-
/**
|
|
15
|
-
* Complete configuration object for MSAL authentication module.
|
|
16
|
-
*
|
|
17
|
-
* This type represents the full configuration including client setup, authentication
|
|
18
|
-
* requirements, telemetry, and version information.
|
|
19
|
-
*/
|
|
20
|
-
export type MsalConfig = z.infer<typeof MsalConfigSchema>;
|
|
6
|
+
import { type MsalConfig } from './msal-config-schema';
|
|
7
|
+
export { MsalConfigSchema, type MsalConfig, type MsalConfigExtension, } from './msal-config-schema';
|
|
8
|
+
export { TelemetryConfigSchema, type TelemetryConfig } from './telemetry-config-schema';
|
|
21
9
|
/**
|
|
22
10
|
* Configuration builder for MSAL v4 authentication module.
|
|
23
11
|
*
|
|
@@ -67,6 +55,20 @@ export declare class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
67
55
|
* ```
|
|
68
56
|
*/
|
|
69
57
|
setClientConfig(config?: MsalClientConfig): this;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the client configuration declared through
|
|
60
|
+
* {@link MsalConfigurator.setClientConfig | setClientConfig}, if any.
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* This is the configuration as declared, not the resolved one a client is
|
|
64
|
+
* built from — see
|
|
65
|
+
* {@link MsalConfigurator._createClientConfig | _createClientConfig} for that.
|
|
66
|
+
* Reading it is how a subclass can tell "nothing was declared" apart from
|
|
67
|
+
* "declared, and here it is", without re-deriving that from a resolved value.
|
|
68
|
+
*
|
|
69
|
+
* @returns The declared client configuration, or `undefined` when none was declared.
|
|
70
|
+
*/
|
|
71
|
+
getClientConfig(): MsalClientConfig | undefined;
|
|
70
72
|
/**
|
|
71
73
|
* Sets the cache lookup policy used for every silent token acquisition.
|
|
72
74
|
*
|
|
@@ -184,6 +186,16 @@ export declare class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
184
186
|
* ```
|
|
185
187
|
*/
|
|
186
188
|
setClient(client: IMsalClient): this;
|
|
189
|
+
/**
|
|
190
|
+
* Returns the currently configured MSAL client, if one has been set.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* This is useful in tests when a mock client has been provided and the test
|
|
194
|
+
* wants to adjust its state after it has been assigned to the configurator.
|
|
195
|
+
*
|
|
196
|
+
* @returns The configured client, or `undefined` when none has been set.
|
|
197
|
+
*/
|
|
198
|
+
getClient(): IMsalClient | undefined;
|
|
187
199
|
/**
|
|
188
200
|
* Sets telemetry provider for MSAL authentication events.
|
|
189
201
|
*
|
|
@@ -222,8 +234,72 @@ export declare class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
222
234
|
/**
|
|
223
235
|
* Processes and validates the configuration.
|
|
224
236
|
*
|
|
225
|
-
* @param
|
|
237
|
+
* @param rawConfig - Raw configuration object
|
|
238
|
+
* @param init - The builder arguments, carrying the host reference when hoisted
|
|
226
239
|
* @returns Processed and validated configuration
|
|
227
240
|
*/
|
|
228
|
-
_processConfig(rawConfig: MsalConfig): Promise<MsalConfig>;
|
|
241
|
+
_processConfig(rawConfig: MsalConfig, init?: ConfigBuilderCallbackArgs): Promise<MsalConfig>;
|
|
242
|
+
/**
|
|
243
|
+
* Creates the client to authenticate through, when none was supplied.
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
246
|
+
* Called by {@link MsalConfigurator._processConfig | _processConfig} only when
|
|
247
|
+
* no client was set, so a client supplied through
|
|
248
|
+
* {@link MsalConfigurator.setClient | setClient} always wins. It is likewise
|
|
249
|
+
* not called when the module is hoisted onto a host application's provider —
|
|
250
|
+
* see {@link MsalConfigurator._isHoisted | _isHoisted}.
|
|
251
|
+
*
|
|
252
|
+
* This is the seam for authenticating through something other than Entra ID.
|
|
253
|
+
* Overriding it replaces only the client, leaving the builder, the schema
|
|
254
|
+
* validation and `MsalProvider` untouched — which is how
|
|
255
|
+
* `MsalMockConfigurator` substitutes an in-process client for tests.
|
|
256
|
+
*
|
|
257
|
+
* An override normally builds from
|
|
258
|
+
* {@link MsalConfigurator._createClientConfig | _createClientConfig}, so it
|
|
259
|
+
* receives the same fully-resolved {@link MsalClientConfig} the real client is
|
|
260
|
+
* built from rather than re-deriving it.
|
|
261
|
+
*
|
|
262
|
+
* Returning `undefined` is legitimate and means "there is nothing to build a
|
|
263
|
+
* client from", which leaves the module without one.
|
|
264
|
+
*
|
|
265
|
+
* @param config - The validated configuration the client is built from.
|
|
266
|
+
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
267
|
+
* @returns The client, or `undefined` when there is nothing to build one from.
|
|
268
|
+
*/
|
|
269
|
+
protected _createClient(config: MsalConfig, _init?: ConfigBuilderCallbackArgs): Promise<IMsalClient | undefined>;
|
|
270
|
+
/**
|
|
271
|
+
* Whether this module is hoisted onto a host application's authentication.
|
|
272
|
+
*
|
|
273
|
+
* @remarks
|
|
274
|
+
* When an application runs inside a host — a portal loading an app, or an app
|
|
275
|
+
* loading a widget — the module initializer returns a proxy of the host's
|
|
276
|
+
* provider instead of building its own (see the host-provider branch of the
|
|
277
|
+
* module initializer). A client built during configuration would therefore be
|
|
278
|
+
* constructed and immediately discarded.
|
|
279
|
+
*
|
|
280
|
+
* Detecting this during configuration lets the configurator skip building a
|
|
281
|
+
* client entirely, which matters most for substituted clients: a mock client
|
|
282
|
+
* built here would otherwise silently shadow the host's real signed-in user.
|
|
283
|
+
*
|
|
284
|
+
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
285
|
+
* @returns `true` when a host provider will be used instead of a locally built client.
|
|
286
|
+
*/
|
|
287
|
+
protected _isHoisted(init?: ConfigBuilderCallbackArgs): boolean;
|
|
288
|
+
/**
|
|
289
|
+
* Resolves the full MSAL client configuration to build a client from.
|
|
290
|
+
*
|
|
291
|
+
* @remarks
|
|
292
|
+
* Applies the defaults a client is expected to be built with — authority
|
|
293
|
+
* derived from the tenant, cache location, telemetry-backed logging and the
|
|
294
|
+
* configured cache lookup policy.
|
|
295
|
+
*
|
|
296
|
+
* Kept separate from {@link MsalConfigurator._createClient | _createClient} so
|
|
297
|
+
* that substituting the client does not also mean re-implementing this
|
|
298
|
+
* resolution. `MsalMockConfigurator` relies on it to hand its mock client the
|
|
299
|
+
* very same configuration the real client would have received.
|
|
300
|
+
*
|
|
301
|
+
* @param config - The validated configuration.
|
|
302
|
+
* @returns The client configuration, or `undefined` when none was declared.
|
|
303
|
+
*/
|
|
304
|
+
protected _createClientConfig(config: MsalConfig): MsalClientConfig | undefined;
|
|
229
305
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -28,5 +28,10 @@ export { module, configureMsal, enableMSAL, type MsalModule, type AuthConfigFn,
|
|
|
28
28
|
export type { IMsalProvider } from './MsalProvider.interface';
|
|
29
29
|
export type { IMsalClient } from './MsalClient.interface';
|
|
30
30
|
export { MsalClient, type MsalClientConfig } from './MsalClient';
|
|
31
|
+
/**
|
|
32
|
+
* Required to implement {@link IMsalProvider}, whose `msalVersion` member is
|
|
33
|
+
* typed as this enum.
|
|
34
|
+
*/
|
|
35
|
+
export { MsalModuleVersion } from './static';
|
|
31
36
|
export type { AccountInfo, AuthenticationResult } from './types';
|
|
32
37
|
export { default } from './module';
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { AccountInfo, AuthenticationResult, AuthorizationCodeRequest, PopupRequest, RedirectRequest, SilentRequest, SsoSilentRequest, EndSessionRequest, EndSessionPopupRequest, InitializeApplicationRequest, WrapperSKU, INavigationClient, BrowserConfiguration, Logger, PerformanceCallbackFunction, EventCallbackFunction, EventType, ClearCacheRequest } from '@azure/msal-browser';
|
|
2
|
+
import type { AcquireTokenOptions, AcquireTokenResult, IMsalClient, LoginOptions, LoginResult } from '../MsalClient.interface';
|
|
3
|
+
import type { MsalClientConfig } from '../MsalClient';
|
|
4
|
+
/**
|
|
5
|
+
* The user a mock MSAL client signs in.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Deliberately separate from {@link MsalClientConfig}: a client is configured
|
|
9
|
+
* with *what it talks to*, never with *who is signed in*. The real client learns
|
|
10
|
+
* the user from Entra ID, so the mock is told after it is constructed — see
|
|
11
|
+
* {@link MsalMockClient.setUser | setUser}.
|
|
12
|
+
*/
|
|
13
|
+
export interface MsalMockUser {
|
|
14
|
+
/** Display name of the signed-in user. Defaults to `Test User`. */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** UPN / email of the signed-in user. Defaults to `test.user@equinor.com`. */
|
|
17
|
+
username?: string;
|
|
18
|
+
/** Object ID of the signed-in user. Defaults to `fusion-mock-user`. */
|
|
19
|
+
userId?: string;
|
|
20
|
+
/** Tenant the user belongs to. Defaults to the client's configured tenant. */
|
|
21
|
+
tenantId?: string;
|
|
22
|
+
/** Scopes granted when a request does not specify its own. */
|
|
23
|
+
scopes?: string[];
|
|
24
|
+
/** Preconfigured account to use for signed-in state. */
|
|
25
|
+
account?: AccountInfo;
|
|
26
|
+
/**
|
|
27
|
+
* Start without a signed-in user, while keeping this identity.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Silent flows then resolve empty so the provider follows its unauthenticated
|
|
31
|
+
* path, while an explicit login still succeeds *as this user*. That lets a
|
|
32
|
+
* test drive the sign-in journey and assert on who it ends up as, rather than
|
|
33
|
+
* only on its end state.
|
|
34
|
+
*
|
|
35
|
+
* Pass `null` instead of a user when the identity does not matter.
|
|
36
|
+
*/
|
|
37
|
+
signedOut?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A stand-in for the MSAL client that resolves tokens in-process.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Constructed from {@link MsalClientConfig} — the very same argument
|
|
44
|
+
* {@link MsalClient} takes — so it is a drop-in substitute rather than a second
|
|
45
|
+
* API to learn. `setClientConfig` therefore means the same thing whether a test
|
|
46
|
+
* runs against Entra ID or against this client.
|
|
47
|
+
*
|
|
48
|
+
* Only the boundary that would contact Entra ID is replaced. The real
|
|
49
|
+
* `MsalProvider` runs on top of it unchanged, so account handling, silent-token
|
|
50
|
+
* preference, scope resolution, proxy providers and telemetry behave as they do in
|
|
51
|
+
* production — the test exercises the framework rather than the mock.
|
|
52
|
+
*
|
|
53
|
+
* Tokens are structurally valid, unsigned JWTs and are byte-identical between runs.
|
|
54
|
+
* They are **not** cryptographically valid and are rejected by any real service.
|
|
55
|
+
*/
|
|
56
|
+
export declare class MsalMockClient implements IMsalClient {
|
|
57
|
+
#private;
|
|
58
|
+
/**
|
|
59
|
+
* Mirrors MSAL's redirect completion, which is a no-op for this in-process mock.
|
|
60
|
+
* @returns Always `null`, because the mock performs no redirect.
|
|
61
|
+
*/
|
|
62
|
+
handleRedirectPromise(): Promise<AuthenticationResult | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Mirrors silent SSO and returns a token for the cached mock account.
|
|
65
|
+
* @param request - Silent SSO options and requested scopes.
|
|
66
|
+
* @returns A mock authentication result.
|
|
67
|
+
* @throws When no account is cached.
|
|
68
|
+
*/
|
|
69
|
+
ssoSilent(request: SsoSilentRequest): Promise<AuthenticationResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Mirrors popup login by signing in the configured mock user immediately.
|
|
72
|
+
* @param request - Optional popup options and requested scopes.
|
|
73
|
+
* @returns A mock authentication result.
|
|
74
|
+
*/
|
|
75
|
+
loginPopup(request?: PopupRequest): Promise<AuthenticationResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Mirrors redirect login without navigating, because the mock has no browser boundary.
|
|
78
|
+
* @param _request - Redirect options, accepted for interface compatibility.
|
|
79
|
+
*/
|
|
80
|
+
loginRedirect(_request?: RedirectRequest): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Mirrors the framework login entry point with an immediate mock sign-in.
|
|
83
|
+
* @param options - Login options including the requested scopes.
|
|
84
|
+
* @returns A mock login result.
|
|
85
|
+
*/
|
|
86
|
+
login(options: LoginOptions): Promise<LoginResult>;
|
|
87
|
+
/** Mirrors logout by removing the active mock account from the cache. */
|
|
88
|
+
logout(): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Mirrors MSAL initialization without performing a network handshake.
|
|
91
|
+
* @param _request - Initialization options, accepted for interface compatibility.
|
|
92
|
+
*/
|
|
93
|
+
initialize(_request?: InitializeApplicationRequest): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Mirrors popup token acquisition using the mock login flow.
|
|
96
|
+
* @param request - Popup token request.
|
|
97
|
+
* @returns A mock authentication result.
|
|
98
|
+
*/
|
|
99
|
+
acquireTokenPopup(request: PopupRequest): Promise<AuthenticationResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Mirrors redirect token acquisition without browser navigation.
|
|
102
|
+
* @param request - Redirect token request.
|
|
103
|
+
*/
|
|
104
|
+
acquireTokenRedirect(request: RedirectRequest): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Mirrors silent token acquisition for the cached mock account.
|
|
107
|
+
* @param request - Silent token request.
|
|
108
|
+
* @returns A mock authentication result.
|
|
109
|
+
* @throws When no account is cached.
|
|
110
|
+
*/
|
|
111
|
+
acquireTokenSilent(request: SilentRequest): Promise<AuthenticationResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Mirrors MSAL event registration; events are intentionally not emitted by the mock.
|
|
114
|
+
* @param _callback - Event handler, accepted for interface compatibility.
|
|
115
|
+
* @param _eventTypes - Event types, accepted for interface compatibility.
|
|
116
|
+
* @returns Always `null`, because the mock registers no callback.
|
|
117
|
+
*/
|
|
118
|
+
addEventCallback(_callback: EventCallbackFunction, _eventTypes?: EventType[]): string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Mirrors event removal as a no-op because this mock registers no callbacks.
|
|
121
|
+
* @param _callbackId - Callback identifier, accepted for interface compatibility.
|
|
122
|
+
*/
|
|
123
|
+
removeEventCallback(_callbackId: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* Mirrors performance callback registration with a stable mock identifier.
|
|
126
|
+
* @param _callback - Performance handler, accepted for interface compatibility.
|
|
127
|
+
* @returns A stable mock callback identifier.
|
|
128
|
+
*/
|
|
129
|
+
addPerformanceCallback(_callback: PerformanceCallbackFunction): string;
|
|
130
|
+
/**
|
|
131
|
+
* Mirrors performance callback removal and reports successful mock removal.
|
|
132
|
+
* @param _callbackId - Callback identifier, accepted for interface compatibility.
|
|
133
|
+
* @returns Always `true` because no callback state is retained.
|
|
134
|
+
*/
|
|
135
|
+
removePerformanceCallback(_callbackId: string): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Mirrors MSAL account lookup against the mock cache.
|
|
138
|
+
* @param accountFilter - Account fields to match.
|
|
139
|
+
* @returns The first matching account, or `null`.
|
|
140
|
+
*/
|
|
141
|
+
getAccount(accountFilter: unknown): AccountInfo | null;
|
|
142
|
+
/**
|
|
143
|
+
* Mirrors MSAL account enumeration using the mock cache.
|
|
144
|
+
* @param _accountFilter - Account filter, accepted for interface compatibility.
|
|
145
|
+
* @returns All accounts currently in the mock cache.
|
|
146
|
+
*/
|
|
147
|
+
getAllAccounts(_accountFilter?: unknown): AccountInfo[];
|
|
148
|
+
/**
|
|
149
|
+
* Mirrors redirect logout without navigating in the mock environment.
|
|
150
|
+
* @param _request - Logout options, accepted for interface compatibility.
|
|
151
|
+
*/
|
|
152
|
+
logoutRedirect(_request?: EndSessionRequest): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Mirrors popup logout without opening a browser window.
|
|
155
|
+
* @param _request - Logout options, accepted for interface compatibility.
|
|
156
|
+
*/
|
|
157
|
+
logoutPopup(_request?: EndSessionPopupRequest): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Mirrors MSAL logger access; the mock does not retain a logger.
|
|
160
|
+
* @returns An interface-compatible empty logger value.
|
|
161
|
+
*/
|
|
162
|
+
getLogger(): Logger;
|
|
163
|
+
/**
|
|
164
|
+
* Mirrors logger configuration as a no-op for the mock.
|
|
165
|
+
* @param _logger - Logger, accepted for interface compatibility.
|
|
166
|
+
*/
|
|
167
|
+
setLogger(_logger: unknown): void;
|
|
168
|
+
/**
|
|
169
|
+
* Mirrors wrapper metadata initialization as a no-op for the mock.
|
|
170
|
+
* @param _sku - Wrapper identifier, accepted for interface compatibility.
|
|
171
|
+
* @param _version - Wrapper version, accepted for interface compatibility.
|
|
172
|
+
*/
|
|
173
|
+
initializeWrapperLibrary(_sku: WrapperSKU, _version: string): void;
|
|
174
|
+
/**
|
|
175
|
+
* Mirrors navigation-client configuration as a no-op because no navigation occurs.
|
|
176
|
+
* @param _navigationClient - Navigation client, accepted for interface compatibility.
|
|
177
|
+
*/
|
|
178
|
+
setNavigationClient(_navigationClient: INavigationClient): void;
|
|
179
|
+
/**
|
|
180
|
+
* Mirrors configuration access and rejects it because the mock has no browser config.
|
|
181
|
+
* @returns Never; this mock does not expose browser configuration.
|
|
182
|
+
* @throws Always, because browser configuration is unsupported.
|
|
183
|
+
*/
|
|
184
|
+
getConfiguration(): BrowserConfiguration;
|
|
185
|
+
/**
|
|
186
|
+
* Mirrors cache hydration as a no-op because mock tokens are created in-process.
|
|
187
|
+
* @param _result - Authentication result, accepted for interface compatibility.
|
|
188
|
+
* @param _request - Original token request, accepted for interface compatibility.
|
|
189
|
+
*/
|
|
190
|
+
hydrateCache(_result: AuthenticationResult, _request: SilentRequest | SsoSilentRequest | RedirectRequest | PopupRequest): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Mirrors MSAL cache clearing by removing every mock account.
|
|
193
|
+
* @param _request - Cache-clear options, accepted for interface compatibility.
|
|
194
|
+
*/
|
|
195
|
+
clearCache(_request?: ClearCacheRequest): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Mirrors the generic token acquisition entry point for the active mock account.
|
|
198
|
+
* @param options - Token acquisition options.
|
|
199
|
+
* @returns A mock result, or `null` without an active account.
|
|
200
|
+
*/
|
|
201
|
+
acquireToken(options: AcquireTokenOptions): Promise<AcquireTokenResult>;
|
|
202
|
+
/**
|
|
203
|
+
* Mirrors authorization-code exchange by signing in and returning a mock result.
|
|
204
|
+
* @param request - Authorization-code request.
|
|
205
|
+
* @returns A mock authentication result.
|
|
206
|
+
*/
|
|
207
|
+
acquireTokenByCode(request: AuthorizationCodeRequest): Promise<AuthenticationResult>;
|
|
208
|
+
/**
|
|
209
|
+
* Creates a mock client for the services the given configuration points at.
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* Takes the same argument as {@link MsalClient}. A user named `Test User` is
|
|
213
|
+
* already in the account cache, so a provider built on this client boots the
|
|
214
|
+
* way one does for a returning user with a live session — no sign-in runs, and
|
|
215
|
+
* the provider's start-up path sees the state it would see in production. Use
|
|
216
|
+
* {@link MsalMockClient.setUser | setUser} to say who that user is.
|
|
217
|
+
*
|
|
218
|
+
* @param config - The same client configuration the real client is built from.
|
|
219
|
+
*/
|
|
220
|
+
constructor(config: MsalClientConfig);
|
|
221
|
+
/**
|
|
222
|
+
* Returns the client identifier used by tokens minted by this mock.
|
|
223
|
+
* @returns The configured client identifier.
|
|
224
|
+
*/
|
|
225
|
+
get clientId(): string | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* Returns the tenant identifier used by tokens minted by this mock.
|
|
228
|
+
* @returns The configured tenant identifier.
|
|
229
|
+
*/
|
|
230
|
+
get tenantId(): string | undefined;
|
|
231
|
+
/**
|
|
232
|
+
* Reports whether the mock currently has an active account.
|
|
233
|
+
* @returns Whether an account is active.
|
|
234
|
+
*/
|
|
235
|
+
get hasValidClaims(): boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Mirrors MSAL active-account access using the mock's single active account.
|
|
238
|
+
* @returns The active account, or `null`.
|
|
239
|
+
*/
|
|
240
|
+
getActiveAccount(): AccountInfo | null;
|
|
241
|
+
/**
|
|
242
|
+
* Makes an account the active one, adding it to the cache if it is unknown.
|
|
243
|
+
*
|
|
244
|
+
* @remarks
|
|
245
|
+
* Real MSAL requires the account to already be cached. Accepting an unknown
|
|
246
|
+
* one is a deliberate concession to tests: it is the shortest way to swap the
|
|
247
|
+
* signed-in user between runs, without reconstructing the framework.
|
|
248
|
+
*
|
|
249
|
+
* @param next - The account to make active, or `null` to sign out.
|
|
250
|
+
*/
|
|
251
|
+
setActiveAccount(next: AccountInfo | null): void;
|
|
252
|
+
/**
|
|
253
|
+
* Declares who is signed in, replacing whoever was.
|
|
254
|
+
*
|
|
255
|
+
* @remarks
|
|
256
|
+
* This is the counterpart to a real sign-in: the client is configured with
|
|
257
|
+
* what it talks to, and learns the user separately. `MsalMockConfigurator`
|
|
258
|
+
* applies it as the configuration is assembled, so the account is in the cache
|
|
259
|
+
* before `MsalProvider.initialize` runs — the provider then behaves as it does
|
|
260
|
+
* for a returning user with a live session.
|
|
261
|
+
*
|
|
262
|
+
* Values left out keep whatever they were. Passing `null` signs out and
|
|
263
|
+
* forgets the identity, so the provider follows its unauthenticated path;
|
|
264
|
+
* `{ signedOut: true }` does the same but keeps the identity, so a later login
|
|
265
|
+
* resolves as that user.
|
|
266
|
+
*
|
|
267
|
+
* @param user - The user to sign in, or `null` when nobody is.
|
|
268
|
+
*/
|
|
269
|
+
setUser(user: MsalMockUser | null): void;
|
|
270
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ConfigBuilderCallback, ConfigBuilderCallbackArgs } from '@equinor/fusion-framework-module';
|
|
2
|
+
import type { IMsalClient } from '../MsalClient.interface';
|
|
3
|
+
import { MsalConfigurator, type MsalConfig } from '../MsalConfigurator';
|
|
4
|
+
import { type MsalMockUser } from './MsalMockClient';
|
|
5
|
+
/**
|
|
6
|
+
* Declares the mock's own branch of the MSAL configuration.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Merging into `MsalConfigExtension` is what lets `setAccount` record the
|
|
10
|
+
* user through the ordinary builder — `_set` derives its target from
|
|
11
|
+
* {@link MsalConfig}, so a key the type does not know about could only be set by
|
|
12
|
+
* casting past it.
|
|
13
|
+
*
|
|
14
|
+
* The schema strips `mock` when it validates, so a declaration made here travels
|
|
15
|
+
* the builder and stops there: it is readable from the raw configuration and
|
|
16
|
+
* absent from the one `MsalProvider` receives.
|
|
17
|
+
*/
|
|
18
|
+
declare module '../msal-config-schema' {
|
|
19
|
+
interface MsalConfigExtension {
|
|
20
|
+
mock?: {
|
|
21
|
+
/**
|
|
22
|
+
* The user to sign in, resolved if it was declared as a callback, or
|
|
23
|
+
* `null` when nobody is signed in.
|
|
24
|
+
*/
|
|
25
|
+
account?: MsalMockUser | null;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The real MSAL configurator, backed by an in-process client.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Nothing else changes: the same builder API, the same validation and the same
|
|
34
|
+
* `MsalProvider` are used. Only the boundary that would contact Entra ID is
|
|
35
|
+
* substituted, through the same
|
|
36
|
+
* {@link MsalConfigurator._createClient | _createClient} seam the real
|
|
37
|
+
* configurator builds its own client from — and from the same
|
|
38
|
+
* {@link MsalConfigurator._createClientConfig | _createClientConfig}, so
|
|
39
|
+
* `setClientConfig` means exactly what it means in production.
|
|
40
|
+
*
|
|
41
|
+
* A user named `Test User` is signed in by default, so an application boots
|
|
42
|
+
* without declaring anything.
|
|
43
|
+
*
|
|
44
|
+
* @example Name the signed-in user
|
|
45
|
+
* ```typescript
|
|
46
|
+
* enableMsalMock(configurator, (builder) => {
|
|
47
|
+
* builder.setAccount({ name: 'Ada Lovelace', username: 'ada@equinor.com' });
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Configure the client exactly as in production
|
|
52
|
+
* ```typescript
|
|
53
|
+
* enableMsalMock(configurator, (builder) => {
|
|
54
|
+
* builder.setClientConfig({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Take full control of authentication
|
|
59
|
+
* ```typescript
|
|
60
|
+
* enableMsalMock(configurator, (builder) => {
|
|
61
|
+
* builder.setClient(new MyOwnMsalClient());
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare class MsalMockConfigurator extends MsalConfigurator {
|
|
66
|
+
#private;
|
|
67
|
+
/**
|
|
68
|
+
* Declares the user to sign in.
|
|
69
|
+
*
|
|
70
|
+
* @remarks
|
|
71
|
+
* Who is signed in is session state, not client configuration — which is what
|
|
72
|
+
* lets {@link MsalMockClient} take the same argument the real client takes: a
|
|
73
|
+
* client is configured with *what it talks to*, never with *who is signed in*.
|
|
74
|
+
*
|
|
75
|
+
* The user is therefore recorded on the configuration as `mock.account`, not
|
|
76
|
+
* on this builder, and is signed in on whichever client the module ends up
|
|
77
|
+
* authenticating through — wherever that client was built:
|
|
78
|
+
*
|
|
79
|
+
* - The client this builder builds, normally. The user is in place before
|
|
80
|
+
* `MsalProvider.initialize` runs, which is what makes the provider's own
|
|
81
|
+
* start-up path observable: with `signedOut` and `setRequiresAuth(true)`, a
|
|
82
|
+
* test sees the real automatic login run.
|
|
83
|
+
* - The **host's** client when the module is hoisted onto a host
|
|
84
|
+
* application's provider, because none is built here. An application inside
|
|
85
|
+
* a portal shares the portal's session, so this changes who the host sees
|
|
86
|
+
* signed in too, as it would in production.
|
|
87
|
+
* - A client supplied through {@link MsalConfigurator.setClient | setClient},
|
|
88
|
+
* when that client is a {@link MsalMockClient}.
|
|
89
|
+
*
|
|
90
|
+
* Throws when that client cannot represent a declared user, rather than
|
|
91
|
+
* failing quietly — a silent no-op is the whole failure mode this exists to
|
|
92
|
+
* prevent.
|
|
93
|
+
*
|
|
94
|
+
* Pass `null` when nobody is signed in, or `{ signedOut: true }` to keep an
|
|
95
|
+
* identity without a session — a later login then resolves as that user.
|
|
96
|
+
*
|
|
97
|
+
* @param account - The user, or an ordinary config-builder callback resolving it.
|
|
98
|
+
* @returns The builder, for chaining.
|
|
99
|
+
*
|
|
100
|
+
* @example Derive the user from the modules in scope
|
|
101
|
+
* ```typescript
|
|
102
|
+
* builder.setAccount(async ({ hasModule }) => ({
|
|
103
|
+
* name: hasModule('app') ? 'App User' : 'Portal User',
|
|
104
|
+
* }));
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
setAccount(account: MsalMockUser | null | ConfigBuilderCallback<MsalMockUser | null>): this;
|
|
108
|
+
/**
|
|
109
|
+
* Assembles the configuration, then signs the declared user in.
|
|
110
|
+
*
|
|
111
|
+
* @remarks
|
|
112
|
+
* Stands a client configuration in first when this builder is the one that
|
|
113
|
+
* will build a client: `MsalClientConfig.auth.clientId` is required to build
|
|
114
|
+
* any client at all and a test has no real credentials to declare. It then
|
|
115
|
+
* flows through the very same
|
|
116
|
+
* {@link MsalConfigurator._createClientConfig | _createClientConfig}
|
|
117
|
+
* enrichment the real client is built from, and anything declared through
|
|
118
|
+
* {@link MsalConfigurator.setClientConfig | setClientConfig} wins — exactly as
|
|
119
|
+
* in production.
|
|
120
|
+
*
|
|
121
|
+
* Doing that here rather than in the constructor is deliberate: a hoisted
|
|
122
|
+
* module authenticates through the host and builds no client, so it must not
|
|
123
|
+
* look configured either.
|
|
124
|
+
*
|
|
125
|
+
* The user is read from `rawConfig`, because the schema strips `mock` when it
|
|
126
|
+
* validates — the key exists to carry a test's declaration through the
|
|
127
|
+
* builder, never to reach the provider.
|
|
128
|
+
*
|
|
129
|
+
* @param rawConfig - The raw configuration to process.
|
|
130
|
+
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
131
|
+
* @returns The processed and validated configuration.
|
|
132
|
+
*/
|
|
133
|
+
_processConfig(rawConfig: MsalConfig, init?: ConfigBuilderCallbackArgs): Promise<MsalConfig>;
|
|
134
|
+
/**
|
|
135
|
+
* Builds an in-process client.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* Called only when no client was set, so
|
|
139
|
+
* {@link MsalConfigurator.setClient | setClient} still replaces authentication
|
|
140
|
+
* outright.
|
|
141
|
+
*
|
|
142
|
+
* Deliberately does not delegate to `super`, which would build a real
|
|
143
|
+
* `MsalClient` and contact Entra ID. It is never reached when the module is
|
|
144
|
+
* hoisted onto a host application's provider, because the base configurator
|
|
145
|
+
* gates client creation on {@link MsalConfigurator._isHoisted | _isHoisted} —
|
|
146
|
+
* a mock client built there would shadow the host's client, the exact scenario
|
|
147
|
+
* an application-inside-a-portal test exists to cover.
|
|
148
|
+
*
|
|
149
|
+
* Knows nothing about who is signed in: a client is built from what it talks
|
|
150
|
+
* to, and the declared user is applied to it afterwards.
|
|
151
|
+
*
|
|
152
|
+
* @param config - The validated configuration the client is built from.
|
|
153
|
+
* @returns A client resolving tokens in-process.
|
|
154
|
+
*/
|
|
155
|
+
protected _createClient(config: MsalConfig): Promise<IMsalClient>;
|
|
156
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims that can be set on a generated mock token.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Mirrors the subset of Entra ID claims that Fusion applications read. Any
|
|
6
|
+
* additional claims are passed through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export interface MockTokenClaims {
|
|
9
|
+
/** Object ID of the signed-in user. */
|
|
10
|
+
oid?: string;
|
|
11
|
+
/** Display name of the signed-in user. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** UPN / email of the signed-in user. */
|
|
14
|
+
preferred_username?: string;
|
|
15
|
+
/** Tenant the token was issued for. */
|
|
16
|
+
tid?: string;
|
|
17
|
+
/** Audience — normally the client or resource the token is intended for. */
|
|
18
|
+
aud?: string;
|
|
19
|
+
/** Issuer. */
|
|
20
|
+
iss?: string;
|
|
21
|
+
/** Scopes granted, space-separated as in a real Entra ID token. */
|
|
22
|
+
scp?: string;
|
|
23
|
+
/** Issued-at, seconds since epoch. */
|
|
24
|
+
iat?: number;
|
|
25
|
+
/** Not-before, seconds since epoch. */
|
|
26
|
+
nbf?: number;
|
|
27
|
+
/** Expiry, seconds since epoch. */
|
|
28
|
+
exp?: number;
|
|
29
|
+
[claim: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates a structurally valid, unsigned JWT for use in tests.
|
|
33
|
+
*
|
|
34
|
+
* The token has three base64url segments and decodes to the supplied claims, so
|
|
35
|
+
* code that splits, decodes, or inspects token claims behaves exactly as it does
|
|
36
|
+
* in production. The signature segment is a fixed placeholder — the token is
|
|
37
|
+
* **not** cryptographically valid and will be rejected by any real service.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Timestamps default to a fixed issue time and a one-hour lifetime so repeated
|
|
41
|
+
* runs produce byte-identical tokens. A test that needs an expired token can set
|
|
42
|
+
* `exp` in the past.
|
|
43
|
+
*
|
|
44
|
+
* @param claims - Claims to embed in the token payload.
|
|
45
|
+
* @returns An unsigned JWT string in `header.payload.signature` form.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const token = createMockToken({ name: 'Test User', scp: 'Files.Read' });
|
|
50
|
+
* const [, payload] = token.split('.');
|
|
51
|
+
* JSON.parse(decodeJwtSegment(payload)).name; // 'Test User'
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare const createMockToken: (claims?: MockTokenClaims) => string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IMsalClient } from '../MsalClient.interface';
|
|
2
|
+
import type { MsalClientConfig } from '../MsalClient';
|
|
3
|
+
import { type MsalMockUser } from './MsalMockClient';
|
|
4
|
+
/**
|
|
5
|
+
* Convenience helper that creates a mock client instance.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The class form is preferred for a more familiar configuration pattern.
|
|
9
|
+
*
|
|
10
|
+
* @param config - The same client configuration the real client is built from.
|
|
11
|
+
* @param user - Optional user to sign in, applied after construction.
|
|
12
|
+
* @returns A client that resolves tokens in-process.
|
|
13
|
+
*/
|
|
14
|
+
export declare const createMsalMockClient: (config: MsalClientConfig, user?: MsalMockUser) => IMsalClient;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes a base64url segment of a {@link createMockToken} JWT back to its JSON string.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Plain `atob` alone mangles non-ASCII claims: it treats its output as latin1,
|
|
6
|
+
* while segments are UTF-8 encoded. This reverses that encoding and also
|
|
7
|
+
* restores the standard base64 alphabet/padding `atob` expects.
|
|
8
|
+
*
|
|
9
|
+
* @param segment - A base64url segment, e.g. from splitting a JWT on `.`.
|
|
10
|
+
* @returns The decoded UTF-8 string.
|
|
11
|
+
*/
|
|
12
|
+
export declare function decodeJwtSegment(segment: string): string;
|
|
13
|
+
export default decodeJwtSegment;
|