@equinor/fusion-framework-module-msal 11.0.0-next.0 → 11.0.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 +67 -120
- package/dist/esm/__tests__/mock/create-mock-user-from-token.test.js +39 -0
- package/dist/esm/__tests__/mock/create-mock-user-from-token.test.js.map +1 -0
- package/dist/esm/__tests__/mock/msal-mock.test.js +53 -1
- package/dist/esm/__tests__/mock/msal-mock.test.js.map +1 -1
- package/dist/esm/mock/MsalMockClient.js +25 -8
- package/dist/esm/mock/MsalMockClient.js.map +1 -1
- package/dist/esm/mock/MsalMockConfigurator.js +63 -11
- package/dist/esm/mock/MsalMockConfigurator.js.map +1 -1
- package/dist/esm/mock/create-mock-user-from-token.js +40 -0
- package/dist/esm/mock/create-mock-user-from-token.js.map +1 -0
- package/dist/esm/mock/index.js +1 -0
- package/dist/esm/mock/index.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/__tests__/mock/create-mock-user-from-token.test.d.ts +1 -0
- package/dist/types/mock/MsalMockClient.d.ts +12 -0
- package/dist/types/mock/MsalMockConfigurator.d.ts +30 -0
- package/dist/types/mock/create-mock-user-from-token.d.ts +25 -0
- package/dist/types/mock/index.d.ts +1 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/testing.md +24 -0
- package/package.json +5 -5
- package/src/__tests__/mock/create-mock-user-from-token.test.ts +46 -0
- package/src/__tests__/mock/msal-mock.test.ts +69 -0
- package/src/mock/MsalMockClient.ts +27 -8
- package/src/mock/MsalMockConfigurator.ts +78 -14
- package/src/mock/create-mock-user-from-token.ts +46 -0
- package/src/mock/index.ts +1 -0
- package/src/version.ts +1 -1
- package/.changeset/msal-auth-provider-fix.md +0 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -267,4 +267,16 @@ export declare class MsalMockClient implements IMsalClient {
|
|
|
267
267
|
* @param user - The user to sign in, or `null` when nobody is.
|
|
268
268
|
*/
|
|
269
269
|
setUser(user: MsalMockUser | null): void;
|
|
270
|
+
/**
|
|
271
|
+
* Overrides the token returned by future results, independent of who is signed in.
|
|
272
|
+
*
|
|
273
|
+
* @remarks
|
|
274
|
+
* Use this when a backend mock validates its own tokens (specific claims, an
|
|
275
|
+
* audience, or a signature) — supplying the exact token here means that
|
|
276
|
+
* backend sees the token it issued, rather than a mock-shaped substitute this
|
|
277
|
+
* client would otherwise fabricate from the signed-in user's fields.
|
|
278
|
+
*
|
|
279
|
+
* @param token - The token to return verbatim, or `null` to resume generating one.
|
|
280
|
+
*/
|
|
281
|
+
setToken(token: string | null): void;
|
|
270
282
|
}
|
|
@@ -23,6 +23,11 @@ declare module '../msal-config-schema' {
|
|
|
23
23
|
* `null` when nobody is signed in.
|
|
24
24
|
*/
|
|
25
25
|
account?: MsalMockUser | null;
|
|
26
|
+
/**
|
|
27
|
+
* The token to return verbatim instead of one generated from the
|
|
28
|
+
* signed-in user's fields.
|
|
29
|
+
*/
|
|
30
|
+
token?: string;
|
|
26
31
|
};
|
|
27
32
|
}
|
|
28
33
|
}
|
|
@@ -105,6 +110,31 @@ export declare class MsalMockConfigurator extends MsalConfigurator {
|
|
|
105
110
|
* ```
|
|
106
111
|
*/
|
|
107
112
|
setAccount(account: MsalMockUser | null | ConfigBuilderCallback<MsalMockUser | null>): this;
|
|
113
|
+
/**
|
|
114
|
+
* Declares the token to return, independent of who is signed in.
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* Use this when a backend mock validates its own tokens (specific claims, an
|
|
118
|
+
* audience, or a signature) — the client then returns this token verbatim
|
|
119
|
+
* instead of fabricating one from the signed-in user's fields.
|
|
120
|
+
*
|
|
121
|
+
* @param token - A JWT (e.g. from `createMockToken`, or issued by an external mock).
|
|
122
|
+
* @param skipResolve - When `true`, override only the token and leave an account
|
|
123
|
+
* declared through {@link setAccount} untouched. Defaults to `false`, which also signs
|
|
124
|
+
* in the user described by the token's claims, via {@link createMockUserFromToken}.
|
|
125
|
+
* @returns The builder, for chaining.
|
|
126
|
+
*
|
|
127
|
+
* @example Sign in as whoever the token names
|
|
128
|
+
* ```typescript
|
|
129
|
+
* builder.setToken(token);
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example Keep a separately declared account, but return this exact token
|
|
133
|
+
* ```typescript
|
|
134
|
+
* builder.setAccount({ name: 'Ada Lovelace' }).setToken(token, true);
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
setToken(token: string, skipResolve?: boolean): this;
|
|
108
138
|
/**
|
|
109
139
|
* Assembles the configuration, then signs the declared user in.
|
|
110
140
|
*
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MsalMockUser } from './MsalMockClient';
|
|
2
|
+
/**
|
|
3
|
+
* Derives a {@link MsalMockUser} from a JWT's payload claims, so a token minted
|
|
4
|
+
* outside this module (e.g. by a backend's own mock) can drive who the mock
|
|
5
|
+
* signs in as.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Maps the standard Entra ID claims Fusion applications read — `name`,
|
|
9
|
+
* `preferred_username`, `oid`, `tid`, `scp` — onto the matching
|
|
10
|
+
* {@link MsalMockUser} fields. Identity only: it does not affect which token
|
|
11
|
+
* the client returns — use {@link MsalMockConfigurator.setToken} for that.
|
|
12
|
+
*
|
|
13
|
+
* @param token - A JWT (e.g. from {@link createMockToken}, or issued by an
|
|
14
|
+
* external mock) with a base64url-encoded payload segment.
|
|
15
|
+
* @returns A mock user built from the token's claims.
|
|
16
|
+
* @throws When the token has no payload segment (`header.payload.signature`).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* enableMsalMock(configurator, (builder) => {
|
|
21
|
+
* builder.setAccount(createMockUserFromToken(token));
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const createMockUserFromToken: (token: string) => MsalMockUser;
|
|
@@ -26,4 +26,5 @@ export { createMsalMockClient } from './create-msal-mock-client';
|
|
|
26
26
|
export { MsalMockConfigurator } from './MsalMockConfigurator';
|
|
27
27
|
export { enableMsalMock, msalMockModule, type AuthConfigMockFn } from './module';
|
|
28
28
|
export { createMockToken, type MockTokenClaims } from './create-mock-token';
|
|
29
|
+
export { createMockUserFromToken } from './create-mock-user-from-token';
|
|
29
30
|
export { decodeJwtSegment } from './decode-jwt-segment';
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "11.0.0
|
|
1
|
+
export declare const version = "11.0.0";
|
package/docs/testing.md
CHANGED
|
@@ -69,6 +69,24 @@ The user is in place **before** `MsalProvider.initialize()` runs, so the provide
|
|
|
69
69
|
|
|
70
70
|
`setClient` replaces the client, but not the rule: the declared user is signed in on whichever client the module authenticates through, so a mock client supplied that way receives it too.
|
|
71
71
|
|
|
72
|
+
## Returning an exact token
|
|
73
|
+
|
|
74
|
+
Most tests only care who is signed in and let the mock fabricate a token from that user's fields. When a backend mock validates the token itself — specific claims, an audience, or a signature — it needs to see the exact token it expects instead:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
enableMsalMock(configurator, (builder) => {
|
|
78
|
+
builder.setToken(token);
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`setToken` also signs in the user the token's claims describe, via `createMockUserFromToken` — so `acquireAccessToken` returns this token, and the account APIs agree with it. Pass `true` as the second argument to keep a separately declared account instead:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
enableMsalMock(configurator, (builder) => {
|
|
86
|
+
builder.setAccount({ name: 'Ada Lovelace' }).setToken(token, true);
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
72
90
|
| Option | Default | Purpose |
|
|
73
91
|
| --- | --- | --- |
|
|
74
92
|
| `name` | `Test User` | Display name |
|
|
@@ -150,6 +168,12 @@ import { createMockToken } from '@equinor/fusion-framework-module-msal/mock';
|
|
|
150
168
|
const token = createMockToken({ oid: 'fusion-mock-user' });
|
|
151
169
|
```
|
|
152
170
|
|
|
171
|
+
To use a generated token as the signed-in user for `ffc app dev --mock`, `ffc app serve --mock`,
|
|
172
|
+
or a Vite SPA, see
|
|
173
|
+
[Generate a mock user and update `.env`](../../../vite-plugins/spa/README.md#generate-a-mock-user-and-update-env).
|
|
174
|
+
That workflow documents persistent mock-auth configuration, identity claims, and custom `scp`
|
|
175
|
+
token scopes.
|
|
176
|
+
|
|
153
177
|
## Exports
|
|
154
178
|
|
|
155
179
|
| Export | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-msal",
|
|
3
|
-
"version": "11.0.0
|
|
3
|
+
"version": "11.0.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",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"semver": "^7.7.4",
|
|
58
58
|
"typescript": "^7.0.2",
|
|
59
59
|
"zod": "^4.4.3",
|
|
60
|
-
"@equinor/fusion-framework-module": "^6.1.3
|
|
61
|
-
"@equinor/fusion-framework-module-telemetry": "^
|
|
60
|
+
"@equinor/fusion-framework-module": "^6.1.3",
|
|
61
|
+
"@equinor/fusion-framework-module-telemetry": "^7.1.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@types/semver": "^7.0.0",
|
|
65
65
|
"semver": "^7.0.0",
|
|
66
66
|
"typescript": ">=5.0.0",
|
|
67
67
|
"zod": "^4.0.0",
|
|
68
|
-
"@equinor/fusion-framework-module": "^6.1.3
|
|
69
|
-
"@equinor/fusion-framework-module-telemetry": "^
|
|
68
|
+
"@equinor/fusion-framework-module": "^6.1.3",
|
|
69
|
+
"@equinor/fusion-framework-module-telemetry": "^7.1.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"@equinor/fusion-framework-module-telemetry": {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createMockToken } from '../../mock/create-mock-token';
|
|
4
|
+
import { createMockUserFromToken } from '../../mock/create-mock-user-from-token';
|
|
5
|
+
|
|
6
|
+
describe('createMockUserFromToken', () => {
|
|
7
|
+
it('maps identity claims onto the matching MsalMockUser fields', () => {
|
|
8
|
+
const token = createMockToken({
|
|
9
|
+
name: 'Ada Lovelace',
|
|
10
|
+
preferred_username: 'ada@equinor.com',
|
|
11
|
+
oid: 'ada-object-id',
|
|
12
|
+
tid: 'ada-tenant-id',
|
|
13
|
+
scp: 'User.Read Files.Read',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(createMockUserFromToken(token)).toEqual({
|
|
17
|
+
name: 'Ada Lovelace',
|
|
18
|
+
username: 'ada@equinor.com',
|
|
19
|
+
userId: 'ada-object-id',
|
|
20
|
+
tenantId: 'ada-tenant-id',
|
|
21
|
+
scopes: ['User.Read', 'Files.Read'],
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('leaves a field undefined rather than fabricating one, when a claim is absent', () => {
|
|
26
|
+
const token = createMockToken({
|
|
27
|
+
name: undefined,
|
|
28
|
+
preferred_username: undefined,
|
|
29
|
+
scp: undefined,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const user = createMockUserFromToken(token);
|
|
33
|
+
|
|
34
|
+
expect(user.name).toBeUndefined();
|
|
35
|
+
expect(user.username).toBeUndefined();
|
|
36
|
+
expect(user.scopes).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws for a token with no payload segment', () => {
|
|
40
|
+
expect(() => createMockUserFromToken('not-a-jwt')).toThrow(/payload segment/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('throws for an empty string', () => {
|
|
44
|
+
expect(() => createMockUserFromToken('')).toThrow(/payload segment/);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -12,6 +12,7 @@ import type { IMsalProvider } from '../../MsalProvider.interface';
|
|
|
12
12
|
import { enableMSAL, module as realModule } from '../../module';
|
|
13
13
|
import {
|
|
14
14
|
MsalMockClient,
|
|
15
|
+
createMockToken,
|
|
15
16
|
createMsalMockClient,
|
|
16
17
|
enableMsalMock,
|
|
17
18
|
msalMockModule,
|
|
@@ -425,6 +426,74 @@ describe('MsalMockClient', () => {
|
|
|
425
426
|
|
|
426
427
|
expect(result?.accessToken).toBe('mock-token');
|
|
427
428
|
});
|
|
429
|
+
|
|
430
|
+
it('returns a token set directly on the client verbatim', async () => {
|
|
431
|
+
const client = new MsalMockClient(clientConfig());
|
|
432
|
+
const token = createMockToken({ name: 'Direct Token' });
|
|
433
|
+
|
|
434
|
+
client.setToken(token);
|
|
435
|
+
const result = await client.acquireToken({ request: { scopes: ['X'] } });
|
|
436
|
+
|
|
437
|
+
expect(result?.accessToken).toBe(token);
|
|
438
|
+
expect(result?.idToken).toBe(token);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('resumes generating tokens once setToken(null) clears the override', async () => {
|
|
442
|
+
const client = new MsalMockClient(clientConfig());
|
|
443
|
+
const token = createMockToken({ name: 'Direct Token' });
|
|
444
|
+
client.setToken(token);
|
|
445
|
+
|
|
446
|
+
client.setToken(null);
|
|
447
|
+
const result = await client.acquireToken({ request: { scopes: ['X'] } });
|
|
448
|
+
|
|
449
|
+
expect(result?.accessToken).not.toBe(token);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('MsalMockConfigurator.setToken', () => {
|
|
454
|
+
it('returns the exact token instead of one generated from the account', async () => {
|
|
455
|
+
const token = createMockToken({ name: 'Token User' });
|
|
456
|
+
const provider = await initializeMockWith((builder) => builder.setToken(token));
|
|
457
|
+
|
|
458
|
+
const result = await provider.client.acquireToken({ request: { scopes: ['X'] } });
|
|
459
|
+
|
|
460
|
+
expect(result?.accessToken).toBe(token);
|
|
461
|
+
expect(result?.idToken).toBe(token);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('signs in the account named by the token claims by default', async () => {
|
|
465
|
+
const token = createMockToken({
|
|
466
|
+
name: 'Token User',
|
|
467
|
+
preferred_username: 'token.user@equinor.com',
|
|
468
|
+
});
|
|
469
|
+
const provider = await initializeMockWith((builder) => builder.setToken(token));
|
|
470
|
+
|
|
471
|
+
expect(provider.account?.name).toBe('Token User');
|
|
472
|
+
expect(provider.account?.username).toBe('token.user@equinor.com');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('keeps a separately declared account when skipResolve is true', async () => {
|
|
476
|
+
// skipResolve overrides only the returned token, leaving setAccount's declaration alone
|
|
477
|
+
const token = createMockToken({ name: 'Token User' });
|
|
478
|
+
const provider = await initializeMockWith((builder) => {
|
|
479
|
+
builder.setAccount({ name: 'Ada Lovelace' });
|
|
480
|
+
builder.setToken(token, true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(provider.account?.name).toBe('Ada Lovelace');
|
|
484
|
+
const result = await provider.client.acquireToken({ request: { scopes: ['X'] } });
|
|
485
|
+
expect(result?.accessToken).toBe(token);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('resumes generating tokens once setToken(null) is called on the client', async () => {
|
|
489
|
+
const token = createMockToken({ name: 'Token User' });
|
|
490
|
+
const provider = await initializeMockWith((builder) => builder.setToken(token));
|
|
491
|
+
|
|
492
|
+
(provider.client as MsalMockClient).setToken(null);
|
|
493
|
+
const result = await provider.client.acquireToken({ request: { scopes: ['X'] } });
|
|
494
|
+
|
|
495
|
+
expect(result?.accessToken).not.toBe(token);
|
|
496
|
+
});
|
|
428
497
|
});
|
|
429
498
|
|
|
430
499
|
describe('setAccount(null)', () => {
|
|
@@ -88,6 +88,7 @@ export class MsalMockClient implements IMsalClient {
|
|
|
88
88
|
};
|
|
89
89
|
#cache = new Map<string, AccountInfo>();
|
|
90
90
|
#activeAccountId: string | null = null;
|
|
91
|
+
#token: string | null = null;
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
94
|
* The account currently signed in, or `null`.
|
|
@@ -549,6 +550,21 @@ export class MsalMockClient implements IMsalClient {
|
|
|
549
550
|
this.#signIn(account ?? this.#createAccount());
|
|
550
551
|
}
|
|
551
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Overrides the token returned by future results, independent of who is signed in.
|
|
555
|
+
*
|
|
556
|
+
* @remarks
|
|
557
|
+
* Use this when a backend mock validates its own tokens (specific claims, an
|
|
558
|
+
* audience, or a signature) — supplying the exact token here means that
|
|
559
|
+
* backend sees the token it issued, rather than a mock-shaped substitute this
|
|
560
|
+
* client would otherwise fabricate from the signed-in user's fields.
|
|
561
|
+
*
|
|
562
|
+
* @param token - The token to return verbatim, or `null` to resume generating one.
|
|
563
|
+
*/
|
|
564
|
+
public setToken(token: string | null): void {
|
|
565
|
+
this.#token = token;
|
|
566
|
+
}
|
|
567
|
+
|
|
552
568
|
/**
|
|
553
569
|
* Creates the one account represented by this mock's configured identity.
|
|
554
570
|
* @returns An MSAL-shaped account for the configured user.
|
|
@@ -571,14 +587,17 @@ export class MsalMockClient implements IMsalClient {
|
|
|
571
587
|
*/
|
|
572
588
|
#createResult(scopes?: string[]): AuthenticationResult {
|
|
573
589
|
const granted = scopes?.length ? scopes : this.#user.scopes;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
590
|
+
// a caller-supplied token is sent verbatim so a backend mock validating it sees what it expects
|
|
591
|
+
const token =
|
|
592
|
+
this.#token ??
|
|
593
|
+
createMockToken({
|
|
594
|
+
name: this.#user.name,
|
|
595
|
+
preferred_username: this.#user.username,
|
|
596
|
+
oid: this.#user.userId,
|
|
597
|
+
tid: this.#user.tenantId,
|
|
598
|
+
aud: this.#user.clientId,
|
|
599
|
+
scp: granted.join(' '),
|
|
600
|
+
});
|
|
582
601
|
|
|
583
602
|
// Object shape matches AuthenticationResult's fields consumers rely on; the real
|
|
584
603
|
// type also carries browser-only fields (e.g. `familyId`) this mock intentionally omits
|
|
@@ -9,6 +9,7 @@ import type { MsalClientConfig } from '../MsalClient';
|
|
|
9
9
|
import { MsalConfigurator, type MsalConfig } from '../MsalConfigurator';
|
|
10
10
|
|
|
11
11
|
import { MsalMockClient, type MsalMockUser } from './MsalMockClient';
|
|
12
|
+
import { createMockUserFromToken } from './create-mock-user-from-token';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Declares the mock's own branch of the MSAL configuration.
|
|
@@ -31,6 +32,11 @@ declare module '../msal-config-schema' {
|
|
|
31
32
|
* `null` when nobody is signed in.
|
|
32
33
|
*/
|
|
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;
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
}
|
|
@@ -135,36 +141,86 @@ export class MsalMockConfigurator extends MsalConfigurator {
|
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
/**
|
|
138
|
-
*
|
|
144
|
+
* Declares the token to return, independent of who is signed in.
|
|
139
145
|
*
|
|
140
146
|
* @remarks
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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.
|
|
146
185
|
*
|
|
147
|
-
* @param account - The user to sign in, or `null` when nobody is.
|
|
148
186
|
* @param config - The validated configuration, carrying the client when one was built.
|
|
149
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.
|
|
150
190
|
* @throws When the resolved client is not a {@link MsalMockClient}.
|
|
151
191
|
*/
|
|
152
|
-
#
|
|
153
|
-
account: MsalMockUser | null,
|
|
192
|
+
#getClient(
|
|
154
193
|
config: MsalConfig,
|
|
155
|
-
init
|
|
156
|
-
|
|
194
|
+
init: ConfigBuilderCallbackArgs | undefined,
|
|
195
|
+
action: string,
|
|
196
|
+
): MsalMockClient {
|
|
157
197
|
const host = (init?.ref as { auth?: IMsalProvider } | undefined)?.auth;
|
|
158
198
|
const client = config.client ?? host?.client;
|
|
159
199
|
|
|
160
|
-
// Reject a real client because mock
|
|
200
|
+
// Reject a real client because mock state cannot be applied to it.
|
|
161
201
|
if (!(client instanceof MsalMockClient)) {
|
|
162
202
|
throw new Error(
|
|
163
|
-
|
|
203
|
+
`MsalMockConfigurator: cannot ${action}, because this module does not authenticate through a mock client. Declare it where that client is configured instead.`,
|
|
164
204
|
);
|
|
165
205
|
}
|
|
166
206
|
|
|
167
|
-
client
|
|
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);
|
|
168
224
|
}
|
|
169
225
|
|
|
170
226
|
/**
|
|
@@ -211,6 +267,14 @@ export class MsalMockConfigurator extends MsalConfigurator {
|
|
|
211
267
|
this.#signIn(account, config, init);
|
|
212
268
|
}
|
|
213
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
|
+
|
|
214
278
|
return config;
|
|
215
279
|
}
|
|
216
280
|
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
};
|
package/src/mock/index.ts
CHANGED
|
@@ -26,4 +26,5 @@ export { createMsalMockClient } from './create-msal-mock-client';
|
|
|
26
26
|
export { MsalMockConfigurator } from './MsalMockConfigurator';
|
|
27
27
|
export { enableMsalMock, msalMockModule, type AuthConfigMockFn } from './module';
|
|
28
28
|
export { createMockToken, type MockTokenClaims } from './create-mock-token';
|
|
29
|
+
export { createMockUserFromToken } from './create-mock-user-from-token';
|
|
29
30
|
export { decodeJwtSegment } from './decode-jwt-segment';
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '11.0.0
|
|
2
|
+
export const version = '11.0.0';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
"@equinor/fusion-framework-module-msal": patch
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Fixed AuthProvider to properly extend BaseModuleProvider, eliminating module initialization warnings about provider inheritance.
|
|
6
|
-
|
|
7
|
-
The AuthProvider class now inherits the standard version property and dispose method from BaseModuleProvider, ensuring proper integration with the Fusion Framework's module system.
|