@equinor/fusion-framework-module-msal 10.0.1 → 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 +173 -0
- package/README.md +17 -368
- package/dist/esm/MsalClient.js +9 -0
- package/dist/esm/MsalClient.js.map +1 -1
- package/dist/esm/MsalConfigurator.js +178 -88
- package/dist/esm/MsalConfigurator.js.map +1 -1
- package/dist/esm/MsalProvider.js +20 -1
- package/dist/esm/MsalProvider.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/create-client-log-callback.js +1 -0
- package/dist/esm/create-client-log-callback.js.map +1 -1
- package/dist/esm/create-proxy-provider.js +5 -0
- package/dist/esm/create-proxy-provider.js.map +1 -1
- 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/module.js +3 -2
- package/dist/esm/module.js.map +1 -1
- 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/util/normalize-uri.js.map +1 -1
- package/dist/esm/v2/Logger.js +173 -0
- package/dist/esm/v2/Logger.js.map +1 -0
- package/dist/esm/v2/create-proxy-client.js +6 -0
- package/dist/esm/v2/create-proxy-client.js.map +1 -1
- package/dist/esm/v2/create-proxy-provider.js +4 -0
- package/dist/esm/v2/create-proxy-provider.js.map +1 -1
- package/dist/esm/v2/types.js +3 -124
- package/dist/esm/v2/types.js.map +1 -1
- package/dist/esm/v4/create-proxy-provider.js +1 -0
- package/dist/esm/v4/create-proxy-provider.js.map +1 -1
- package/dist/esm/v4/types.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/esm/versioning/VersionError.js.map +1 -1
- package/dist/esm/versioning/resolve-version.js +4 -1
- package/dist/esm/versioning/resolve-version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/MsalClient.d.ts +4 -0
- package/dist/types/MsalConfigurator.d.ts +97 -20
- package/dist/types/MsalProvider.d.ts +13 -0
- 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/create-proxy-provider.d.ts +3 -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/static.d.ts +1 -1
- package/dist/types/telemetry-config-schema.d.ts +8 -0
- package/dist/types/v2/IAuthClient.interface.d.ts +2 -2
- package/dist/types/v2/Logger.d.ts +92 -0
- package/dist/types/v2/MsalProvider.interface.d.ts +1 -1
- package/dist/types/v2/types.d.ts +3 -49
- package/dist/types/v4/types.d.ts +1 -3
- 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 +14 -7
- package/src/MsalClient.ts +9 -0
- package/src/MsalConfigurator.ts +204 -115
- package/src/MsalProvider.ts +20 -1
- 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/create-client-log-callback.ts +1 -0
- package/src/create-proxy-provider.ts +5 -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/module.ts +3 -2
- package/src/msal-config-schema.ts +81 -0
- package/src/telemetry-config-schema.ts +25 -0
- package/src/v2/IAuthClient.interface.ts +2 -2
- package/src/v2/Logger.ts +204 -0
- package/src/v2/MsalProvider.interface.ts +1 -1
- package/src/v2/create-proxy-client.ts +6 -0
- package/src/v2/create-proxy-provider.ts +4 -0
- package/src/v2/types.ts +8 -158
- package/src/v4/create-proxy-provider.ts +1 -0
- package/src/v4/types.ts +3 -3
- package/src/version.ts +1 -1
- package/src/versioning/resolve-version.ts +4 -1
- package/vitest.config.ts +1 -1
package/src/MsalConfigurator.ts
CHANGED
|
@@ -1,68 +1,23 @@
|
|
|
1
|
-
import z from 'zod';
|
|
2
|
-
import { BaseConfigBuilder } from '@equinor/fusion-framework-module';
|
|
3
|
-
import semver from 'semver';
|
|
4
|
-
import type { IMsalProvider } from './MsalProvider.interface';
|
|
5
1
|
import {
|
|
6
|
-
|
|
7
|
-
type
|
|
8
|
-
} from '@equinor/fusion-framework-module
|
|
2
|
+
BaseConfigBuilder,
|
|
3
|
+
type ConfigBuilderCallbackArgs,
|
|
4
|
+
} from '@equinor/fusion-framework-module';
|
|
5
|
+
import { TelemetryLevel } from '@equinor/fusion-framework-module-telemetry';
|
|
6
|
+
import { CacheLookupPolicy, LogLevel } from '@azure/msal-browser';
|
|
7
|
+
|
|
8
|
+
import type { ITelemetryProvider } from '@equinor/fusion-framework-module-telemetry';
|
|
9
|
+
import type { IMsalProvider } from './MsalProvider.interface';
|
|
9
10
|
import { MsalClient, type MsalClientConfig, type IMsalClient } from './MsalClient';
|
|
10
11
|
import { createClientLogCallback } from './create-client-log-callback';
|
|
11
|
-
import { CacheLookupPolicy, LogLevel } from '@azure/msal-browser';
|
|
12
12
|
import { version } from './version';
|
|
13
|
+
import { MsalConfigSchema, type MsalConfig } from './msal-config-schema';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
provider: z.custom<ITelemetryProvider>().optional(),
|
|
21
|
-
metadata: z.record(z.string(), z.unknown()).optional().default({
|
|
22
|
-
module: 'msal',
|
|
23
|
-
version,
|
|
24
|
-
}),
|
|
25
|
-
scope: z.array(z.string()).optional().default(['framework', 'authentication']),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Telemetry configuration for MSAL module.
|
|
30
|
-
*
|
|
31
|
-
* This configuration controls how authentication events are tracked and logged
|
|
32
|
-
* through the framework's telemetry system.
|
|
33
|
-
*/
|
|
34
|
-
export type TelemetryConfig = z.infer<typeof TelemetryConfigSchema>;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Zod schema for MSAL module configuration validation.
|
|
38
|
-
*
|
|
39
|
-
* @internal
|
|
40
|
-
*/
|
|
41
|
-
const MsalConfigSchema = z.object({
|
|
42
|
-
client: z.custom<IMsalClient>().optional(),
|
|
43
|
-
provider: z.custom<IMsalProvider>().optional(),
|
|
44
|
-
requiresAuth: z.boolean().optional(),
|
|
45
|
-
redirectUri: z.string().optional(),
|
|
46
|
-
loginHint: z.string().optional(),
|
|
47
|
-
authCode: z.string().optional(),
|
|
48
|
-
cacheLookupPolicy: z
|
|
49
|
-
.custom<CacheLookupPolicy>(
|
|
50
|
-
(val) =>
|
|
51
|
-
typeof val === 'number' &&
|
|
52
|
-
Object.values(CacheLookupPolicy).includes(val as CacheLookupPolicy),
|
|
53
|
-
)
|
|
54
|
-
.optional(),
|
|
55
|
-
version: z.string().transform((x: string) => String(semver.coerce(x))),
|
|
56
|
-
telemetry: TelemetryConfigSchema,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Complete configuration object for MSAL authentication module.
|
|
61
|
-
*
|
|
62
|
-
* This type represents the full configuration including client setup, authentication
|
|
63
|
-
* requirements, telemetry, and version information.
|
|
64
|
-
*/
|
|
65
|
-
export type MsalConfig = z.infer<typeof MsalConfigSchema>;
|
|
15
|
+
export {
|
|
16
|
+
MsalConfigSchema,
|
|
17
|
+
type MsalConfig,
|
|
18
|
+
type MsalConfigExtension,
|
|
19
|
+
} from './msal-config-schema';
|
|
20
|
+
export { TelemetryConfigSchema, type TelemetryConfig } from './telemetry-config-schema';
|
|
66
21
|
|
|
67
22
|
/**
|
|
68
23
|
* Configuration builder for MSAL v4 authentication module.
|
|
@@ -79,11 +34,13 @@ export type MsalConfig = z.infer<typeof MsalConfigSchema>;
|
|
|
79
34
|
*/
|
|
80
35
|
export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
81
36
|
#msalConfig?: MsalClientConfig;
|
|
37
|
+
#client?: IMsalClient;
|
|
82
38
|
|
|
83
39
|
/**
|
|
84
40
|
* The MSAL module version being configured.
|
|
85
41
|
*
|
|
86
42
|
* @default Latest
|
|
43
|
+
* @returns The configured MSAL module version.
|
|
87
44
|
*/
|
|
88
45
|
public get version(): string {
|
|
89
46
|
return version;
|
|
@@ -100,11 +57,15 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
100
57
|
this._set('version', async () => this.version);
|
|
101
58
|
// Auto-detect and integrate telemetry module if available
|
|
102
59
|
this._set('telemetry.provider', async (args) => {
|
|
60
|
+
// Only resolve the telemetry instance when the telemetry module is registered
|
|
103
61
|
if (args.hasModule('telemetry')) {
|
|
104
62
|
const telemetry = await args.requireInstance('telemetry');
|
|
105
63
|
return telemetry;
|
|
106
64
|
}
|
|
107
65
|
});
|
|
66
|
+
// Always resolve the configured client instance through the builder.
|
|
67
|
+
// This keeps the client getter live and avoids re-registering the same config key.
|
|
68
|
+
this._set('client', async () => this.#client);
|
|
108
69
|
// Default cache lookup policy to AccessTokenAndRefreshToken to avoid iframe fallback delays
|
|
109
70
|
this._set('cacheLookupPolicy', async () => CacheLookupPolicy.AccessTokenAndRefreshToken);
|
|
110
71
|
}
|
|
@@ -134,6 +95,23 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
134
95
|
return this;
|
|
135
96
|
}
|
|
136
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Returns the client configuration declared through
|
|
100
|
+
* {@link MsalConfigurator.setClientConfig | setClientConfig}, if any.
|
|
101
|
+
*
|
|
102
|
+
* @remarks
|
|
103
|
+
* This is the configuration as declared, not the resolved one a client is
|
|
104
|
+
* built from — see
|
|
105
|
+
* {@link MsalConfigurator._createClientConfig | _createClientConfig} for that.
|
|
106
|
+
* Reading it is how a subclass can tell "nothing was declared" apart from
|
|
107
|
+
* "declared, and here it is", without re-deriving that from a resolved value.
|
|
108
|
+
*
|
|
109
|
+
* @returns The declared client configuration, or `undefined` when none was declared.
|
|
110
|
+
*/
|
|
111
|
+
public getClientConfig(): MsalClientConfig | undefined {
|
|
112
|
+
return this.#msalConfig;
|
|
113
|
+
}
|
|
114
|
+
|
|
137
115
|
/**
|
|
138
116
|
* Sets the cache lookup policy used for every silent token acquisition.
|
|
139
117
|
*
|
|
@@ -272,10 +250,23 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
272
250
|
* ```
|
|
273
251
|
*/
|
|
274
252
|
setClient(client: IMsalClient): this {
|
|
275
|
-
this
|
|
253
|
+
this.#client = client;
|
|
276
254
|
return this;
|
|
277
255
|
}
|
|
278
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Returns the currently configured MSAL client, if one has been set.
|
|
259
|
+
*
|
|
260
|
+
* @remarks
|
|
261
|
+
* This is useful in tests when a mock client has been provided and the test
|
|
262
|
+
* wants to adjust its state after it has been assigned to the configurator.
|
|
263
|
+
*
|
|
264
|
+
* @returns The configured client, or `undefined` when none has been set.
|
|
265
|
+
*/
|
|
266
|
+
public getClient(): IMsalClient | undefined {
|
|
267
|
+
return this.#client;
|
|
268
|
+
}
|
|
269
|
+
|
|
279
270
|
/**
|
|
280
271
|
* Sets telemetry provider for MSAL authentication events.
|
|
281
272
|
*
|
|
@@ -326,72 +317,170 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
326
317
|
/**
|
|
327
318
|
* Processes and validates the configuration.
|
|
328
319
|
*
|
|
329
|
-
* @param
|
|
320
|
+
* @param rawConfig - Raw configuration object
|
|
321
|
+
* @param init - The builder arguments, carrying the host reference when hoisted
|
|
330
322
|
* @returns Processed and validated configuration
|
|
331
323
|
*/
|
|
332
|
-
async _processConfig(
|
|
324
|
+
async _processConfig(
|
|
325
|
+
rawConfig: MsalConfig,
|
|
326
|
+
init?: ConfigBuilderCallbackArgs,
|
|
327
|
+
): Promise<MsalConfig> {
|
|
333
328
|
// Validate and coerce configuration using Zod schema
|
|
334
329
|
const config = await MsalConfigSchema.parseAsync(rawConfig);
|
|
335
330
|
|
|
336
|
-
// Auto-create client if
|
|
331
|
+
// Auto-create client if no client instance was supplied
|
|
337
332
|
// This allows users to provide configuration without manually instantiating the client
|
|
338
|
-
|
|
339
|
-
|
|
333
|
+
// A hoisted module authenticates through the host's provider, so any client built here
|
|
334
|
+
// would be discarded — gate it here rather than in `_createClient`, so a substituted
|
|
335
|
+
// client (see `MsalMockConfigurator`) cannot shadow the host's signed-in user
|
|
336
|
+
if (!config.client && !this._isHoisted(init)) {
|
|
337
|
+
config.client = await this._createClient(config, init);
|
|
338
|
+
}
|
|
340
339
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
level: TelemetryLevel.Debug,
|
|
344
|
-
scope: config.telemetry.scope,
|
|
345
|
-
metadata: { ...config.telemetry.metadata, clientConfig },
|
|
346
|
-
});
|
|
340
|
+
return config;
|
|
341
|
+
}
|
|
347
342
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Creates the client to authenticate through, when none was supplied.
|
|
345
|
+
*
|
|
346
|
+
* @remarks
|
|
347
|
+
* Called by {@link MsalConfigurator._processConfig | _processConfig} only when
|
|
348
|
+
* no client was set, so a client supplied through
|
|
349
|
+
* {@link MsalConfigurator.setClient | setClient} always wins. It is likewise
|
|
350
|
+
* not called when the module is hoisted onto a host application's provider —
|
|
351
|
+
* see {@link MsalConfigurator._isHoisted | _isHoisted}.
|
|
352
|
+
*
|
|
353
|
+
* This is the seam for authenticating through something other than Entra ID.
|
|
354
|
+
* Overriding it replaces only the client, leaving the builder, the schema
|
|
355
|
+
* validation and `MsalProvider` untouched — which is how
|
|
356
|
+
* `MsalMockConfigurator` substitutes an in-process client for tests.
|
|
357
|
+
*
|
|
358
|
+
* An override normally builds from
|
|
359
|
+
* {@link MsalConfigurator._createClientConfig | _createClientConfig}, so it
|
|
360
|
+
* receives the same fully-resolved {@link MsalClientConfig} the real client is
|
|
361
|
+
* built from rather than re-deriving it.
|
|
362
|
+
*
|
|
363
|
+
* Returning `undefined` is legitimate and means "there is nothing to build a
|
|
364
|
+
* client from", which leaves the module without one.
|
|
365
|
+
*
|
|
366
|
+
* @param config - The validated configuration the client is built from.
|
|
367
|
+
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
368
|
+
* @returns The client, or `undefined` when there is nothing to build one from.
|
|
369
|
+
*/
|
|
370
|
+
protected async _createClient(
|
|
371
|
+
config: MsalConfig,
|
|
372
|
+
_init?: ConfigBuilderCallbackArgs,
|
|
373
|
+
): Promise<IMsalClient | undefined> {
|
|
374
|
+
const clientConfig = this._createClientConfig(config);
|
|
375
|
+
// A client can be omitted for a hoisted module or an intentionally incomplete setup.
|
|
376
|
+
if (!clientConfig) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
353
379
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
clientConfig.cache = { cacheLocation: 'localStorage' };
|
|
358
|
-
}
|
|
380
|
+
// Instantiate MSAL client with fully configured options
|
|
381
|
+
return new MsalClient(clientConfig);
|
|
382
|
+
}
|
|
359
383
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
logLevel: LogLevel.Warning,
|
|
381
|
-
// Preserve any user-provided logger options (allows customization)
|
|
382
|
-
...clientConfig.system?.loggerOptions,
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
// Apply silent cache lookup policy if configured
|
|
387
|
-
if (config.cacheLookupPolicy !== undefined) {
|
|
388
|
-
clientConfig.cacheLookupPolicy = config.cacheLookupPolicy;
|
|
389
|
-
}
|
|
384
|
+
/**
|
|
385
|
+
* Whether this module is hoisted onto a host application's authentication.
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* When an application runs inside a host — a portal loading an app, or an app
|
|
389
|
+
* loading a widget — the module initializer returns a proxy of the host's
|
|
390
|
+
* provider instead of building its own (see the host-provider branch of the
|
|
391
|
+
* module initializer). A client built during configuration would therefore be
|
|
392
|
+
* constructed and immediately discarded.
|
|
393
|
+
*
|
|
394
|
+
* Detecting this during configuration lets the configurator skip building a
|
|
395
|
+
* client entirely, which matters most for substituted clients: a mock client
|
|
396
|
+
* built here would otherwise silently shadow the host's real signed-in user.
|
|
397
|
+
*
|
|
398
|
+
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
399
|
+
* @returns `true` when a host provider will be used instead of a locally built client.
|
|
400
|
+
*/
|
|
401
|
+
protected _isHoisted(init?: ConfigBuilderCallbackArgs): boolean {
|
|
402
|
+
return !!(init?.ref as { auth?: IMsalProvider } | undefined)?.auth;
|
|
403
|
+
}
|
|
390
404
|
|
|
391
|
-
|
|
392
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Resolves the full MSAL client configuration to build a client from.
|
|
407
|
+
*
|
|
408
|
+
* @remarks
|
|
409
|
+
* Applies the defaults a client is expected to be built with — authority
|
|
410
|
+
* derived from the tenant, cache location, telemetry-backed logging and the
|
|
411
|
+
* configured cache lookup policy.
|
|
412
|
+
*
|
|
413
|
+
* Kept separate from {@link MsalConfigurator._createClient | _createClient} so
|
|
414
|
+
* that substituting the client does not also mean re-implementing this
|
|
415
|
+
* resolution. `MsalMockConfigurator` relies on it to hand its mock client the
|
|
416
|
+
* very same configuration the real client would have received.
|
|
417
|
+
*
|
|
418
|
+
* @param config - The validated configuration.
|
|
419
|
+
* @returns The client configuration, or `undefined` when none was declared.
|
|
420
|
+
*/
|
|
421
|
+
protected _createClientConfig(config: MsalConfig): MsalClientConfig | undefined {
|
|
422
|
+
const declared = this.#msalConfig;
|
|
423
|
+
// Do not construct a client when configuration has not supplied client settings.
|
|
424
|
+
if (!declared) {
|
|
425
|
+
return undefined;
|
|
393
426
|
}
|
|
394
427
|
|
|
395
|
-
|
|
428
|
+
config.telemetry.provider?.trackEvent({
|
|
429
|
+
name: 'module-msal.configurator._processConfig.creating-client',
|
|
430
|
+
level: TelemetryLevel.Debug,
|
|
431
|
+
scope: config.telemetry.scope,
|
|
432
|
+
metadata: { ...config.telemetry.metadata, clientConfig: declared },
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Copied rather than enriched in place, so the object a caller passed to
|
|
436
|
+
// `setClientConfig` is never rewritten behind its back — a caller may well
|
|
437
|
+
// be reusing or asserting on it
|
|
438
|
+
const clientConfig: MsalClientConfig = {
|
|
439
|
+
...declared,
|
|
440
|
+
auth: { ...declared.auth },
|
|
441
|
+
// Default to localStorage: MSAL supports sessionStorage too, but
|
|
442
|
+
// localStorage is the standard for persistent auth in browsers
|
|
443
|
+
cache: declared.cache ?? { cacheLocation: 'localStorage' },
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Auto-generate authority URL from tenant ID if not explicitly provided
|
|
447
|
+
// This simplifies configuration for most common cases
|
|
448
|
+
if (!clientConfig.auth.authority && clientConfig.auth.tenantId) {
|
|
449
|
+
clientConfig.auth.authority = `https://login.microsoftonline.com/${clientConfig.auth.tenantId}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Integrate framework telemetry with MSAL logging system
|
|
453
|
+
// This allows MSAL events to flow through the framework's telemetry pipeline
|
|
454
|
+
if (!clientConfig.system?.loggerOptions && config.telemetry?.provider) {
|
|
455
|
+
const { provider, metadata, scope } = config.telemetry;
|
|
456
|
+
|
|
457
|
+
provider.trackEvent({
|
|
458
|
+
name: 'module-msal.configurator._processConfig.client-telemetry-connected',
|
|
459
|
+
level: TelemetryLevel.Debug,
|
|
460
|
+
scope,
|
|
461
|
+
metadata,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
clientConfig.system = {
|
|
465
|
+
...clientConfig.system,
|
|
466
|
+
loggerOptions: {
|
|
467
|
+
// Only log PII in development to protect user privacy in production
|
|
468
|
+
piiLoggingEnabled: process.env.NODE_ENV === 'development',
|
|
469
|
+
// Bridge MSAL log events to framework telemetry system
|
|
470
|
+
loggerCallback: createClientLogCallback(provider, metadata, [...scope, '3rd-party']),
|
|
471
|
+
// Use Warning level by default - captures errors and warnings without being verbose
|
|
472
|
+
logLevel: LogLevel.Warning,
|
|
473
|
+
// Preserve any user-provided logger options (allows customization)
|
|
474
|
+
...clientConfig.system?.loggerOptions,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Apply silent cache lookup policy if configured
|
|
480
|
+
if (config.cacheLookupPolicy !== undefined) {
|
|
481
|
+
clientConfig.cacheLookupPolicy = config.cacheLookupPolicy;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return clientConfig;
|
|
396
485
|
}
|
|
397
486
|
}
|
package/src/MsalProvider.ts
CHANGED
|
@@ -67,6 +67,8 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
67
67
|
* Default OAuth scopes used when the caller provides no scopes.
|
|
68
68
|
*
|
|
69
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.
|
|
70
72
|
*/
|
|
71
73
|
get defaultScopes(): string[] {
|
|
72
74
|
const clientId = this.#client.clientId;
|
|
@@ -91,6 +93,8 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
91
93
|
*
|
|
92
94
|
* Provides access to the underlying MSAL PublicClientApplication for advanced use cases.
|
|
93
95
|
* Prefer using provider methods for standard authentication operations.
|
|
96
|
+
*
|
|
97
|
+
* @returns The underlying MSAL client instance.
|
|
94
98
|
*/
|
|
95
99
|
get client(): IMsalClient {
|
|
96
100
|
return this.#client;
|
|
@@ -101,6 +105,8 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
101
105
|
*
|
|
102
106
|
* Returns the active account if a user is authenticated, or null if no user is logged in.
|
|
103
107
|
* This is a shorthand for `client.getActiveAccount()`.
|
|
108
|
+
*
|
|
109
|
+
* @returns The currently authenticated account, or `null` if no user is logged in.
|
|
104
110
|
*/
|
|
105
111
|
get account(): AccountInfo | null {
|
|
106
112
|
return this.#client.getActiveAccount();
|
|
@@ -175,6 +181,8 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
175
181
|
*
|
|
176
182
|
* The provider will attempt automatic login with empty scopes if requiresAuth is true.
|
|
177
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.
|
|
178
186
|
*/
|
|
179
187
|
async initialize(): Promise<void> {
|
|
180
188
|
// Guard: skip authentication when running inside MSAL's hidden iframe.
|
|
@@ -204,6 +212,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
204
212
|
// Use MSAL's acquireTokenByCode to exchange backend auth code for tokens
|
|
205
213
|
// This follows Microsoft's standard SPA Auth Code Flow pattern
|
|
206
214
|
const clientId = this.#client.clientId;
|
|
215
|
+
// A client ID is required to build the default scope used for the auth code exchange
|
|
207
216
|
if (!clientId) {
|
|
208
217
|
throw new Error('Client ID is required for auth code exchange');
|
|
209
218
|
}
|
|
@@ -248,6 +257,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
248
257
|
// Priority 1: Check if returning from redirect-based authentication
|
|
249
258
|
// This handles cases where user just completed a login/acquireToken via redirect
|
|
250
259
|
const handleRedirectResult = await this.handleRedirect();
|
|
260
|
+
// A returned account means the user just completed a redirect-based login
|
|
251
261
|
if (handleRedirectResult?.account) {
|
|
252
262
|
// Successfully authenticated via redirect - set as active account
|
|
253
263
|
// This means the user was redirected to Microsoft and came back authenticated
|
|
@@ -263,6 +273,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
263
273
|
// Note: Using default scopes here as we don't know what scopes the app needs yet
|
|
264
274
|
// App should call acquireToken with actual scopes after initialization
|
|
265
275
|
const loginResult = await this.login({ request: { scopes: this.defaultScopes } });
|
|
276
|
+
// Only set the active account when the automatic login actually returned one
|
|
266
277
|
if (loginResult?.account) {
|
|
267
278
|
// Automatic login successful - set as active account
|
|
268
279
|
this.#client.setActiveAccount(loginResult.account);
|
|
@@ -315,6 +326,8 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
315
326
|
* @remark Empty scopes are currently tracked as telemetry exceptions but execution continues for monitoring purposes.
|
|
316
327
|
* This behavior will be changed to throw exceptions once sufficient metrics are collected.
|
|
317
328
|
*
|
|
329
|
+
* @throws {Error} Re-throws any error encountered during token acquisition after tracking it via telemetry.
|
|
330
|
+
*
|
|
318
331
|
* @example
|
|
319
332
|
* ```typescript
|
|
320
333
|
* // Modern API format
|
|
@@ -371,6 +384,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
371
384
|
|
|
372
385
|
// Handle empty scopes - currently monitoring for telemetry, will throw in future
|
|
373
386
|
if (candidateScopes.length === 0) {
|
|
387
|
+
// Fall back to the client-id-derived default scope when one is available
|
|
374
388
|
if (defaultScopes.length > 0) {
|
|
375
389
|
this._trackEvent('acquireToken.missing-scope.defaulted', TelemetryLevel.Warning, {
|
|
376
390
|
properties: { ...telemetryProperties, defaultScopes },
|
|
@@ -383,7 +397,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
383
397
|
exception,
|
|
384
398
|
properties: telemetryProperties,
|
|
385
399
|
});
|
|
386
|
-
// TODO: throw exception when sufficient metrics are collected
|
|
400
|
+
// TODO(#5113): throw exception when sufficient metrics are collected
|
|
387
401
|
// This allows us to monitor how often empty scopes are provided before enforcing validation
|
|
388
402
|
}
|
|
389
403
|
}
|
|
@@ -521,6 +535,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
521
535
|
}
|
|
522
536
|
}
|
|
523
537
|
|
|
538
|
+
// Perform interactive authentication based on specified behavior
|
|
524
539
|
switch (behavior) {
|
|
525
540
|
case 'popup':
|
|
526
541
|
return await this.#client.loginPopup(request);
|
|
@@ -606,6 +621,7 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
606
621
|
async handleRedirect(): Promise<AuthenticationResult | null> {
|
|
607
622
|
// Process any pending redirect from authentication flow
|
|
608
623
|
const result = await this.client.handleRedirectPromise();
|
|
624
|
+
// Only track/log when a redirect result is actually returned
|
|
609
625
|
if (result) {
|
|
610
626
|
// Track successful redirect completion for monitoring
|
|
611
627
|
this._trackEvent('handleRedirect.success', TelemetryLevel.Information, {
|
|
@@ -632,6 +648,9 @@ export class MsalProvider extends BaseModuleProvider<MsalConfig> implements IMsa
|
|
|
632
648
|
* - Version compatibility is tracked via telemetry
|
|
633
649
|
* - Throws error if unsupported version is requested
|
|
634
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
|
+
*
|
|
635
654
|
* @example
|
|
636
655
|
* ```typescript
|
|
637
656
|
* // Create v2-compatible proxy
|
|
@@ -20,6 +20,23 @@ const createInitialConfig = (): Pick<MsalConfig, 'telemetry'> => ({
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
describe('MsalConfigurator', () => {
|
|
23
|
+
it('enriches a copy, leaving the declared client configuration untouched', async () => {
|
|
24
|
+
// A caller may reuse or assert on the object it passed, and the defaults
|
|
25
|
+
// applied here are derived — rewriting it behind their back is not ours to do
|
|
26
|
+
const declared = { auth: { clientId: 'my-app', tenantId: 'my-tenant' } };
|
|
27
|
+
const configurator = new MsalConfigurator();
|
|
28
|
+
|
|
29
|
+
configurator.setClientConfig(declared);
|
|
30
|
+
|
|
31
|
+
const config = await configurator.createConfigAsync(
|
|
32
|
+
createConfigCallbackArgs(),
|
|
33
|
+
createInitialConfig(),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(declared).toEqual({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
|
|
37
|
+
expect(config.client?.tenantId).toBe('my-tenant');
|
|
38
|
+
});
|
|
39
|
+
|
|
23
40
|
it('setAuthCode should normalize surrounding whitespace', async () => {
|
|
24
41
|
const configurator = new MsalConfigurator();
|
|
25
42
|
|
|
@@ -74,6 +91,95 @@ describe('MsalConfigurator', () => {
|
|
|
74
91
|
expect(config.client).toBeUndefined();
|
|
75
92
|
});
|
|
76
93
|
|
|
94
|
+
describe('_createClient', () => {
|
|
95
|
+
it('builds from the same resolved client config the real client would get', async () => {
|
|
96
|
+
// The mock relies on this: substituting the client must not also mean
|
|
97
|
+
// re-implementing authority, cache and telemetry resolution
|
|
98
|
+
const received: unknown[] = [];
|
|
99
|
+
class CustomConfigurator extends MsalConfigurator {
|
|
100
|
+
protected override async _createClient(config: MsalConfig): Promise<IMsalClient> {
|
|
101
|
+
received.push(this._createClientConfig(config));
|
|
102
|
+
return createClient();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const configurator = new CustomConfigurator();
|
|
107
|
+
configurator.setClientConfig({ auth: { clientId: 'client-id', tenantId: 'tenant-id' } });
|
|
108
|
+
|
|
109
|
+
await configurator.createConfigAsync(createConfigCallbackArgs(), createInitialConfig());
|
|
110
|
+
|
|
111
|
+
expect(received).toEqual([
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
auth: expect.objectContaining({
|
|
114
|
+
clientId: 'client-id',
|
|
115
|
+
// derived by the configurator, not by the caller
|
|
116
|
+
authority: 'https://login.microsoftonline.com/tenant-id',
|
|
117
|
+
}),
|
|
118
|
+
cache: { cacheLocation: 'localStorage' },
|
|
119
|
+
}),
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('supplies the client when none was set', async () => {
|
|
124
|
+
const client = createClient();
|
|
125
|
+
class CustomConfigurator extends MsalConfigurator {
|
|
126
|
+
protected override async _createClient(): Promise<IMsalClient> {
|
|
127
|
+
return client;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const config = await new CustomConfigurator().createConfigAsync(
|
|
132
|
+
createConfigCallbackArgs(),
|
|
133
|
+
createInitialConfig(),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(config.client).toBe(client);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('is not consulted when a client was set, so setClient always wins', async () => {
|
|
140
|
+
const own = createClient();
|
|
141
|
+
const createOther = vi.fn().mockResolvedValue(createClient());
|
|
142
|
+
class CustomConfigurator extends MsalConfigurator {
|
|
143
|
+
protected override _createClient(): Promise<IMsalClient> {
|
|
144
|
+
return createOther();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const configurator = new CustomConfigurator();
|
|
149
|
+
configurator.setClient(own);
|
|
150
|
+
|
|
151
|
+
const config = await configurator.createConfigAsync(
|
|
152
|
+
createConfigCallbackArgs(),
|
|
153
|
+
createInitialConfig(),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(config.client).toBe(own);
|
|
157
|
+
expect(createOther).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('is not consulted when hoisted, so a host provider is never shadowed', async () => {
|
|
161
|
+
// A hoisted module authenticates through the host's provider, so anything
|
|
162
|
+
// built here would be discarded — or worse, shadow the host's user
|
|
163
|
+
const createOther = vi.fn().mockResolvedValue(createClient());
|
|
164
|
+
class CustomConfigurator extends MsalConfigurator {
|
|
165
|
+
protected override _createClient(): Promise<IMsalClient> {
|
|
166
|
+
return createOther();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const configurator = new CustomConfigurator();
|
|
171
|
+
configurator.setClientConfig({ auth: { clientId: 'client-id', tenantId: 'tenant-id' } });
|
|
172
|
+
|
|
173
|
+
const config = await configurator.createConfigAsync(
|
|
174
|
+
{ ...createConfigCallbackArgs(), ref: { auth: {} } },
|
|
175
|
+
createInitialConfig(),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(config.client).toBeUndefined();
|
|
179
|
+
expect(createOther).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
77
183
|
describe('cacheLookupPolicy', () => {
|
|
78
184
|
it('defaults to CacheLookupPolicy.AccessTokenAndRefreshToken', async () => {
|
|
79
185
|
const configurator = new MsalConfigurator();
|