@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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Version Management
|
|
2
|
+
|
|
3
|
+
The MSAL module includes built-in version checking to ensure compatibility between different MSAL library versions.
|
|
4
|
+
|
|
5
|
+
## Version Resolution
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { resolveVersion, VersionError } from '@equinor/fusion-framework-module-msal/versioning';
|
|
9
|
+
|
|
10
|
+
// Resolve and validate a version
|
|
11
|
+
const result = resolveVersion('2.0.0');
|
|
12
|
+
console.log(result.isLatest); // false
|
|
13
|
+
console.log(result.satisfiesLatest); // true
|
|
14
|
+
console.log(result.enumVersion); // MsalModuleVersion.V2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Version Checking Behavior
|
|
18
|
+
|
|
19
|
+
- **Major Version Incompatibility**: Throws `VersionError` if requested major version is greater than latest
|
|
20
|
+
- **Minor Version Mismatch**: Logs warning but allows execution
|
|
21
|
+
- **Patch Differences**: Ignored for compatibility
|
|
22
|
+
- **Invalid Versions**: Throws `VersionError` with descriptive message
|
|
23
|
+
|
|
24
|
+
## API Reference
|
|
25
|
+
|
|
26
|
+
### `resolveVersion(version: string | SemVer): ResolvedVersion`
|
|
27
|
+
|
|
28
|
+
Resolves and validates a version string against the latest available MSAL version.
|
|
29
|
+
|
|
30
|
+
**Parameters:**
|
|
31
|
+
- `version` - Version string or SemVer object to resolve
|
|
32
|
+
|
|
33
|
+
**Returns:** `ResolvedVersion` object containing:
|
|
34
|
+
- `wantedVersion: SemVer` - The parsed requested version
|
|
35
|
+
- `latestVersion: SemVer` - The latest available version
|
|
36
|
+
- `isLatest: boolean` - Whether the version is exactly the latest
|
|
37
|
+
- `satisfiesLatest: boolean` - Whether the major version matches latest
|
|
38
|
+
- `enumVersion: MsalModuleVersion` - Corresponding enum version
|
|
39
|
+
|
|
40
|
+
**Throws:** `VersionError` for invalid or incompatible versions
|
|
41
|
+
|
|
42
|
+
### `VersionError`
|
|
43
|
+
|
|
44
|
+
Error class for version-related issues with the following types:
|
|
45
|
+
- `InvalidVersion` - Requested version is not a valid semver
|
|
46
|
+
- `InvalidLatestVersion` - Latest version parsing failed (build issue)
|
|
47
|
+
- `MajorIncompatibility` - Major version is greater than latest
|
|
48
|
+
- `MinorMismatch` - Minor version differs (warning only)
|
|
49
|
+
- `PatchDifference` - Patch version differs (info only)
|
|
50
|
+
- `IncompatibleVersion` - General incompatibility
|
|
51
|
+
|
|
52
|
+
## Error Handling
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { resolveVersion, VersionError } from '@equinor/fusion-framework-module-msal/versioning';
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = resolveVersion('3.0.0'); // Assuming latest is 2.x
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof VersionError) {
|
|
61
|
+
console.error('Version error:', error.message);
|
|
62
|
+
console.error('Requested:', error.requestedVersion);
|
|
63
|
+
console.error('Latest:', error.latestVersion);
|
|
64
|
+
console.error('Type:', error.type);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-msal",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0-next.0",
|
|
4
4
|
"description": "Microsoft Authentication Library (MSAL) integration module for Fusion Framework",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"./v4": {
|
|
17
17
|
"import": "./dist/esm/v4/index.js",
|
|
18
18
|
"types": "./dist/types/v4/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./mock": {
|
|
21
|
+
"import": "./dist/esm/mock/index.js",
|
|
22
|
+
"types": "./dist/types/mock/index.d.ts"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"typesVersions": {
|
|
@@ -28,6 +32,9 @@
|
|
|
28
32
|
],
|
|
29
33
|
"v4": [
|
|
30
34
|
"dist/types/v4/index.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"mock": [
|
|
37
|
+
"dist/types/mock/index.d.ts"
|
|
31
38
|
]
|
|
32
39
|
}
|
|
33
40
|
},
|
|
@@ -50,16 +57,16 @@
|
|
|
50
57
|
"semver": "^7.7.4",
|
|
51
58
|
"typescript": "^7.0.2",
|
|
52
59
|
"zod": "^4.4.3",
|
|
53
|
-
"@equinor/fusion-framework-module": "^6.1.
|
|
54
|
-
"@equinor/fusion-framework-module-telemetry": "^
|
|
60
|
+
"@equinor/fusion-framework-module": "^6.1.3-next.0",
|
|
61
|
+
"@equinor/fusion-framework-module-telemetry": "^8.0.0-next.0"
|
|
55
62
|
},
|
|
56
63
|
"peerDependencies": {
|
|
57
64
|
"@types/semver": "^7.0.0",
|
|
58
65
|
"semver": "^7.0.0",
|
|
59
66
|
"typescript": ">=5.0.0",
|
|
60
67
|
"zod": "^4.0.0",
|
|
61
|
-
"@equinor/fusion-framework-module": "^6.1.
|
|
62
|
-
"@equinor/fusion-framework-module-telemetry": "^
|
|
68
|
+
"@equinor/fusion-framework-module": "^6.1.3-next.0",
|
|
69
|
+
"@equinor/fusion-framework-module-telemetry": "^8.0.0-next.0"
|
|
63
70
|
},
|
|
64
71
|
"peerDependenciesMeta": {
|
|
65
72
|
"@equinor/fusion-framework-module-telemetry": {
|
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,6 +34,7 @@ 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.
|
|
@@ -107,6 +63,9 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
107
63
|
return telemetry;
|
|
108
64
|
}
|
|
109
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);
|
|
110
69
|
// Default cache lookup policy to AccessTokenAndRefreshToken to avoid iframe fallback delays
|
|
111
70
|
this._set('cacheLookupPolicy', async () => CacheLookupPolicy.AccessTokenAndRefreshToken);
|
|
112
71
|
}
|
|
@@ -136,6 +95,23 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
136
95
|
return this;
|
|
137
96
|
}
|
|
138
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
|
+
|
|
139
115
|
/**
|
|
140
116
|
* Sets the cache lookup policy used for every silent token acquisition.
|
|
141
117
|
*
|
|
@@ -274,10 +250,23 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
274
250
|
* ```
|
|
275
251
|
*/
|
|
276
252
|
setClient(client: IMsalClient): this {
|
|
277
|
-
this
|
|
253
|
+
this.#client = client;
|
|
278
254
|
return this;
|
|
279
255
|
}
|
|
280
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
|
+
|
|
281
270
|
/**
|
|
282
271
|
* Sets telemetry provider for MSAL authentication events.
|
|
283
272
|
*
|
|
@@ -328,72 +317,170 @@ export class MsalConfigurator extends BaseConfigBuilder<MsalConfig> {
|
|
|
328
317
|
/**
|
|
329
318
|
* Processes and validates the configuration.
|
|
330
319
|
*
|
|
331
|
-
* @param
|
|
320
|
+
* @param rawConfig - Raw configuration object
|
|
321
|
+
* @param init - The builder arguments, carrying the host reference when hoisted
|
|
332
322
|
* @returns Processed and validated configuration
|
|
333
323
|
*/
|
|
334
|
-
async _processConfig(
|
|
324
|
+
async _processConfig(
|
|
325
|
+
rawConfig: MsalConfig,
|
|
326
|
+
init?: ConfigBuilderCallbackArgs,
|
|
327
|
+
): Promise<MsalConfig> {
|
|
335
328
|
// Validate and coerce configuration using Zod schema
|
|
336
329
|
const config = await MsalConfigSchema.parseAsync(rawConfig);
|
|
337
330
|
|
|
338
|
-
// Auto-create client if
|
|
331
|
+
// Auto-create client if no client instance was supplied
|
|
339
332
|
// This allows users to provide configuration without manually instantiating the client
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
339
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
level: TelemetryLevel.Debug,
|
|
346
|
-
scope: config.telemetry.scope,
|
|
347
|
-
metadata: { ...config.telemetry.metadata, clientConfig },
|
|
348
|
-
});
|
|
340
|
+
return config;
|
|
341
|
+
}
|
|
349
342
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
}
|
|
355
379
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
clientConfig.cache = { cacheLocation: 'localStorage' };
|
|
360
|
-
}
|
|
380
|
+
// Instantiate MSAL client with fully configured options
|
|
381
|
+
return new MsalClient(clientConfig);
|
|
382
|
+
}
|
|
361
383
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
logLevel: LogLevel.Warning,
|
|
383
|
-
// Preserve any user-provided logger options (allows customization)
|
|
384
|
-
...clientConfig.system?.loggerOptions,
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
// Apply silent cache lookup policy if configured
|
|
389
|
-
if (config.cacheLookupPolicy !== undefined) {
|
|
390
|
-
clientConfig.cacheLookupPolicy = config.cacheLookupPolicy;
|
|
391
|
-
}
|
|
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
|
+
}
|
|
392
404
|
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
395
426
|
}
|
|
396
427
|
|
|
397
|
-
|
|
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;
|
|
398
485
|
}
|
|
399
486
|
}
|
|
@@ -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();
|