@axa-fr/oidc-client 7.27.8 → 7.27.12
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/README.md +30 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1262 -1185
- package/dist/index.umd.cjs +2 -2
- package/dist/initSession.d.ts +1 -1
- package/dist/initSession.d.ts.map +1 -1
- package/dist/initWorker.d.ts +22 -0
- package/dist/initWorker.d.ts.map +1 -1
- package/dist/oidcClient.d.ts +14 -0
- package/dist/oidcClient.d.ts.map +1 -1
- package/dist/protocol.d.ts +75 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.spec.d.ts +2 -0
- package/dist/protocol.spec.d.ts.map +1 -0
- package/dist/signalServiceWorker.spec.d.ts +2 -0
- package/dist/signalServiceWorker.spec.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +20 -0
- package/src/initSession.spec.ts +65 -0
- package/src/initSession.ts +70 -24
- package/src/initWorker.ts +72 -3
- package/src/oidcClient.ts +29 -0
- package/src/protocol.spec.ts +96 -0
- package/src/protocol.ts +106 -0
- package/src/signalServiceWorker.spec.ts +77 -0
- package/src/version.ts +1 -1
package/src/initSession.ts
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
// Guarded writes to storage. Assigning `undefined` or `null` through bracket
|
|
2
|
+
// notation (or `setItem`) coerces the value to the literal strings
|
|
3
|
+
// `"undefined"` / `"null"`, which then poison the next `JSON.parse` read.
|
|
4
|
+
// See https://github.com/AxaFrance/oidc-client/issues/1257 (and #871, #1274).
|
|
5
|
+
const writeJson = (storage: Storage, key: string, value: unknown) => {
|
|
6
|
+
if (value === undefined || value === null) {
|
|
7
|
+
delete storage[key];
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
storage[key] = JSON.stringify(value);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const writeRaw = (storage: Storage, key: string, value: string | null | undefined) => {
|
|
14
|
+
if (value === undefined || value === null) {
|
|
15
|
+
delete storage[key];
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
storage[key] = value;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const parseJsonOrNull = <T = unknown>(raw: unknown): T | null => {
|
|
22
|
+
if (typeof raw !== 'string') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
// Defence in depth against pre-existing poisoned values written by older
|
|
26
|
+
// versions of this library before the setter guards above were in place.
|
|
27
|
+
if (raw === 'undefined' || raw === 'null' || raw === '') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(raw) as T;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
1
37
|
export const initSession = (
|
|
2
38
|
configurationName,
|
|
3
39
|
storage = sessionStorage,
|
|
@@ -6,7 +42,7 @@ export const initSession = (
|
|
|
6
42
|
const loginStorage = loginStateStorage ?? storage;
|
|
7
43
|
|
|
8
44
|
const clearAsync = status => {
|
|
9
|
-
storage
|
|
45
|
+
writeJson(storage, `oidc.${configurationName}`, { tokens: null, status });
|
|
10
46
|
delete storage[`oidc.${configurationName}.userInfo`];
|
|
11
47
|
if (loginStateStorage && loginStateStorage !== storage) {
|
|
12
48
|
delete loginStorage[`oidc.login.${configurationName}`];
|
|
@@ -18,20 +54,23 @@ export const initSession = (
|
|
|
18
54
|
};
|
|
19
55
|
|
|
20
56
|
const initAsync = async () => {
|
|
21
|
-
|
|
22
|
-
|
|
57
|
+
const existing = parseJsonOrNull(storage[`oidc.${configurationName}`]) as {
|
|
58
|
+
tokens: any;
|
|
59
|
+
status: any;
|
|
60
|
+
} | null;
|
|
61
|
+
if (!existing) {
|
|
62
|
+
writeJson(storage, `oidc.${configurationName}`, { tokens: null, status: null });
|
|
23
63
|
return { tokens: null, status: null };
|
|
24
64
|
}
|
|
25
|
-
|
|
26
|
-
return Promise.resolve({ tokens: data.tokens, status: data.status });
|
|
65
|
+
return Promise.resolve({ tokens: existing.tokens, status: existing.status });
|
|
27
66
|
};
|
|
28
67
|
|
|
29
68
|
const setTokens = tokens => {
|
|
30
|
-
storage
|
|
69
|
+
writeJson(storage, `oidc.${configurationName}`, { tokens });
|
|
31
70
|
};
|
|
32
71
|
|
|
33
72
|
const setSessionStateAsync = async sessionState => {
|
|
34
|
-
storage
|
|
73
|
+
writeRaw(storage, `oidc.session_state.${configurationName}`, sessionState);
|
|
35
74
|
};
|
|
36
75
|
|
|
37
76
|
const getSessionStateAsync = async () => {
|
|
@@ -39,15 +78,15 @@ export const initSession = (
|
|
|
39
78
|
};
|
|
40
79
|
|
|
41
80
|
const setNonceAsync = nonce => {
|
|
42
|
-
loginStorage
|
|
81
|
+
writeRaw(loginStorage, `oidc.nonce.${configurationName}`, nonce?.nonce);
|
|
43
82
|
};
|
|
44
83
|
|
|
45
84
|
const setDemonstratingProofOfPossessionJwkAsync = (jwk: JsonWebKey) => {
|
|
46
|
-
storage
|
|
85
|
+
writeJson(storage, `oidc.jwk.${configurationName}`, jwk);
|
|
47
86
|
};
|
|
48
87
|
|
|
49
88
|
const getDemonstratingProofOfPossessionJwkAsync = () => {
|
|
50
|
-
return
|
|
89
|
+
return parseJsonOrNull<JsonWebKey>(storage[`oidc.jwk.${configurationName}`]);
|
|
51
90
|
};
|
|
52
91
|
|
|
53
92
|
const getNonceAsync = async () => {
|
|
@@ -56,7 +95,7 @@ export const initSession = (
|
|
|
56
95
|
};
|
|
57
96
|
|
|
58
97
|
const setDemonstratingProofOfPossessionNonce = async (dpopNonce: string) => {
|
|
59
|
-
storage
|
|
98
|
+
writeRaw(storage, `oidc.dpop_nonce.${configurationName}`, dpopNonce);
|
|
60
99
|
};
|
|
61
100
|
|
|
62
101
|
const getDemonstratingProofOfPossessionNonce = (): string => {
|
|
@@ -64,31 +103,38 @@ export const initSession = (
|
|
|
64
103
|
};
|
|
65
104
|
|
|
66
105
|
const getTokens = () => {
|
|
67
|
-
|
|
106
|
+
const parsed = parseJsonOrNull(storage[`oidc.${configurationName}`]) as {
|
|
107
|
+
tokens: any;
|
|
108
|
+
} | null;
|
|
109
|
+
if (!parsed) {
|
|
68
110
|
return null;
|
|
69
111
|
}
|
|
70
|
-
return JSON.stringify({ tokens:
|
|
112
|
+
return JSON.stringify({ tokens: parsed.tokens });
|
|
71
113
|
};
|
|
72
114
|
|
|
73
115
|
const getLoginParamsCache = {};
|
|
74
116
|
const setLoginParams = data => {
|
|
117
|
+
if (data === undefined || data === null) {
|
|
118
|
+
delete getLoginParamsCache[configurationName];
|
|
119
|
+
delete loginStorage[`oidc.login.${configurationName}`];
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
75
122
|
getLoginParamsCache[configurationName] = data;
|
|
76
|
-
loginStorage
|
|
123
|
+
writeJson(loginStorage, `oidc.login.${configurationName}`, data);
|
|
77
124
|
};
|
|
78
125
|
const getLoginParams = () => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
126
|
+
if (getLoginParamsCache[configurationName]) {
|
|
127
|
+
return getLoginParamsCache[configurationName];
|
|
128
|
+
}
|
|
129
|
+
const parsed = parseJsonOrNull(loginStorage[`oidc.login.${configurationName}`]);
|
|
130
|
+
if (parsed === null) {
|
|
82
131
|
console.warn(
|
|
83
132
|
`storage[oidc.login.${configurationName}] is empty, you should have an bad OIDC or code configuration somewhere.`,
|
|
84
133
|
);
|
|
85
134
|
return null;
|
|
86
135
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
getLoginParamsCache[configurationName] = JSON.parse(dataString);
|
|
90
|
-
}
|
|
91
|
-
return getLoginParamsCache[configurationName];
|
|
136
|
+
getLoginParamsCache[configurationName] = parsed;
|
|
137
|
+
return parsed;
|
|
92
138
|
};
|
|
93
139
|
|
|
94
140
|
const getStateAsync = async () => {
|
|
@@ -96,7 +142,7 @@ export const initSession = (
|
|
|
96
142
|
};
|
|
97
143
|
|
|
98
144
|
const setStateAsync = async (state: string) => {
|
|
99
|
-
loginStorage
|
|
145
|
+
writeRaw(loginStorage, `oidc.state.${configurationName}`, state);
|
|
100
146
|
};
|
|
101
147
|
|
|
102
148
|
const getCodeVerifierAsync = async () => {
|
|
@@ -104,7 +150,7 @@ export const initSession = (
|
|
|
104
150
|
};
|
|
105
151
|
|
|
106
152
|
const setCodeVerifierAsync = async codeVerifier => {
|
|
107
|
-
loginStorage
|
|
153
|
+
writeRaw(loginStorage, `oidc.code_verifier.${configurationName}`, codeVerifier);
|
|
108
154
|
};
|
|
109
155
|
|
|
110
156
|
return {
|
package/src/initWorker.ts
CHANGED
|
@@ -4,6 +4,20 @@ import timer from './timer.js';
|
|
|
4
4
|
import { OidcConfiguration } from './types.js';
|
|
5
5
|
import codeVersion from './version.js';
|
|
6
6
|
|
|
7
|
+
export const DEFAULT_SW_MESSAGE_TIMEOUT_MS = 5000;
|
|
8
|
+
|
|
9
|
+
export interface ServiceWorkerSignalMessage {
|
|
10
|
+
type: string;
|
|
11
|
+
configurationName?: string;
|
|
12
|
+
data?: unknown;
|
|
13
|
+
tabId?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ServiceWorkerSignalOptions {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
let keepAliveServiceWorkerTimeoutId = null;
|
|
8
22
|
let keepAliveController: AbortController | undefined;
|
|
9
23
|
|
|
@@ -57,8 +71,6 @@ export const getTabId = (configurationName: string) => {
|
|
|
57
71
|
return newTabId;
|
|
58
72
|
};
|
|
59
73
|
|
|
60
|
-
const DEFAULT_SW_MESSAGE_TIMEOUT_MS = 5000;
|
|
61
|
-
|
|
62
74
|
const getServiceWorkerTarget = (registration: ServiceWorkerRegistration): ServiceWorker | null => {
|
|
63
75
|
return (
|
|
64
76
|
navigator.serviceWorker.controller ??
|
|
@@ -541,14 +553,34 @@ export const initWorkerAsync = async (
|
|
|
541
553
|
|
|
542
554
|
const getLoginParamsCache = {};
|
|
543
555
|
const setLoginParams = data => {
|
|
556
|
+
if (data === undefined || data === null) {
|
|
557
|
+
delete getLoginParamsCache[configurationName];
|
|
558
|
+
delete localStorage[`oidc.login.${configurationName}`];
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
544
561
|
getLoginParamsCache[configurationName] = data;
|
|
545
562
|
localStorage[`oidc.login.${configurationName}`] = JSON.stringify(data);
|
|
546
563
|
};
|
|
547
564
|
|
|
548
565
|
const getLoginParams = () => {
|
|
566
|
+
if (getLoginParamsCache[configurationName]) {
|
|
567
|
+
return getLoginParamsCache[configurationName];
|
|
568
|
+
}
|
|
549
569
|
const dataString = localStorage[`oidc.login.${configurationName}`];
|
|
550
|
-
|
|
570
|
+
// Guard against the literal strings "undefined" / "null" written by older
|
|
571
|
+
// builds of this library through bracket-notation assignment.
|
|
572
|
+
if (
|
|
573
|
+
typeof dataString !== 'string' ||
|
|
574
|
+
dataString === '' ||
|
|
575
|
+
dataString === 'undefined' ||
|
|
576
|
+
dataString === 'null'
|
|
577
|
+
) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
551
581
|
getLoginParamsCache[configurationName] = JSON.parse(dataString);
|
|
582
|
+
} catch {
|
|
583
|
+
return null;
|
|
552
584
|
}
|
|
553
585
|
return getLoginParamsCache[configurationName];
|
|
554
586
|
};
|
|
@@ -653,6 +685,18 @@ export const initWorkerAsync = async (
|
|
|
653
685
|
});
|
|
654
686
|
};
|
|
655
687
|
|
|
688
|
+
const signalAsync = (
|
|
689
|
+
message: ServiceWorkerSignalMessage,
|
|
690
|
+
options?: ServiceWorkerSignalOptions,
|
|
691
|
+
): Promise<any> =>
|
|
692
|
+
sendMessageAsync(
|
|
693
|
+
registration,
|
|
694
|
+
options,
|
|
695
|
+
)({
|
|
696
|
+
...message,
|
|
697
|
+
configurationName: message.configurationName ?? configurationName,
|
|
698
|
+
});
|
|
699
|
+
|
|
656
700
|
return {
|
|
657
701
|
clearAsync,
|
|
658
702
|
initAsync,
|
|
@@ -672,5 +716,30 @@ export const initWorkerAsync = async (
|
|
|
672
716
|
getDemonstratingProofOfPossessionNonce,
|
|
673
717
|
setDemonstratingProofOfPossessionJwkAsync,
|
|
674
718
|
getDemonstratingProofOfPossessionJwkAsync,
|
|
719
|
+
signalAsync,
|
|
675
720
|
};
|
|
676
721
|
};
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Sends a typed message to the OIDC service worker for the given
|
|
725
|
+
* configuration. Wraps `MessageChannel` setup, request/response correlation
|
|
726
|
+
* and timeouts.
|
|
727
|
+
*
|
|
728
|
+
* Resolves with the SW response, rejects on timeout or when no SW is
|
|
729
|
+
* registered for the configuration. The provided `configurationName` is
|
|
730
|
+
* used when the message itself does not carry one.
|
|
731
|
+
*/
|
|
732
|
+
export const signalServiceWorkerAsync = async (
|
|
733
|
+
configuration: OidcConfiguration,
|
|
734
|
+
configurationName: string,
|
|
735
|
+
message: ServiceWorkerSignalMessage,
|
|
736
|
+
options?: ServiceWorkerSignalOptions,
|
|
737
|
+
): Promise<any> => {
|
|
738
|
+
const worker = await initWorkerAsync(configuration, configurationName);
|
|
739
|
+
if (!worker) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`signalServiceWorkerAsync: no service worker registered for configuration "${configurationName}"`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
return worker.signalAsync(message, options);
|
|
745
|
+
};
|
package/src/oidcClient.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { fetchWithTokens } from './fetch';
|
|
2
|
+
import {
|
|
3
|
+
ServiceWorkerSignalMessage,
|
|
4
|
+
ServiceWorkerSignalOptions,
|
|
5
|
+
signalServiceWorkerAsync,
|
|
6
|
+
} from './initWorker.js';
|
|
2
7
|
import { ILOidcLocation, OidcLocation } from './location';
|
|
3
8
|
import { LoginCallback, Oidc } from './oidc.js';
|
|
4
9
|
import { getValidTokenAsync, OidcToken, Tokens, ValidToken } from './parseTokens.js';
|
|
@@ -130,6 +135,30 @@ export class OidcClient {
|
|
|
130
135
|
userInfo<T extends OidcUserInfo = OidcUserInfo>(): T {
|
|
131
136
|
return this._oidc.userInfo;
|
|
132
137
|
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* High-level helper to send a message to the OIDC service worker.
|
|
141
|
+
*
|
|
142
|
+
* Wraps the low-level `postMessage` + `MessageChannel` plumbing and
|
|
143
|
+
* returns the response posted back by the worker. Use the typed message
|
|
144
|
+
* symbols exported from `@axa-fr/oidc-client-service-worker/protocol`
|
|
145
|
+
* (`ServiceWorkerMessageType`) to build messages.
|
|
146
|
+
*
|
|
147
|
+
* @throws if no service worker is registered for the current
|
|
148
|
+
* configuration, or if the worker does not respond before the timeout
|
|
149
|
+
* elapses.
|
|
150
|
+
*/
|
|
151
|
+
async signalServiceWorker<TResponse = unknown>(
|
|
152
|
+
message: ServiceWorkerSignalMessage,
|
|
153
|
+
options?: ServiceWorkerSignalOptions,
|
|
154
|
+
): Promise<TResponse> {
|
|
155
|
+
return signalServiceWorkerAsync(
|
|
156
|
+
this._oidc.configuration,
|
|
157
|
+
this._oidc.configurationName,
|
|
158
|
+
message,
|
|
159
|
+
options,
|
|
160
|
+
) as Promise<TResponse>;
|
|
161
|
+
}
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
export interface OidcUserInfo {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildDpopSecuredPlaceholder,
|
|
5
|
+
buildSecuredTokenPlaceholder,
|
|
6
|
+
buildStorageKey,
|
|
7
|
+
DPOP_TOKEN_PLACEHOLDER_PREFIX,
|
|
8
|
+
isServiceWorkerMessageType,
|
|
9
|
+
PROTOCOL_VERSION,
|
|
10
|
+
ServiceWorkerMessageType,
|
|
11
|
+
STORAGE_KEY_PREFIX,
|
|
12
|
+
SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY,
|
|
13
|
+
TOKEN_PLACEHOLDERS,
|
|
14
|
+
} from './protocol';
|
|
15
|
+
|
|
16
|
+
describe('public oidc-client protocol surface', () => {
|
|
17
|
+
it('exposes a stable PROTOCOL_VERSION', () => {
|
|
18
|
+
expect(PROTOCOL_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('matches the wire-level message types documented in PROTOCOL.md', () => {
|
|
22
|
+
expect(ServiceWorkerMessageType).toEqual({
|
|
23
|
+
SKIP_WAITING: 'SKIP_WAITING',
|
|
24
|
+
CLAIM: 'claim',
|
|
25
|
+
CLEAR: 'clear',
|
|
26
|
+
INIT: 'init',
|
|
27
|
+
SET_STATE: 'setState',
|
|
28
|
+
GET_STATE: 'getState',
|
|
29
|
+
SET_CODE_VERIFIER: 'setCodeVerifier',
|
|
30
|
+
GET_CODE_VERIFIER: 'getCodeVerifier',
|
|
31
|
+
SET_SESSION_STATE: 'setSessionState',
|
|
32
|
+
GET_SESSION_STATE: 'getSessionState',
|
|
33
|
+
SET_NONCE: 'setNonce',
|
|
34
|
+
GET_NONCE: 'getNonce',
|
|
35
|
+
SET_DPOP_NONCE: 'setDemonstratingProofOfPossessionNonce',
|
|
36
|
+
GET_DPOP_NONCE: 'getDemonstratingProofOfPossessionNonce',
|
|
37
|
+
SET_DPOP_JWK: 'setDemonstratingProofOfPossessionJwk',
|
|
38
|
+
GET_DPOP_JWK: 'getDemonstratingProofOfPossessionJwk',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('exposes the token placeholders the service worker emits', () => {
|
|
43
|
+
expect(TOKEN_PLACEHOLDERS).toEqual({
|
|
44
|
+
ACCESS_TOKEN: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
45
|
+
REFRESH_TOKEN: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
46
|
+
NONCE_TOKEN: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
47
|
+
CODE_VERIFIER: 'CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
48
|
+
});
|
|
49
|
+
expect(DPOP_TOKEN_PLACEHOLDER_PREFIX).toBe('DPOP_SECURED_BY_OIDC_SERVICE_WORKER');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('protocol helpers', () => {
|
|
54
|
+
it('builds secured token placeholders that match the SW output', () => {
|
|
55
|
+
expect(buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS.ACCESS_TOKEN, 'demo', 'tab-1')).toBe(
|
|
56
|
+
'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_demo#tabId=tab-1',
|
|
57
|
+
);
|
|
58
|
+
expect(buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS.NONCE_TOKEN, 'demo')).toBe(
|
|
59
|
+
'NONCE_SECURED_BY_OIDC_SERVICE_WORKER_demo#tabId=default',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('builds DPoP placeholders that match the SW output', () => {
|
|
64
|
+
expect(buildDpopSecuredPlaceholder('demo', 'tab-2')).toBe(
|
|
65
|
+
`${DPOP_TOKEN_PLACEHOLDER_PREFIX}_demo#tabId=tab-2`,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.each([
|
|
70
|
+
[STORAGE_KEY_PREFIX.STATE, 'demo', 'oidc.state.demo'],
|
|
71
|
+
[STORAGE_KEY_PREFIX.NONCE, 'demo', 'oidc.nonce.demo'],
|
|
72
|
+
[STORAGE_KEY_PREFIX.CODE_VERIFIER, 'demo', 'oidc.code_verifier.demo'],
|
|
73
|
+
[STORAGE_KEY_PREFIX.LOGIN_PARAMS, 'demo', 'oidc.login.demo'],
|
|
74
|
+
[STORAGE_KEY_PREFIX.TAB_ID, 'demo', 'oidc.tabId.demo'],
|
|
75
|
+
])('combines storage prefix %s + %s into %s', (prefix, configurationName, expected) => {
|
|
76
|
+
expect(buildStorageKey(prefix, configurationName)).toBe(expected);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('exposes the SW controllerchange reload counter key', () => {
|
|
80
|
+
expect(SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY).toBe('oidc.sw.controllerchange_reload_count');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it.each(Object.values(ServiceWorkerMessageType))(
|
|
84
|
+
'recognises "%s" as a known message type',
|
|
85
|
+
type => {
|
|
86
|
+
expect(isServiceWorkerMessageType(type)).toBe(true);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
it.each(['unknown', '', 42, null, undefined, {}])(
|
|
91
|
+
'rejects %p as not a known message type',
|
|
92
|
+
value => {
|
|
93
|
+
expect(isServiceWorkerMessageType(value)).toBe(false);
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
});
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, supported entry point for the OIDC service worker `postMessage`
|
|
3
|
+
* protocol, available directly from `@axa-fr/oidc-client`.
|
|
4
|
+
*
|
|
5
|
+
* The same exports are also published from
|
|
6
|
+
* `@axa-fr/oidc-client-service-worker/protocol`. The two modules are kept
|
|
7
|
+
* deliberately in sync (and verified by a unit test) so applications that
|
|
8
|
+
* depend only on `@axa-fr/oidc-client` can interact with the service worker
|
|
9
|
+
* without adding a transitive dependency.
|
|
10
|
+
*
|
|
11
|
+
* See `packages/oidc-client-service-worker/PROTOCOL.md` for the full
|
|
12
|
+
* specification.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Semver-protected version of the service worker `postMessage` protocol.
|
|
17
|
+
*/
|
|
18
|
+
export const PROTOCOL_VERSION = '1.0.0' as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every supported service worker message type. The values are also the
|
|
22
|
+
* literal strings used on the wire as `MessageEventData.type` and must
|
|
23
|
+
* remain stable across patch and minor versions of the protocol.
|
|
24
|
+
*/
|
|
25
|
+
export const ServiceWorkerMessageType = {
|
|
26
|
+
SKIP_WAITING: 'SKIP_WAITING',
|
|
27
|
+
CLAIM: 'claim',
|
|
28
|
+
CLEAR: 'clear',
|
|
29
|
+
INIT: 'init',
|
|
30
|
+
SET_STATE: 'setState',
|
|
31
|
+
GET_STATE: 'getState',
|
|
32
|
+
SET_CODE_VERIFIER: 'setCodeVerifier',
|
|
33
|
+
GET_CODE_VERIFIER: 'getCodeVerifier',
|
|
34
|
+
SET_SESSION_STATE: 'setSessionState',
|
|
35
|
+
GET_SESSION_STATE: 'getSessionState',
|
|
36
|
+
SET_NONCE: 'setNonce',
|
|
37
|
+
GET_NONCE: 'getNonce',
|
|
38
|
+
SET_DPOP_NONCE: 'setDemonstratingProofOfPossessionNonce',
|
|
39
|
+
GET_DPOP_NONCE: 'getDemonstratingProofOfPossessionNonce',
|
|
40
|
+
SET_DPOP_JWK: 'setDemonstratingProofOfPossessionJwk',
|
|
41
|
+
GET_DPOP_JWK: 'getDemonstratingProofOfPossessionJwk',
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
export type ServiceWorkerMessageTypeKey = keyof typeof ServiceWorkerMessageType;
|
|
45
|
+
export type ServiceWorkerMessageTypeValue =
|
|
46
|
+
(typeof ServiceWorkerMessageType)[ServiceWorkerMessageTypeKey];
|
|
47
|
+
|
|
48
|
+
export interface ServiceWorkerMessage<TData = unknown> {
|
|
49
|
+
type: ServiceWorkerMessageTypeValue | 'SKIP_WAITING' | 'claim';
|
|
50
|
+
configurationName: string;
|
|
51
|
+
data: TData;
|
|
52
|
+
tabId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ServiceWorkerResponse {
|
|
56
|
+
configurationName?: string;
|
|
57
|
+
error?: unknown;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Stable internal token placeholders – matched by the SW request handler. */
|
|
62
|
+
export const TOKEN_PLACEHOLDERS = {
|
|
63
|
+
ACCESS_TOKEN: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
64
|
+
REFRESH_TOKEN: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
65
|
+
NONCE_TOKEN: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
66
|
+
CODE_VERIFIER: 'CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
export const DPOP_TOKEN_PLACEHOLDER_PREFIX = 'DPOP_SECURED_BY_OIDC_SERVICE_WORKER' as const;
|
|
70
|
+
|
|
71
|
+
export const STORAGE_KEY_PREFIX = {
|
|
72
|
+
TAB_ID: 'oidc.tabId.',
|
|
73
|
+
STATE: 'oidc.state.',
|
|
74
|
+
NONCE: 'oidc.nonce.',
|
|
75
|
+
CODE_VERIFIER: 'oidc.code_verifier.',
|
|
76
|
+
LOGIN_PARAMS: 'oidc.login.',
|
|
77
|
+
SW_VERSION_MISMATCH_RELOAD: 'oidc.sw.version_mismatch_reload.',
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
export const SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY =
|
|
81
|
+
'oidc.sw.controllerchange_reload_count' as const;
|
|
82
|
+
|
|
83
|
+
export const buildStorageKey = (
|
|
84
|
+
prefix: (typeof STORAGE_KEY_PREFIX)[keyof typeof STORAGE_KEY_PREFIX],
|
|
85
|
+
configurationName: string,
|
|
86
|
+
): string => `${prefix}${configurationName}`;
|
|
87
|
+
|
|
88
|
+
export const buildSecuredTokenPlaceholder = (
|
|
89
|
+
placeholder: (typeof TOKEN_PLACEHOLDERS)[keyof typeof TOKEN_PLACEHOLDERS],
|
|
90
|
+
configurationName: string,
|
|
91
|
+
tabId: string = 'default',
|
|
92
|
+
): string => `${placeholder}_${configurationName}#tabId=${tabId}`;
|
|
93
|
+
|
|
94
|
+
export const buildDpopSecuredPlaceholder = (
|
|
95
|
+
configurationName: string,
|
|
96
|
+
tabId: string = 'default',
|
|
97
|
+
): string => `${DPOP_TOKEN_PLACEHOLDER_PREFIX}_${configurationName}#tabId=${tabId}`;
|
|
98
|
+
|
|
99
|
+
export const isServiceWorkerMessageType = (
|
|
100
|
+
value: unknown,
|
|
101
|
+
): value is ServiceWorkerMessageTypeValue => {
|
|
102
|
+
if (typeof value !== 'string') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return Object.values(ServiceWorkerMessageType).includes(value as ServiceWorkerMessageTypeValue);
|
|
106
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import * as initWorker from './initWorker';
|
|
4
|
+
import { OidcClient } from './oidcClient';
|
|
5
|
+
import { ServiceWorkerMessageType } from './protocol';
|
|
6
|
+
|
|
7
|
+
const buildClient = (overrides: Partial<{ configurationName: string }> = {}) => {
|
|
8
|
+
const oidc = {
|
|
9
|
+
configuration: { client_id: 'demo-client' },
|
|
10
|
+
configurationName: overrides.configurationName ?? 'demo',
|
|
11
|
+
};
|
|
12
|
+
return new OidcClient(oidc as never);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('OidcClient.signalServiceWorker', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('forwards the typed message to signalServiceWorkerAsync with the OIDC config', async () => {
|
|
21
|
+
const expected = { configurationName: 'demo', state: 'restored-state' };
|
|
22
|
+
const spy = vi
|
|
23
|
+
.spyOn(initWorker, 'signalServiceWorkerAsync')
|
|
24
|
+
.mockResolvedValue(expected as never);
|
|
25
|
+
|
|
26
|
+
const client = buildClient();
|
|
27
|
+
const response = await client.signalServiceWorker<{ state: string }>({
|
|
28
|
+
type: ServiceWorkerMessageType.GET_STATE,
|
|
29
|
+
configurationName: 'demo',
|
|
30
|
+
data: null,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(response).toEqual(expected);
|
|
34
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
35
|
+
expect(spy).toHaveBeenCalledWith(
|
|
36
|
+
{ client_id: 'demo-client' },
|
|
37
|
+
'demo',
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
type: 'getState',
|
|
40
|
+
data: null,
|
|
41
|
+
}),
|
|
42
|
+
undefined,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('propagates a custom timeout option', async () => {
|
|
47
|
+
const spy = vi.spyOn(initWorker, 'signalServiceWorkerAsync').mockResolvedValue({} as never);
|
|
48
|
+
|
|
49
|
+
const client = buildClient();
|
|
50
|
+
await client.signalServiceWorker(
|
|
51
|
+
{ type: ServiceWorkerMessageType.CLEAR, configurationName: 'demo', data: { status: null } },
|
|
52
|
+
{ timeoutMs: 9000 },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(spy).toHaveBeenCalledWith(
|
|
56
|
+
expect.anything(),
|
|
57
|
+
'demo',
|
|
58
|
+
expect.objectContaining({ type: 'clear' }),
|
|
59
|
+
{ timeoutMs: 9000 },
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rejects when the underlying helper rejects', async () => {
|
|
64
|
+
const error = new Error('no SW');
|
|
65
|
+
vi.spyOn(initWorker, 'signalServiceWorkerAsync').mockRejectedValue(error);
|
|
66
|
+
|
|
67
|
+
const client = buildClient();
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
client.signalServiceWorker({
|
|
71
|
+
type: ServiceWorkerMessageType.GET_STATE,
|
|
72
|
+
configurationName: 'demo',
|
|
73
|
+
data: null,
|
|
74
|
+
}),
|
|
75
|
+
).rejects.toBe(error);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '7.27.
|
|
1
|
+
export default '7.27.12';
|