@axa-fr/oidc-client 7.27.11 → 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 +1162 -1122
- package/dist/index.umd.cjs +2 -2
- 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/package.json +2 -2
- package/src/index.ts +20 -0
- package/src/initWorker.ts +51 -2
- 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/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';
|