@equinor/fusion-framework-module-msal 11.0.0 → 11.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +8 -5
- package/CHANGELOG.md +0 -1212
- package/docs/api-reference.md +0 -85
- package/docs/auth-code-flow.md +0 -86
- package/docs/migration-v2-to-v4.md +0 -115
- package/docs/testing.md +0 -191
- package/docs/troubleshooting.md +0 -17
- package/docs/version-management.md +0 -67
- package/src/MsalClient.interface.ts +0 -139
- package/src/MsalClient.ts +0 -326
- package/src/MsalConfigurator.ts +0 -486
- package/src/MsalProvider.interface.ts +0 -179
- package/src/MsalProvider.ts +0 -776
- package/src/MsalProxyProvider.interface.ts +0 -72
- package/src/__tests__/MsalConfigurator.test.ts +0 -222
- package/src/__tests__/MsalProvider.test.ts +0 -74
- package/src/__tests__/create-proxy-provider.test.ts +0 -77
- package/src/__tests__/mock/create-mock-user-from-token.test.ts +0 -46
- package/src/__tests__/mock/msal-mock.test.ts +0 -613
- package/src/__tests__/versioning/resolve-version.test.ts +0 -161
- package/src/create-client-log-callback.ts +0 -102
- package/src/create-proxy-provider.ts +0 -97
- package/src/index.ts +0 -48
- package/src/mock/MsalMockClient.ts +0 -618
- package/src/mock/MsalMockConfigurator.ts +0 -305
- package/src/mock/create-mock-token.ts +0 -92
- package/src/mock/create-mock-user-from-token.ts +0 -46
- package/src/mock/create-msal-mock-client.ts +0 -25
- package/src/mock/decode-jwt-segment.ts +0 -22
- package/src/mock/index.ts +0 -30
- package/src/mock/module.ts +0 -54
- package/src/module.ts +0 -142
- package/src/msal-config-schema.ts +0 -81
- package/src/static.ts +0 -38
- package/src/telemetry-config-schema.ts +0 -25
- package/src/types.ts +0 -16
- package/src/util/compare-origin.ts +0 -18
- package/src/util/normalize-uri.ts +0 -24
- package/src/util/redirect.ts +0 -19
- package/src/v2/IAuthClient.interface.ts +0 -114
- package/src/v2/Logger.ts +0 -204
- package/src/v2/MsalProvider.interface.ts +0 -102
- package/src/v2/create-proxy-client.ts +0 -195
- package/src/v2/create-proxy-provider.ts +0 -177
- package/src/v2/map-account-info.ts +0 -23
- package/src/v2/map-authentication-result.ts +0 -28
- package/src/v2/types.ts +0 -674
- package/src/v4/create-proxy-provider.ts +0 -75
- package/src/v4/index.ts +0 -13
- package/src/v4/types.ts +0 -727
- package/src/version.ts +0 -2
- package/src/versioning/VersionError.ts +0 -64
- package/src/versioning/index.ts +0 -29
- package/src/versioning/resolve-version.ts +0 -154
- package/src/versioning/types.ts +0 -60
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ConfigBuilderCallback,
|
|
3
|
-
ConfigBuilderCallbackArgs,
|
|
4
|
-
} from '@equinor/fusion-framework-module';
|
|
5
|
-
|
|
6
|
-
import type { IMsalClient } from '../MsalClient.interface';
|
|
7
|
-
import type { IMsalProvider } from '../MsalProvider.interface';
|
|
8
|
-
import type { MsalClientConfig } from '../MsalClient';
|
|
9
|
-
import { MsalConfigurator, type MsalConfig } from '../MsalConfigurator';
|
|
10
|
-
|
|
11
|
-
import { MsalMockClient, type MsalMockUser } from './MsalMockClient';
|
|
12
|
-
import { createMockUserFromToken } from './create-mock-user-from-token';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Declares the mock's own branch of the MSAL configuration.
|
|
16
|
-
*
|
|
17
|
-
* @remarks
|
|
18
|
-
* Merging into `MsalConfigExtension` is what lets `setAccount` record the
|
|
19
|
-
* user through the ordinary builder — `_set` derives its target from
|
|
20
|
-
* {@link MsalConfig}, so a key the type does not know about could only be set by
|
|
21
|
-
* casting past it.
|
|
22
|
-
*
|
|
23
|
-
* The schema strips `mock` when it validates, so a declaration made here travels
|
|
24
|
-
* the builder and stops there: it is readable from the raw configuration and
|
|
25
|
-
* absent from the one `MsalProvider` receives.
|
|
26
|
-
*/
|
|
27
|
-
declare module '../msal-config-schema' {
|
|
28
|
-
interface MsalConfigExtension {
|
|
29
|
-
mock?: {
|
|
30
|
-
/**
|
|
31
|
-
* The user to sign in, resolved if it was declared as a callback, or
|
|
32
|
-
* `null` when nobody is signed in.
|
|
33
|
-
*/
|
|
34
|
-
account?: MsalMockUser | null;
|
|
35
|
-
/**
|
|
36
|
-
* The token to return verbatim instead of one generated from the
|
|
37
|
-
* signed-in user's fields.
|
|
38
|
-
*/
|
|
39
|
-
token?: string;
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The client configuration used when a test declares none.
|
|
46
|
-
*
|
|
47
|
-
* @remarks
|
|
48
|
-
* `MsalClientConfig.auth.clientId` is required, so a mock still needs a client
|
|
49
|
-
* configuration to exist. Supplying a default is what lets an application boot
|
|
50
|
-
* under test without declaring credentials it does not have.
|
|
51
|
-
*/
|
|
52
|
-
const defaultMockClientConfig: MsalClientConfig = {
|
|
53
|
-
auth: {
|
|
54
|
-
clientId: 'fusion-mock-client',
|
|
55
|
-
tenantId: 'fusion-mock-tenant',
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The real MSAL configurator, backed by an in-process client.
|
|
61
|
-
*
|
|
62
|
-
* @remarks
|
|
63
|
-
* Nothing else changes: the same builder API, the same validation and the same
|
|
64
|
-
* `MsalProvider` are used. Only the boundary that would contact Entra ID is
|
|
65
|
-
* substituted, through the same
|
|
66
|
-
* {@link MsalConfigurator._createClient | _createClient} seam the real
|
|
67
|
-
* configurator builds its own client from — and from the same
|
|
68
|
-
* {@link MsalConfigurator._createClientConfig | _createClientConfig}, so
|
|
69
|
-
* `setClientConfig` means exactly what it means in production.
|
|
70
|
-
*
|
|
71
|
-
* A user named `Test User` is signed in by default, so an application boots
|
|
72
|
-
* without declaring anything.
|
|
73
|
-
*
|
|
74
|
-
* @example Name the signed-in user
|
|
75
|
-
* ```typescript
|
|
76
|
-
* enableMsalMock(configurator, (builder) => {
|
|
77
|
-
* builder.setAccount({ name: 'Ada Lovelace', username: 'ada@equinor.com' });
|
|
78
|
-
* });
|
|
79
|
-
* ```
|
|
80
|
-
*
|
|
81
|
-
* @example Configure the client exactly as in production
|
|
82
|
-
* ```typescript
|
|
83
|
-
* enableMsalMock(configurator, (builder) => {
|
|
84
|
-
* builder.setClientConfig({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
|
|
85
|
-
* });
|
|
86
|
-
* ```
|
|
87
|
-
*
|
|
88
|
-
* @example Take full control of authentication
|
|
89
|
-
* ```typescript
|
|
90
|
-
* enableMsalMock(configurator, (builder) => {
|
|
91
|
-
* builder.setClient(new MyOwnMsalClient());
|
|
92
|
-
* });
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
export class MsalMockConfigurator extends MsalConfigurator {
|
|
96
|
-
/**
|
|
97
|
-
* Declares the user to sign in.
|
|
98
|
-
*
|
|
99
|
-
* @remarks
|
|
100
|
-
* Who is signed in is session state, not client configuration — which is what
|
|
101
|
-
* lets {@link MsalMockClient} take the same argument the real client takes: a
|
|
102
|
-
* client is configured with *what it talks to*, never with *who is signed in*.
|
|
103
|
-
*
|
|
104
|
-
* The user is therefore recorded on the configuration as `mock.account`, not
|
|
105
|
-
* on this builder, and is signed in on whichever client the module ends up
|
|
106
|
-
* authenticating through — wherever that client was built:
|
|
107
|
-
*
|
|
108
|
-
* - The client this builder builds, normally. The user is in place before
|
|
109
|
-
* `MsalProvider.initialize` runs, which is what makes the provider's own
|
|
110
|
-
* start-up path observable: with `signedOut` and `setRequiresAuth(true)`, a
|
|
111
|
-
* test sees the real automatic login run.
|
|
112
|
-
* - The **host's** client when the module is hoisted onto a host
|
|
113
|
-
* application's provider, because none is built here. An application inside
|
|
114
|
-
* a portal shares the portal's session, so this changes who the host sees
|
|
115
|
-
* signed in too, as it would in production.
|
|
116
|
-
* - A client supplied through {@link MsalConfigurator.setClient | setClient},
|
|
117
|
-
* when that client is a {@link MsalMockClient}.
|
|
118
|
-
*
|
|
119
|
-
* Throws when that client cannot represent a declared user, rather than
|
|
120
|
-
* failing quietly — a silent no-op is the whole failure mode this exists to
|
|
121
|
-
* prevent.
|
|
122
|
-
*
|
|
123
|
-
* Pass `null` when nobody is signed in, or `{ signedOut: true }` to keep an
|
|
124
|
-
* identity without a session — a later login then resolves as that user.
|
|
125
|
-
*
|
|
126
|
-
* @param account - The user, or an ordinary config-builder callback resolving it.
|
|
127
|
-
* @returns The builder, for chaining.
|
|
128
|
-
*
|
|
129
|
-
* @example Derive the user from the modules in scope
|
|
130
|
-
* ```typescript
|
|
131
|
-
* builder.setAccount(async ({ hasModule }) => ({
|
|
132
|
-
* name: hasModule('app') ? 'App User' : 'Portal User',
|
|
133
|
-
* }));
|
|
134
|
-
* ```
|
|
135
|
-
*/
|
|
136
|
-
public setAccount(
|
|
137
|
-
account: MsalMockUser | null | ConfigBuilderCallback<MsalMockUser | null>,
|
|
138
|
-
): this {
|
|
139
|
-
this._set('mock.account', account);
|
|
140
|
-
return this;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Declares the token to return, independent of who is signed in.
|
|
145
|
-
*
|
|
146
|
-
* @remarks
|
|
147
|
-
* Use this when a backend mock validates its own tokens (specific claims, an
|
|
148
|
-
* audience, or a signature) — the client then returns this token verbatim
|
|
149
|
-
* instead of fabricating one from the signed-in user's fields.
|
|
150
|
-
*
|
|
151
|
-
* @param token - A JWT (e.g. from `createMockToken`, or issued by an external mock).
|
|
152
|
-
* @param skipResolve - When `true`, override only the token and leave an account
|
|
153
|
-
* declared through {@link setAccount} untouched. Defaults to `false`, which also signs
|
|
154
|
-
* in the user described by the token's claims, via {@link createMockUserFromToken}.
|
|
155
|
-
* @returns The builder, for chaining.
|
|
156
|
-
*
|
|
157
|
-
* @example Sign in as whoever the token names
|
|
158
|
-
* ```typescript
|
|
159
|
-
* builder.setToken(token);
|
|
160
|
-
* ```
|
|
161
|
-
*
|
|
162
|
-
* @example Keep a separately declared account, but return this exact token
|
|
163
|
-
* ```typescript
|
|
164
|
-
* builder.setAccount({ name: 'Ada Lovelace' }).setToken(token, true);
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
public setToken(token: string, skipResolve = false): this {
|
|
168
|
-
this._set('mock.token', token);
|
|
169
|
-
// skipResolve defaults to false - most callers want the token's claims to name who is signed in
|
|
170
|
-
if (!skipResolve) {
|
|
171
|
-
this.setAccount(createMockUserFromToken(token));
|
|
172
|
-
}
|
|
173
|
-
return this;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Resolves the client the module authenticates through, wherever it was built.
|
|
178
|
-
*
|
|
179
|
-
* @remarks
|
|
180
|
-
* Shared by {@link setAccount} and {@link setToken} application: neither can
|
|
181
|
-
* assume the scope declaring mock state is the scope that built the client,
|
|
182
|
-
* which is exactly what is not true when an application is tested inside a
|
|
183
|
-
* portal. The host built that client, in a scope this builder never sees, so
|
|
184
|
-
* the client has to be located rather than assumed.
|
|
185
|
-
*
|
|
186
|
-
* @param config - The validated configuration, carrying the client when one was built.
|
|
187
|
-
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
188
|
-
* @param action - Describes what could not be applied, for the thrown error.
|
|
189
|
-
* @returns The resolved mock client.
|
|
190
|
-
* @throws When the resolved client is not a {@link MsalMockClient}.
|
|
191
|
-
*/
|
|
192
|
-
#getClient(
|
|
193
|
-
config: MsalConfig,
|
|
194
|
-
init: ConfigBuilderCallbackArgs | undefined,
|
|
195
|
-
action: string,
|
|
196
|
-
): MsalMockClient {
|
|
197
|
-
const host = (init?.ref as { auth?: IMsalProvider } | undefined)?.auth;
|
|
198
|
-
const client = config.client ?? host?.client;
|
|
199
|
-
|
|
200
|
-
// Reject a real client because mock state cannot be applied to it.
|
|
201
|
-
if (!(client instanceof MsalMockClient)) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`MsalMockConfigurator: cannot ${action}, because this module does not authenticate through a mock client. Declare it where that client is configured instead.`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return client;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Signs the declared user in on the client the module authenticates through.
|
|
212
|
-
*
|
|
213
|
-
* @param account - The user to sign in, or `null` when nobody is.
|
|
214
|
-
* @param config - The validated configuration, carrying the client when one was built.
|
|
215
|
-
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
216
|
-
* @throws When the resolved client is not a {@link MsalMockClient}.
|
|
217
|
-
*/
|
|
218
|
-
#signIn(
|
|
219
|
-
account: MsalMockUser | null,
|
|
220
|
-
config: MsalConfig,
|
|
221
|
-
init?: ConfigBuilderCallbackArgs,
|
|
222
|
-
): void {
|
|
223
|
-
this.#getClient(config, init, 'sign a user in').setUser(account);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Assembles the configuration, then signs the declared user in.
|
|
228
|
-
*
|
|
229
|
-
* @remarks
|
|
230
|
-
* Stands a client configuration in first when this builder is the one that
|
|
231
|
-
* will build a client: `MsalClientConfig.auth.clientId` is required to build
|
|
232
|
-
* any client at all and a test has no real credentials to declare. It then
|
|
233
|
-
* flows through the very same
|
|
234
|
-
* {@link MsalConfigurator._createClientConfig | _createClientConfig}
|
|
235
|
-
* enrichment the real client is built from, and anything declared through
|
|
236
|
-
* {@link MsalConfigurator.setClientConfig | setClientConfig} wins — exactly as
|
|
237
|
-
* in production.
|
|
238
|
-
*
|
|
239
|
-
* Doing that here rather than in the constructor is deliberate: a hoisted
|
|
240
|
-
* module authenticates through the host and builds no client, so it must not
|
|
241
|
-
* look configured either.
|
|
242
|
-
*
|
|
243
|
-
* The user is read from `rawConfig`, because the schema strips `mock` when it
|
|
244
|
-
* validates — the key exists to carry a test's declaration through the
|
|
245
|
-
* builder, never to reach the provider.
|
|
246
|
-
*
|
|
247
|
-
* @param rawConfig - The raw configuration to process.
|
|
248
|
-
* @param init - The builder arguments, carrying the host reference when hoisted.
|
|
249
|
-
* @returns The processed and validated configuration.
|
|
250
|
-
*/
|
|
251
|
-
override async _processConfig(
|
|
252
|
-
rawConfig: MsalConfig,
|
|
253
|
-
init?: ConfigBuilderCallbackArgs,
|
|
254
|
-
): Promise<MsalConfig> {
|
|
255
|
-
// Supply mock credentials only when this builder owns client construction.
|
|
256
|
-
if (!this._isHoisted(init) && !this.getClientConfig()) {
|
|
257
|
-
this.setClientConfig(defaultMockClientConfig);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const config = await super._processConfig(rawConfig, init);
|
|
261
|
-
|
|
262
|
-
// `null` is a declaration in its own right — nobody is signed in — so only
|
|
263
|
-
// an absent one means the test said nothing about the user
|
|
264
|
-
const account = rawConfig.mock?.account;
|
|
265
|
-
// Apply even null because null explicitly requests a signed-out mock state.
|
|
266
|
-
if (account !== undefined) {
|
|
267
|
-
this.#signIn(account, config, init);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Applied after the account so a token declared alongside `skipResolve: true`
|
|
271
|
-
// overrides whatever `setUser` above just fabricated.
|
|
272
|
-
const token = rawConfig.mock?.token;
|
|
273
|
-
// absent means the test declared no token override; leave the client generating its own
|
|
274
|
-
if (token !== undefined) {
|
|
275
|
-
this.#getClient(config, init, 'set a token').setToken(token);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return config;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Builds an in-process client.
|
|
283
|
-
*
|
|
284
|
-
* @remarks
|
|
285
|
-
* Called only when no client was set, so
|
|
286
|
-
* {@link MsalConfigurator.setClient | setClient} still replaces authentication
|
|
287
|
-
* outright.
|
|
288
|
-
*
|
|
289
|
-
* Deliberately does not delegate to `super`, which would build a real
|
|
290
|
-
* `MsalClient` and contact Entra ID. It is never reached when the module is
|
|
291
|
-
* hoisted onto a host application's provider, because the base configurator
|
|
292
|
-
* gates client creation on {@link MsalConfigurator._isHoisted | _isHoisted} —
|
|
293
|
-
* a mock client built there would shadow the host's client, the exact scenario
|
|
294
|
-
* an application-inside-a-portal test exists to cover.
|
|
295
|
-
*
|
|
296
|
-
* Knows nothing about who is signed in: a client is built from what it talks
|
|
297
|
-
* to, and the declared user is applied to it afterwards.
|
|
298
|
-
*
|
|
299
|
-
* @param config - The validated configuration the client is built from.
|
|
300
|
-
* @returns A client resolving tokens in-process.
|
|
301
|
-
*/
|
|
302
|
-
protected override async _createClient(config: MsalConfig): Promise<IMsalClient> {
|
|
303
|
-
return new MsalMockClient(this._createClientConfig(config) ?? defaultMockClientConfig);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
/**
|
|
33
|
-
* Encodes a value as base64url without padding, as used in JWT segments.
|
|
34
|
-
*
|
|
35
|
-
* @param value - Raw string to encode.
|
|
36
|
-
* @returns The base64url representation.
|
|
37
|
-
*/
|
|
38
|
-
const base64Url = (value: string): string => {
|
|
39
|
-
// btoa operates on latin1; encodeURIComponent round-trip keeps non-ASCII names intact
|
|
40
|
-
const bytes = new TextEncoder().encode(value);
|
|
41
|
-
let binary = '';
|
|
42
|
-
// Iterate over encoded bytes so Unicode claims are preserved before base64url encoding.
|
|
43
|
-
for (const byte of bytes) {
|
|
44
|
-
binary += String.fromCharCode(byte);
|
|
45
|
-
}
|
|
46
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Creates a structurally valid, unsigned JWT for use in tests.
|
|
51
|
-
*
|
|
52
|
-
* The token has three base64url segments and decodes to the supplied claims, so
|
|
53
|
-
* code that splits, decodes, or inspects token claims behaves exactly as it does
|
|
54
|
-
* in production. The signature segment is a fixed placeholder — the token is
|
|
55
|
-
* **not** cryptographically valid and will be rejected by any real service.
|
|
56
|
-
*
|
|
57
|
-
* @remarks
|
|
58
|
-
* Timestamps default to a fixed issue time and a one-hour lifetime so repeated
|
|
59
|
-
* runs produce byte-identical tokens. A test that needs an expired token can set
|
|
60
|
-
* `exp` in the past.
|
|
61
|
-
*
|
|
62
|
-
* @param claims - Claims to embed in the token payload.
|
|
63
|
-
* @returns An unsigned JWT string in `header.payload.signature` form.
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```typescript
|
|
67
|
-
* const token = createMockToken({ name: 'Test User', scp: 'Files.Read' });
|
|
68
|
-
* const [, payload] = token.split('.');
|
|
69
|
-
* JSON.parse(decodeJwtSegment(payload)).name; // 'Test User'
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export const createMockToken = (claims: MockTokenClaims = {}): string => {
|
|
73
|
-
const header = { alg: 'none', typ: 'JWT' };
|
|
74
|
-
// Fixed default clock keeps generated tokens byte-identical between runs
|
|
75
|
-
const issuedAt = claims.iat ?? 1_700_000_000;
|
|
76
|
-
const payload: MockTokenClaims = {
|
|
77
|
-
iss: 'https://login.microsoftonline.com/fusion-test-tenant/v2.0',
|
|
78
|
-
aud: 'fusion-test-client',
|
|
79
|
-
tid: 'fusion-test-tenant',
|
|
80
|
-
oid: 'fusion-test-user',
|
|
81
|
-
iat: issuedAt,
|
|
82
|
-
nbf: issuedAt,
|
|
83
|
-
exp: issuedAt + 3600,
|
|
84
|
-
...claims,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
return [
|
|
88
|
-
base64Url(JSON.stringify(header)),
|
|
89
|
-
base64Url(JSON.stringify(payload)),
|
|
90
|
-
'fusion-test-signature',
|
|
91
|
-
].join('.');
|
|
92
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { MsalMockUser } from './MsalMockClient';
|
|
2
|
-
import { decodeJwtSegment } from './decode-jwt-segment';
|
|
3
|
-
import type { MockTokenClaims } from './create-mock-token';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Derives a {@link MsalMockUser} from a JWT's payload claims, so a token minted
|
|
7
|
-
* outside this module (e.g. by a backend's own mock) can drive who the mock
|
|
8
|
-
* signs in as.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Maps the standard Entra ID claims Fusion applications read — `name`,
|
|
12
|
-
* `preferred_username`, `oid`, `tid`, `scp` — onto the matching
|
|
13
|
-
* {@link MsalMockUser} fields. Identity only: it does not affect which token
|
|
14
|
-
* the client returns — use {@link MsalMockConfigurator.setToken} for that.
|
|
15
|
-
*
|
|
16
|
-
* @param token - A JWT (e.g. from {@link createMockToken}, or issued by an
|
|
17
|
-
* external mock) with a base64url-encoded payload segment.
|
|
18
|
-
* @returns A mock user built from the token's claims.
|
|
19
|
-
* @throws When the token has no payload segment (`header.payload.signature`).
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```typescript
|
|
23
|
-
* enableMsalMock(configurator, (builder) => {
|
|
24
|
-
* builder.setAccount(createMockUserFromToken(token));
|
|
25
|
-
* });
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export const createMockUserFromToken = (token: string): MsalMockUser => {
|
|
29
|
-
const [, payload] = token.split('.');
|
|
30
|
-
// fail loudly rather than signing in an empty/garbage user from a malformed token
|
|
31
|
-
if (!payload) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
'createMockUserFromToken: expected a JWT with a payload segment (header.payload.signature)',
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const claims: MockTokenClaims = JSON.parse(decodeJwtSegment(payload));
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
name: claims.name,
|
|
41
|
-
username: claims.preferred_username,
|
|
42
|
-
userId: claims.oid,
|
|
43
|
-
tenantId: claims.tid,
|
|
44
|
-
scopes: claims.scp?.split(' '),
|
|
45
|
-
};
|
|
46
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { IMsalClient } from '../MsalClient.interface';
|
|
2
|
-
import type { MsalClientConfig } from '../MsalClient';
|
|
3
|
-
import { MsalMockClient, type MsalMockUser } from './MsalMockClient';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Convenience helper that creates a mock client instance.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* The class form is preferred for a more familiar configuration pattern.
|
|
10
|
-
*
|
|
11
|
-
* @param config - The same client configuration the real client is built from.
|
|
12
|
-
* @param user - Optional user to sign in, applied after construction.
|
|
13
|
-
* @returns A client that resolves tokens in-process.
|
|
14
|
-
*/
|
|
15
|
-
export const createMsalMockClient = (
|
|
16
|
-
config: MsalClientConfig,
|
|
17
|
-
user?: MsalMockUser,
|
|
18
|
-
): IMsalClient => {
|
|
19
|
-
const client = new MsalMockClient(config);
|
|
20
|
-
// Apply the optional identity after construction so the helper matches setUser semantics.
|
|
21
|
-
if (user) {
|
|
22
|
-
client.setUser(user);
|
|
23
|
-
}
|
|
24
|
-
return client;
|
|
25
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
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 function decodeJwtSegment(segment: string): string {
|
|
13
|
-
const base64 = segment
|
|
14
|
-
.replace(/-/g, '+')
|
|
15
|
-
.replace(/_/g, '/')
|
|
16
|
-
.padEnd(segment.length + ((4 - (segment.length % 4)) % 4), '=');
|
|
17
|
-
const binary = atob(base64);
|
|
18
|
-
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
19
|
-
return new TextDecoder().decode(bytes);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export default decodeJwtSegment;
|
package/src/mock/index.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock MSAL for tests: real provider, real configurator, fake client.
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* Substituting the client is the smallest change that removes Entra ID from a test.
|
|
6
|
-
* Everything above it — scope resolution, silent-first token acquisition, account
|
|
7
|
-
* handling, proxy providers, telemetry — is the production code path.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* import { enableMsalMock } from '@equinor/fusion-framework-module-msal/mock';
|
|
12
|
-
*
|
|
13
|
-
* // default mock user
|
|
14
|
-
* enableMsalMock(configurator);
|
|
15
|
-
*
|
|
16
|
-
* // or a specific one
|
|
17
|
-
* enableMsalMock(configurator, (builder) => {
|
|
18
|
-
* builder.setAccount({ name: 'Ada Lovelace', signedOut: true });
|
|
19
|
-
* });
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @packageDocumentation
|
|
23
|
-
*/
|
|
24
|
-
export { MsalMockClient, type MsalMockUser } from './MsalMockClient';
|
|
25
|
-
export { createMsalMockClient } from './create-msal-mock-client';
|
|
26
|
-
export { MsalMockConfigurator } from './MsalMockConfigurator';
|
|
27
|
-
export { enableMsalMock, msalMockModule, type AuthConfigMockFn } from './module';
|
|
28
|
-
export { createMockToken, type MockTokenClaims } from './create-mock-token';
|
|
29
|
-
export { createMockUserFromToken } from './create-mock-user-from-token';
|
|
30
|
-
export { decodeJwtSegment } from './decode-jwt-segment';
|
package/src/mock/module.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { IModulesConfigurator } from '@equinor/fusion-framework-module';
|
|
2
|
-
|
|
3
|
-
import { module, type MsalModule } from '../module';
|
|
4
|
-
|
|
5
|
-
import { MsalMockConfigurator } from './MsalMockConfigurator';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The MSAL module with a mock client instead of a live connection to Entra ID.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Only `configure` differs from the real module. `initialize` is the production
|
|
12
|
-
* one, untouched, so proxy providers, host-provider hoisting and provider
|
|
13
|
-
* initialization all behave exactly as they do in production — and a test
|
|
14
|
-
* observes the real start-up path rather than a rehearsal of it.
|
|
15
|
-
*/
|
|
16
|
-
export const msalMockModule: MsalModule = {
|
|
17
|
-
...module,
|
|
18
|
-
configure: () => new MsalMockConfigurator(),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Configuration callback for {@link enableMsalMock}.
|
|
23
|
-
*/
|
|
24
|
-
export type AuthConfigMockFn<TRef = unknown> = (
|
|
25
|
-
configurator: MsalMockConfigurator,
|
|
26
|
-
ref?: TRef,
|
|
27
|
-
) => void;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Enables MSAL against a mock client, so a test needs no credentials and no network.
|
|
31
|
-
*
|
|
32
|
-
* @remarks
|
|
33
|
-
* Registered last, this replaces whichever auth module the configurator already
|
|
34
|
-
* carries, so it works on a `FrameworkConfigurator` that pre-registers the real one.
|
|
35
|
-
*
|
|
36
|
-
* @param configurator - The modules configurator to register on.
|
|
37
|
-
* @param configure - Optional callback to override the default mock client.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```typescript
|
|
41
|
-
* enableMsalMock(configurator, (builder) => {
|
|
42
|
-
* builder.setAccount({ name: 'Ada Lovelace' });
|
|
43
|
-
* });
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export const enableMsalMock = (
|
|
47
|
-
// biome-ignore lint/suspicious/noExplicitAny: must be any to support all module types
|
|
48
|
-
configurator: IModulesConfigurator<any, any>,
|
|
49
|
-
configure?: AuthConfigMockFn,
|
|
50
|
-
): void => {
|
|
51
|
-
configurator.addConfig({ module: msalMockModule, configure } as {
|
|
52
|
-
module: MsalModule;
|
|
53
|
-
});
|
|
54
|
-
};
|