@axa-fr/oidc-client 6.26.6

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.
Files changed (82) hide show
  1. package/README.md +209 -0
  2. package/bin/post-install.mjs +58 -0
  3. package/dist/OidcServiceWorker.js +561 -0
  4. package/dist/OidcTrustedDomains.js +27 -0
  5. package/dist/cache.d.ts +3 -0
  6. package/dist/cache.d.ts.map +1 -0
  7. package/dist/checkSession.d.ts +4 -0
  8. package/dist/checkSession.d.ts.map +1 -0
  9. package/dist/checkSessionIFrame.d.ts +17 -0
  10. package/dist/checkSessionIFrame.d.ts.map +1 -0
  11. package/dist/crypto.d.ts +4 -0
  12. package/dist/crypto.d.ts.map +1 -0
  13. package/dist/events.d.ts +29 -0
  14. package/dist/events.d.ts.map +1 -0
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +1236 -0
  18. package/dist/index.umd.cjs +2 -0
  19. package/dist/iniWorker.spec.d.ts +2 -0
  20. package/dist/iniWorker.spec.d.ts.map +1 -0
  21. package/dist/initSession.d.ts +22 -0
  22. package/dist/initSession.d.ts.map +1 -0
  23. package/dist/initWorker.d.ts +30 -0
  24. package/dist/initWorker.d.ts.map +1 -0
  25. package/dist/login.d.ts +8 -0
  26. package/dist/login.d.ts.map +1 -0
  27. package/dist/logout.d.ts +8 -0
  28. package/dist/logout.d.ts.map +1 -0
  29. package/dist/logout.spec.d.ts +1 -0
  30. package/dist/logout.spec.d.ts.map +1 -0
  31. package/dist/oidc.d.ts +101 -0
  32. package/dist/oidc.d.ts.map +1 -0
  33. package/dist/parseTokens.d.ts +37 -0
  34. package/dist/parseTokens.d.ts.map +1 -0
  35. package/dist/parseTokens.spec.d.ts +2 -0
  36. package/dist/parseTokens.spec.d.ts.map +1 -0
  37. package/dist/renewTokens.d.ts +4 -0
  38. package/dist/renewTokens.d.ts.map +1 -0
  39. package/dist/requests.d.ts +33 -0
  40. package/dist/requests.d.ts.map +1 -0
  41. package/dist/requests.spec.d.ts +2 -0
  42. package/dist/requests.spec.d.ts.map +1 -0
  43. package/dist/route-utils.d.ts +13 -0
  44. package/dist/route-utils.d.ts.map +1 -0
  45. package/dist/route-utils.spec.d.ts +2 -0
  46. package/dist/route-utils.spec.d.ts.map +1 -0
  47. package/dist/silentLogin.d.ts +10 -0
  48. package/dist/silentLogin.d.ts.map +1 -0
  49. package/dist/timer.d.ts +13 -0
  50. package/dist/timer.d.ts.map +1 -0
  51. package/dist/types.d.ts +38 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/user.d.ts +2 -0
  54. package/dist/user.d.ts.map +1 -0
  55. package/dist/vanillaOidc.d.ts +85 -0
  56. package/dist/vanillaOidc.d.ts.map +1 -0
  57. package/package.json +60 -0
  58. package/src/cache.ts +26 -0
  59. package/src/checkSession.ts +60 -0
  60. package/src/checkSessionIFrame.ts +83 -0
  61. package/src/crypto.ts +61 -0
  62. package/src/events.ts +28 -0
  63. package/src/index.ts +10 -0
  64. package/src/iniWorker.spec.ts +21 -0
  65. package/src/initSession.ts +89 -0
  66. package/src/initWorker.ts +321 -0
  67. package/src/login.ts +174 -0
  68. package/src/logout.spec.ts +65 -0
  69. package/src/logout.ts +101 -0
  70. package/src/oidc.ts +613 -0
  71. package/src/parseTokens.spec.ts +50 -0
  72. package/src/parseTokens.ts +194 -0
  73. package/src/renewTokens.ts +37 -0
  74. package/src/requests.spec.ts +9 -0
  75. package/src/requests.ts +169 -0
  76. package/src/route-utils.spec.ts +24 -0
  77. package/src/route-utils.ts +79 -0
  78. package/src/silentLogin.ts +144 -0
  79. package/src/timer.ts +163 -0
  80. package/src/types.ts +41 -0
  81. package/src/user.ts +40 -0
  82. package/src/vanillaOidc.ts +108 -0
@@ -0,0 +1,321 @@
1
+ import { parseOriginalTokens } from './parseTokens.js';
2
+ import timer from './timer.js';
3
+ import { OidcConfiguration } from './types.js';
4
+
5
+ export const getOperatingSystem = (navigator) => {
6
+ const nVer = navigator.appVersion;
7
+ const nAgt = navigator.userAgent;
8
+ const unknown = '-';
9
+ // system
10
+ let os = unknown;
11
+ const clientStrings = [
12
+ { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ },
13
+ { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ },
14
+ { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ },
15
+ { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ },
16
+ { s: 'Windows Vista', r: /Windows NT 6.0/ },
17
+ { s: 'Windows Server 2003', r: /Windows NT 5.2/ },
18
+ { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ },
19
+ { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ },
20
+ { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ },
21
+ { s: 'Windows 98', r: /(Windows 98|Win98)/ },
22
+ { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ },
23
+ { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ },
24
+ { s: 'Windows CE', r: /Windows CE/ },
25
+ { s: 'Windows 3.11', r: /Win16/ },
26
+ { s: 'Android', r: /Android/ },
27
+ { s: 'Open BSD', r: /OpenBSD/ },
28
+ { s: 'Sun OS', r: /SunOS/ },
29
+ { s: 'Chrome OS', r: /CrOS/ },
30
+ { s: 'Linux', r: /(Linux|X11(?!.*CrOS))/ },
31
+ { s: 'iOS', r: /(iPhone|iPad|iPod)/ },
32
+ { s: 'Mac OS X', r: /Mac OS X/ },
33
+ { s: 'Mac OS', r: /(Mac OS|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ },
34
+ { s: 'QNX', r: /QNX/ },
35
+ { s: 'UNIX', r: /UNIX/ },
36
+ { s: 'BeOS', r: /BeOS/ },
37
+ { s: 'OS/2', r: /OS\/2/ },
38
+ { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ },
39
+ ];
40
+ for (const id in clientStrings) {
41
+ const cs = clientStrings[id];
42
+ if (cs.r.test(nAgt)) {
43
+ os = cs.s;
44
+ break;
45
+ }
46
+ }
47
+
48
+ let osVersion = unknown;
49
+
50
+ if (/Windows/.test(os)) {
51
+ osVersion = /Windows (.*)/.exec(os)[1];
52
+ os = 'Windows';
53
+ }
54
+
55
+ switch (os) {
56
+ case 'Mac OS':
57
+ case 'Mac OS X':
58
+ case 'Android':
59
+ osVersion = /(?:Android|Mac OS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh) ([._\d]+)/.exec(nAgt)[1];
60
+ break;
61
+
62
+ case 'iOS': {
63
+ const osVersionArray = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer);
64
+ osVersion = osVersionArray[1] + '.' + osVersionArray[2] + '.' + (parseInt(osVersionArray[3]) | 0);
65
+ break;
66
+ }
67
+ }
68
+ return {
69
+ os,
70
+ osVersion,
71
+ };
72
+ };
73
+
74
+ function getBrowser() {
75
+ const ua = navigator.userAgent; let tem;
76
+ let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
77
+ if (/trident/i.test(M[1])) {
78
+ tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
79
+ return { name: 'ie', version: (tem[1] || '') };
80
+ }
81
+ if (M[1] === 'Chrome') {
82
+ tem = ua.match(/\bOPR|Edge\/(\d+)/);
83
+
84
+ if (tem != null) {
85
+ let version = tem[1];
86
+ if (!version) {
87
+ const splits = ua.split(tem[0] + '/');
88
+ if (splits.length > 1) {
89
+ version = splits[1];
90
+ }
91
+ }
92
+
93
+ return { name: 'opera', version };
94
+ }
95
+ }
96
+ M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
97
+ if ((tem = ua.match(/version\/(\d+)/i)) != null) { M.splice(1, 1, tem[1]); }
98
+ return {
99
+ name: M[0].toLowerCase(),
100
+ version: M[1],
101
+ };
102
+ }
103
+
104
+ let keepAliveServiceWorkerTimeoutId = null;
105
+
106
+ export const sleepAsync = (milliseconds) => {
107
+ return new Promise(resolve => timer.setTimeout(resolve, milliseconds));
108
+ };
109
+
110
+ const keepAlive = () => {
111
+ try {
112
+ const operatingSystem = getOperatingSystem(navigator);
113
+ const minSleepSeconds = operatingSystem.os === 'Android' ? 240 : 150;
114
+ const promise = fetch(`/OidcKeepAliveServiceWorker.json?minSleepSeconds=${minSleepSeconds}`);
115
+ promise.catch(error => { console.log(error); });
116
+ sleepAsync(minSleepSeconds * 1000).then(keepAlive);
117
+ } catch (error) { console.log(error); }
118
+ };
119
+
120
+ const isServiceWorkerProxyActiveAsync = () => {
121
+ return fetch('/OidcKeepAliveServiceWorker.json', {
122
+ headers: {
123
+ 'oidc-vanilla': 'true',
124
+ },
125
+ }).then((response) => {
126
+ return response.statusText === 'oidc-service-worker';
127
+ }).catch(error => { console.log(error); });
128
+ };
129
+
130
+ export const excludeOs = (operatingSystem) => {
131
+ if (operatingSystem.os === 'iOS' && operatingSystem.osVersion.startsWith('12')) {
132
+ return true;
133
+ }
134
+ if (operatingSystem.os === 'Mac OS X' && operatingSystem.osVersion.startsWith('10_15_6')) {
135
+ return true;
136
+ }
137
+ return false;
138
+ };
139
+
140
+ const sendMessageAsync = (registration) => (data) => {
141
+ return new Promise(function(resolve, reject) {
142
+ const messageChannel = new MessageChannel();
143
+ messageChannel.port1.onmessage = function (event) {
144
+ if (event.data && event.data.error) {
145
+ reject(event.data.error);
146
+ } else {
147
+ resolve(event.data);
148
+ }
149
+ };
150
+ registration.active.postMessage(data, [messageChannel.port2]);
151
+ });
152
+ };
153
+
154
+ export const initWorkerAsync = async(serviceWorkerRelativeUrl, configurationName) => {
155
+ if (typeof window === 'undefined' || typeof navigator === 'undefined' || !navigator.serviceWorker || !serviceWorkerRelativeUrl) {
156
+ return null;
157
+ }
158
+ const { name, version } = getBrowser();
159
+ if (name === 'chrome' && parseInt(version) < 90) {
160
+ return null;
161
+ }
162
+ if (name === 'opera') {
163
+ if (!version) {
164
+ return null;
165
+ }
166
+ if (parseInt(version.split('.')[0]) < 80) {
167
+ return null;
168
+ }
169
+ }
170
+ if (name === 'ie') {
171
+ return null;
172
+ }
173
+
174
+ const operatingSystem = getOperatingSystem(navigator);
175
+ if (excludeOs(operatingSystem)) {
176
+ return null;
177
+ }
178
+
179
+ const registration = await navigator.serviceWorker.register(serviceWorkerRelativeUrl);
180
+
181
+ try {
182
+ await navigator.serviceWorker.ready;
183
+ } catch (err) {
184
+ return null;
185
+ }
186
+
187
+ const unregisterAsync = async () => {
188
+ return await registration.unregister();
189
+ };
190
+
191
+ registration.addEventListener('updatefound', () => {
192
+ const newWorker = registration.installing;
193
+ newWorker.addEventListener('statechange', () => {
194
+ switch (newWorker.state) {
195
+ case 'installed':
196
+ if (navigator.serviceWorker.controller) {
197
+ registration.unregister().then(() => {
198
+ window.location.reload();
199
+ });
200
+ }
201
+ break;
202
+ }
203
+ });
204
+ });
205
+
206
+ const clearAsync = async (status) => {
207
+ return sendMessageAsync(registration)({ type: 'clear', data: { status }, configurationName });
208
+ };
209
+ const initAsync = async (oidcServerConfiguration, where, oidcConfiguration:OidcConfiguration) => {
210
+ const result = await sendMessageAsync(registration)({
211
+ type: 'init',
212
+ data: {
213
+ oidcServerConfiguration,
214
+ where,
215
+ oidcConfiguration: {
216
+ token_renew_mode: oidcConfiguration.token_renew_mode,
217
+ service_worker_convert_all_requests_to_cors: oidcConfiguration.service_worker_convert_all_requests_to_cors,
218
+ },
219
+ },
220
+ configurationName,
221
+ });
222
+ // @ts-ignore
223
+ return { tokens: parseOriginalTokens(result.tokens, null, oidcConfiguration.token_renew_mode), status: result.status };
224
+ };
225
+
226
+ const startKeepAliveServiceWorker = () => {
227
+ if (keepAliveServiceWorkerTimeoutId == null) {
228
+ keepAliveServiceWorkerTimeoutId = 'not_null';
229
+ keepAlive();
230
+ }
231
+ };
232
+
233
+ const setSessionStateAsync = (sessionState:string) => {
234
+ return sendMessageAsync(registration)({ type: 'setSessionState', data: { sessionState }, configurationName });
235
+ };
236
+
237
+ const getSessionStateAsync = async () => {
238
+ const result = await sendMessageAsync(registration)({ type: 'getSessionState', data: null, configurationName });
239
+ // @ts-ignore
240
+ return result.sessionState;
241
+ };
242
+
243
+ const setNonceAsync = (nonce) => {
244
+ sessionStorage['oidc.nonce'] = nonce.nonce;
245
+ return sendMessageAsync(registration)({ type: 'setNonce', data: { nonce }, configurationName });
246
+ };
247
+ const getNonceAsync = async () => {
248
+ // @ts-ignore
249
+ const result = await sendMessageAsync(registration)({ type: 'getNonce', data: null, configurationName });
250
+ // @ts-ignore
251
+ let nonce = result.nonce;
252
+ if (!nonce) {
253
+ nonce = sessionStorage['oidc.nonce'];
254
+ console.warn('nonce not found in service worker, using sessionStorage');
255
+ }
256
+ return { nonce };
257
+ };
258
+
259
+ let getLoginParamsCache = null;
260
+ const setLoginParams = (configurationName:string, data) => {
261
+ getLoginParamsCache = data;
262
+ localStorage[`oidc.login.${configurationName}`] = JSON.stringify(data);
263
+ };
264
+ const getLoginParams = (configurationName) => {
265
+ const dataString = localStorage[`oidc.login.${configurationName}`];
266
+ if (!getLoginParamsCache) {
267
+ getLoginParamsCache = JSON.parse(dataString);
268
+ }
269
+ return getLoginParamsCache;
270
+ };
271
+
272
+ const getStateAsync = async () => {
273
+ const result = await sendMessageAsync(registration)({ type: 'getState', data: null, configurationName });
274
+ // @ts-ignore
275
+ let state = result.state;
276
+ if (!state) {
277
+ state = sessionStorage[`oidc.state.${configurationName}`];
278
+ console.warn('state not found in service worker, using sessionStorage');
279
+ }
280
+ return state;
281
+ };
282
+
283
+ const setStateAsync = async (state:string) => {
284
+ sessionStorage[`oidc.state.${configurationName}`] = state;
285
+ return sendMessageAsync(registration)({ type: 'setState', data: { state }, configurationName });
286
+ };
287
+
288
+ const getCodeVerifierAsync = async () => {
289
+ const result = await sendMessageAsync(registration)({ type: 'getCodeVerifier', data: null, configurationName });
290
+ // @ts-ignore
291
+ let codeVerifier = result.codeVerifier;
292
+ if (!codeVerifier) {
293
+ codeVerifier = sessionStorage[`oidc.code_verifier.${configurationName}`];
294
+ console.warn('codeVerifier not found in service worker, using sessionStorage');
295
+ }
296
+ return codeVerifier;
297
+ };
298
+
299
+ const setCodeVerifierAsync = async (codeVerifier:string) => {
300
+ sessionStorage[`oidc.code_verifier.${configurationName}`] = codeVerifier;
301
+ return sendMessageAsync(registration)({ type: 'setCodeVerifier', data: { codeVerifier }, configurationName });
302
+ };
303
+
304
+ return {
305
+ clearAsync,
306
+ initAsync,
307
+ startKeepAliveServiceWorker,
308
+ isServiceWorkerProxyActiveAsync,
309
+ setSessionStateAsync,
310
+ getSessionStateAsync,
311
+ setNonceAsync,
312
+ getNonceAsync,
313
+ unregisterAsync,
314
+ setLoginParams,
315
+ getLoginParams,
316
+ getStateAsync,
317
+ setStateAsync,
318
+ getCodeVerifierAsync,
319
+ setCodeVerifierAsync,
320
+ };
321
+ };
package/src/login.ts ADDED
@@ -0,0 +1,174 @@
1
+ import { generateRandom } from './crypto.js';
2
+ import { eventNames } from './events.js';
3
+ import { initSession } from './initSession.js';
4
+ import { initWorkerAsync } from './initWorker.js';
5
+ import { isTokensOidcValid } from './parseTokens.js';
6
+ import { performAuthorizationRequestAsync, performFirstTokenRequestAsync } from './requests.js';
7
+ import { getParseQueryStringFromLocation } from './route-utils.js';
8
+ import { OidcConfiguration, StringMap } from './types.js';
9
+
10
+ // eslint-disable-next-line @typescript-eslint/ban-types
11
+ export const defaultLoginAsync = (window, configurationName, configuration:OidcConfiguration, publishEvent :(string, any)=>void, initAsync:Function) => (callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined) => {
12
+ const originExtras = extras;
13
+ extras = { ...extras };
14
+ const loginLocalAsync = async () => {
15
+ const location = window.location;
16
+ const url = callbackPath || location.pathname + (location.search || '') + (location.hash || '');
17
+
18
+ if (!('state' in extras)) {
19
+ extras.state = generateRandom(16);
20
+ }
21
+
22
+ publishEvent(eventNames.loginAsync_begin, {});
23
+ if (extras) {
24
+ for (const key of Object.keys(extras)) {
25
+ if (key.endsWith(':token_request')) {
26
+ delete extras[key];
27
+ }
28
+ }
29
+ }
30
+ try {
31
+ const redirectUri = isSilentSignin ? configuration.silent_redirect_uri : configuration.redirect_uri;
32
+ if (!scope) {
33
+ scope = configuration.scope;
34
+ }
35
+
36
+ const extraFinal = !configuration.extras ? extras : { ...configuration.extras, ...extras };
37
+ if (!extraFinal.nonce) {
38
+ extraFinal.nonce = generateRandom(12);
39
+ }
40
+ const nonce = { nonce: extraFinal.nonce };
41
+ const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, configurationName);
42
+ const oidcServerConfiguration = await initAsync(configuration.authority, configuration.authority_configuration);
43
+ let storage;
44
+ if (serviceWorker) {
45
+ serviceWorker.setLoginParams(configurationName, { callbackPath: url, extras: originExtras });
46
+ serviceWorker.startKeepAliveServiceWorker();
47
+ await serviceWorker.initAsync(oidcServerConfiguration, 'loginAsync', configuration);
48
+ await serviceWorker.setNonceAsync(nonce);
49
+ storage = serviceWorker;
50
+ } else {
51
+ const session = initSession(configurationName, configuration.storage ?? sessionStorage);
52
+ session.setLoginParams(configurationName, { callbackPath: url, extras: originExtras });
53
+ await session.setNonceAsync(nonce);
54
+ storage = session;
55
+ }
56
+
57
+ // @ts-ignore
58
+ const extraInternal = {
59
+ client_id: configuration.client_id,
60
+ redirect_uri: redirectUri,
61
+ scope,
62
+ response_type: 'code',
63
+ ...extraFinal,
64
+ };
65
+ await performAuthorizationRequestAsync(storage)(oidcServerConfiguration.authorizationEndpoint, extraInternal);
66
+ } catch (exception) {
67
+ publishEvent(eventNames.loginAsync_error, exception);
68
+ throw exception;
69
+ }
70
+ };
71
+ return loginLocalAsync();
72
+ };
73
+
74
+ export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => {
75
+ try {
76
+ oidc.publishEvent(eventNames.loginCallbackAsync_begin, {});
77
+ const configuration = oidc.configuration;
78
+ const clientId = configuration.client_id;
79
+ const redirectUri = isSilentSignin ? configuration.silent_redirect_uri : configuration.redirect_uri;
80
+ const authority = configuration.authority;
81
+ const tokenRequestTimeout = configuration.token_request_timeout;
82
+ const oidcServerConfiguration = await oidc.initAsync(authority, configuration.authority_configuration);
83
+ const queryParams = getParseQueryStringFromLocation(window.location.href);
84
+ const sessionState = queryParams.session_state;
85
+ const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, oidc.configurationName);
86
+ let storage;
87
+ let nonceData;
88
+ let getLoginParams;
89
+ let state;
90
+ if (serviceWorker) {
91
+ serviceWorker.startKeepAliveServiceWorker();
92
+ await serviceWorker.initAsync(oidcServerConfiguration, 'loginCallbackAsync', configuration);
93
+ await serviceWorker.setSessionStateAsync(sessionState);
94
+ nonceData = await serviceWorker.getNonceAsync();
95
+ getLoginParams = serviceWorker.getLoginParams(oidc.configurationName);
96
+ state = await serviceWorker.getStateAsync();
97
+ storage = serviceWorker;
98
+ } else {
99
+ const session = initSession(oidc.configurationName, configuration.storage ?? sessionStorage);
100
+ await session.setSessionStateAsync(sessionState);
101
+ nonceData = await session.getNonceAsync();
102
+ getLoginParams = session.getLoginParams(oidc.configurationName);
103
+ state = await session.getStateAsync();
104
+ storage = session;
105
+ }
106
+
107
+ const params = getParseQueryStringFromLocation(window.location.toString());
108
+
109
+ if (params.iss && params.iss !== oidcServerConfiguration.issuer) {
110
+ throw new Error('issuer not valid');
111
+ }
112
+ if (params.state && params.state !== state) {
113
+ throw new Error('state not valid');
114
+ }
115
+
116
+ const data = {
117
+ code: params.code,
118
+ grant_type: 'authorization_code',
119
+ client_id: configuration.client_id,
120
+ redirect_uri: redirectUri,
121
+ };
122
+
123
+ const extras = {};
124
+ // @ts-ignore
125
+ if (configuration.token_request_extras) {
126
+ for (const [key, value] of Object.entries(configuration.token_request_extras)) {
127
+ extras[key] = value;
128
+ }
129
+ }
130
+ if (getLoginParams && getLoginParams.extras) {
131
+ for (const [key, value] of Object.entries(getLoginParams.extras)) {
132
+ if (key.endsWith(':token_request')) {
133
+ extras[key.replace(':token_request', '')] = value;
134
+ }
135
+ }
136
+ }
137
+
138
+ const tokenResponse = await performFirstTokenRequestAsync(storage)(oidcServerConfiguration.tokenEndpoint, { ...data, ...extras }, oidc.configuration.token_renew_mode, tokenRequestTimeout);
139
+
140
+ if (!tokenResponse.success) {
141
+ throw new Error('Token request failed');
142
+ }
143
+
144
+ let loginParams;
145
+ const formattedTokens = tokenResponse.data.tokens;
146
+ if (serviceWorker) {
147
+ await serviceWorker.initAsync(redirectUri, 'syncTokensAsync', configuration);
148
+ loginParams = serviceWorker.getLoginParams(oidc.configurationName);
149
+ } else {
150
+ const session = initSession(oidc.configurationName, configuration.storage);
151
+ loginParams = session.getLoginParams(oidc.configurationName);
152
+ }
153
+ // @ts-ignore
154
+ if (tokenResponse.data.state !== extras.state) {
155
+ throw new Error('state is not valid');
156
+ }
157
+ const { isValid, reason } = isTokensOidcValid(formattedTokens, nonceData.nonce, oidcServerConfiguration);
158
+ if (!isValid) {
159
+ throw new Error(`Tokens are not OpenID valid, reason: ${reason}`);
160
+ }
161
+
162
+ await oidc.startCheckSessionAsync(oidcServerConfiguration.checkSessionIframe, clientId, sessionState, isSilentSignin);
163
+ oidc.publishEvent(eventNames.loginCallbackAsync_end, {});
164
+ return {
165
+ tokens: formattedTokens,
166
+ state: 'request.state',
167
+ callbackPath: loginParams.callbackPath,
168
+ };
169
+ } catch (exception) {
170
+ console.error(exception);
171
+ oidc.publishEvent(eventNames.loginCallbackAsync_error, exception);
172
+ throw exception;
173
+ }
174
+ };
@@ -0,0 +1,65 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { logoutAsync } from "./logout";
6
+
7
+ describe('Logout test suite', () => {
8
+
9
+ it.each([
10
+ {logout_tokens_to_invalidate:['access_token', 'refresh_token'], expectedResults: ["token=abcd&token_type_hint=access_token&client_id=interactive.public.short","token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short"]},
11
+ {logout_tokens_to_invalidate:['refresh_token'], expectedResults: ["token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short"]},
12
+ {logout_tokens_to_invalidate:['access_token'], expectedResults: ["token=abcd&token_type_hint=access_token&client_id=interactive.public.short"]},
13
+ {logout_tokens_to_invalidate:[], expectedResults: []},
14
+ ])('Logout should revoke tokens $logout_tokens_to_invalidate', async ({ logout_tokens_to_invalidate, expectedResults}) => {
15
+
16
+ const configuration = {
17
+ client_id: 'interactive.public.short',
18
+ redirect_uri: 'http://localhost:4200/authentication/callback',
19
+ scope: 'openid profile email api offline_access',
20
+ authority: 'http://api',
21
+ refresh_time_before_tokens_expiration_in_second: 70,
22
+ logout_tokens_to_invalidate,
23
+ };
24
+
25
+ const fetch = (url, data) => {
26
+ if(url === "http://api/connect/revocation") {
27
+ return Promise.resolve({status: 200});
28
+ }
29
+ return Promise.resolve({
30
+ status : 200,
31
+ });
32
+ };
33
+
34
+ const mockFetchFn = vi.fn().mockImplementation(fetch);
35
+
36
+ const oidc = {
37
+ configuration,
38
+ tokens : {idToken: "abcd", accessToken: "abcd", refreshToken: "abdc" },
39
+ initAsync: () => Promise.resolve({
40
+ revocationEndpoint: "http://api/connect/revocation",
41
+ endSessionEndpoint: "http://api/connect/endsession",
42
+ }),
43
+ destroyAsync: () => Promise.resolve(),
44
+ logoutSameTabAsync: () => Promise.resolve(),
45
+ };
46
+
47
+ const oidcDatabase = {default: () => oidc};
48
+
49
+ const window = {
50
+ location: {
51
+ href: "",
52
+ origin: "http://localhost:4200",
53
+ },
54
+ };
55
+
56
+ await logoutAsync(oidc, oidcDatabase, mockFetchFn, window, console)("/logged_out");
57
+
58
+ // @ts-ignore
59
+
60
+ const results = mockFetchFn.mock.calls.map((call, index) => call[1].body);
61
+
62
+ expect(results).toEqual(expectedResults);
63
+ expect(window.location.href).toBe("http://api/connect/endsession?id_token_hint=abcd&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Flogged_out");
64
+ });
65
+ });
package/src/logout.ts ADDED
@@ -0,0 +1,101 @@
1
+ import { initSession } from './initSession.js';
2
+ import { initWorkerAsync } from './initWorker.js';
3
+ import { performRevocationRequestAsync, TOKEN_TYPE } from './requests.js';
4
+ import timer from './timer.js';
5
+ import { StringMap } from './types.js';
6
+
7
+ export const oidcLogoutTokens = {
8
+ access_token: 'access_token',
9
+ refresh_token: 'refresh_token',
10
+ };
11
+
12
+ export const destroyAsync = (oidc) => async (status) => {
13
+ timer.clearTimeout(oidc.timeoutId);
14
+ oidc.timeoutId = null;
15
+ if (oidc.checkSessionIFrame) {
16
+ oidc.checkSessionIFrame.stop();
17
+ }
18
+ const serviceWorker = await initWorkerAsync(oidc.configuration.service_worker_relative_url, oidc.configurationName);
19
+ if (!serviceWorker) {
20
+ const session = initSession(oidc.configurationName, oidc.configuration.storage);
21
+ await session.clearAsync(status);
22
+ } else {
23
+ await serviceWorker.clearAsync(status);
24
+ }
25
+ oidc.tokens = null;
26
+ oidc.userInfo = null;
27
+ };
28
+
29
+ export const logoutAsync = (oidc, oidcDatabase, fetch, window, console) => async (callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) => {
30
+ const configuration = oidc.configuration;
31
+ const oidcServerConfiguration = await oidc.initAsync(configuration.authority, configuration.authority_configuration);
32
+ if (callbackPathOrUrl && (typeof callbackPathOrUrl !== 'string')) {
33
+ callbackPathOrUrl = undefined;
34
+ console.warn('callbackPathOrUrl path is not a string');
35
+ }
36
+ const path = (callbackPathOrUrl === null || callbackPathOrUrl === undefined) ? location.pathname + (location.search || '') + (location.hash || '') : callbackPathOrUrl;
37
+ let isUri = false;
38
+ if (callbackPathOrUrl) {
39
+ isUri = callbackPathOrUrl.includes('https://') || callbackPathOrUrl.includes('http://');
40
+ }
41
+ const url = isUri ? callbackPathOrUrl : window.location.origin + path;
42
+ // @ts-ignore
43
+ const idToken = oidc.tokens ? oidc.tokens.idToken : '';
44
+ try {
45
+ const revocationEndpoint = oidcServerConfiguration.revocationEndpoint;
46
+ if (revocationEndpoint) {
47
+ const promises = [];
48
+ const accessToken = oidc.tokens.accessToken;
49
+ if (accessToken && configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.access_token)) {
50
+ const revokeAccessTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, accessToken, TOKEN_TYPE.access_token, configuration.client_id);
51
+ promises.push(revokeAccessTokenPromise);
52
+ }
53
+ const refreshToken = oidc.tokens.refreshToken;
54
+ if (refreshToken && configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.refresh_token)) {
55
+ const revokeRefreshTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, refreshToken, TOKEN_TYPE.refresh_token, configuration.client_id);
56
+ promises.push(revokeRefreshTokenPromise);
57
+ }
58
+ if (promises.length > 0) {
59
+ await Promise.all(promises);
60
+ }
61
+ }
62
+ } catch (exception) {
63
+ console.warn('logoutAsync: error when revoking tokens, if the error persist, you ay configure property logout_tokens_to_invalidate from configuration to avoid this error');
64
+ console.warn(exception);
65
+ }
66
+ // @ts-ignore
67
+ const sub = oidc.tokens && oidc.tokens.idTokenPayload ? oidc.tokens.idTokenPayload.sub : null;
68
+ await oidc.destroyAsync('LOGGED_OUT');
69
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
+ for (const [key, itemOidc] of Object.entries(oidcDatabase)) {
71
+ if (itemOidc !== oidc) {
72
+ // @ts-ignore
73
+ await oidc.logoutSameTabAsync(oidc.configuration.client_id, sub);
74
+ }
75
+ }
76
+
77
+ if (oidcServerConfiguration.endSessionEndpoint) {
78
+ if (!extras) {
79
+ extras = {
80
+ id_token_hint: idToken,
81
+ };
82
+ if (callbackPathOrUrl !== null) {
83
+ extras.post_logout_redirect_uri = url;
84
+ }
85
+ }
86
+ let queryString = '';
87
+ if (extras) {
88
+ for (const [key, value] of Object.entries(extras)) {
89
+ if (queryString === '') {
90
+ queryString += '?';
91
+ } else {
92
+ queryString += '&';
93
+ }
94
+ queryString += `${key}=${encodeURIComponent(value)}`;
95
+ }
96
+ }
97
+ window.location.href = `${oidcServerConfiguration.endSessionEndpoint}${queryString}`;
98
+ } else {
99
+ window.location.reload();
100
+ }
101
+ };