@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
package/src/oidc.ts ADDED
@@ -0,0 +1,613 @@
1
+ import { startCheckSessionAsync as defaultStartCheckSessionAsync } from './checkSession.js';
2
+ import { CheckSessionIFrame } from './checkSessionIFrame.js';
3
+ import { eventNames } from './events.js';
4
+ import { initSession } from './initSession.js';
5
+ import { initWorkerAsync, sleepAsync } from './initWorker.js';
6
+ import { defaultLoginAsync, loginCallbackAsync } from './login.js';
7
+ import { destroyAsync, logoutAsync } from './logout.js';
8
+ import {
9
+ computeTimeLeft,
10
+ isTokensOidcValid,
11
+ setTokens, TokenRenewMode,
12
+ Tokens,
13
+ } from './parseTokens.js';
14
+ import { autoRenewTokens, renewTokensAndStartTimerAsync } from './renewTokens.js';
15
+ import { fetchFromIssuer, performTokenRequestAsync } from './requests.js';
16
+ import { getParseQueryStringFromLocation } from './route-utils.js';
17
+ import defaultSilentLoginAsync, { _silentLoginAsync } from './silentLogin.js';
18
+ import timer from './timer.js';
19
+ import { AuthorityConfiguration, Fetch, OidcConfiguration, StringMap } from './types.js';
20
+ import { userInfoAsync } from './user.js';
21
+
22
+ export const getFetchDefault = () => {
23
+ return fetch;
24
+ };
25
+
26
+ export interface OidcAuthorizationServiceConfigurationJson {
27
+ check_session_iframe?: string;
28
+ issuer:string;
29
+ }
30
+
31
+ export class OidcAuthorizationServiceConfiguration {
32
+ private checkSessionIframe: string;
33
+ private issuer: string;
34
+ private authorizationEndpoint: string;
35
+ private tokenEndpoint: string;
36
+ private revocationEndpoint: string;
37
+ private userInfoEndpoint: string;
38
+ private endSessionEndpoint: string;
39
+
40
+ constructor(request: any) {
41
+ this.authorizationEndpoint = request.authorization_endpoint;
42
+ this.tokenEndpoint = request.token_endpoint;
43
+ this.revocationEndpoint = request.revocation_endpoint;
44
+ this.userInfoEndpoint = request.userinfo_endpoint;
45
+ this.checkSessionIframe = request.check_session_iframe;
46
+ this.issuer = request.issuer;
47
+ this.endSessionEndpoint = request.end_session_endpoint;
48
+ }
49
+ }
50
+
51
+ const oidcDatabase = {};
52
+ const oidcFactory = (getFetch : () => Fetch) => (configuration: OidcConfiguration, name = 'default') => {
53
+ if (oidcDatabase[name]) {
54
+ return oidcDatabase[name];
55
+ }
56
+ oidcDatabase[name] = new Oidc(configuration, name, getFetch);
57
+ return oidcDatabase[name];
58
+ };
59
+ export type LoginCallback = {
60
+ callbackPath:string;
61
+ }
62
+
63
+ export type InternalLoginCallback = {
64
+ callbackPath:string;
65
+ parsedTokens:Tokens;
66
+ }
67
+
68
+ const loginCallbackWithAutoTokensRenewAsync = async (oidc) : Promise<LoginCallback> => {
69
+ const { parsedTokens, callbackPath } = await oidc.loginCallbackAsync();
70
+ oidc.timeoutId = autoRenewTokens(oidc, parsedTokens.refreshToken, parsedTokens.expiresAt);
71
+ return { callbackPath };
72
+ };
73
+
74
+ const getRandomInt = (max) => {
75
+ return Math.floor(Math.random() * max);
76
+ };
77
+
78
+ export class Oidc {
79
+ public configuration: OidcConfiguration;
80
+ public userInfo: null;
81
+ public tokens?: Tokens;
82
+ public events: Array<any>;
83
+ private timeoutId: NodeJS.Timeout;
84
+ public configurationName: string;
85
+ private checkSessionIFrame: CheckSessionIFrame;
86
+ private getFetch: () => Fetch;
87
+ constructor(configuration:OidcConfiguration, configurationName = 'default', getFetch : () => Fetch) {
88
+ let silent_login_uri = configuration.silent_login_uri;
89
+ if (configuration.silent_redirect_uri && !configuration.silent_login_uri) {
90
+ silent_login_uri = `${configuration.silent_redirect_uri.replace('-callback', '').replace('callback', '')}-login`;
91
+ }
92
+ let refresh_time_before_tokens_expiration_in_second = configuration.refresh_time_before_tokens_expiration_in_second ?? 120;
93
+ if (refresh_time_before_tokens_expiration_in_second > 60) {
94
+ refresh_time_before_tokens_expiration_in_second = refresh_time_before_tokens_expiration_in_second - Math.floor(Math.random() * 40);
95
+ }
96
+ if (!configuration.logout_tokens_to_invalidate) {
97
+ configuration.logout_tokens_to_invalidate = ['access_token', 'refresh_token'];
98
+ }
99
+ if (!configuration.authority_timeout_wellknowurl_in_millisecond) {
100
+ configuration.authority_timeout_wellknowurl_in_millisecond = 10000;
101
+ }
102
+ this.configuration = {
103
+ ...configuration,
104
+ silent_login_uri,
105
+ monitor_session: configuration.monitor_session ?? false,
106
+ refresh_time_before_tokens_expiration_in_second,
107
+ silent_login_timeout: configuration.silent_login_timeout ?? 12000,
108
+ token_renew_mode: configuration.token_renew_mode ?? TokenRenewMode.access_token_or_id_token_invalid,
109
+ };
110
+ this.getFetch = getFetch ?? getFetchDefault;
111
+ this.configurationName = configurationName;
112
+ this.tokens = null;
113
+ this.userInfo = null;
114
+ this.events = [];
115
+ this.timeoutId = null;
116
+ this.synchroniseTokensAsync.bind(this);
117
+ this.loginCallbackWithAutoTokensRenewAsync.bind(this);
118
+ this.initAsync.bind(this);
119
+ this.loginCallbackAsync.bind(this);
120
+ this.subscribeEvents.bind(this);
121
+ this.removeEventSubscription.bind(this);
122
+ this.publishEvent.bind(this);
123
+ this.destroyAsync.bind(this);
124
+ this.logoutAsync.bind(this);
125
+ this.renewTokensAsync.bind(this);
126
+ this.initAsync(this.configuration.authority, this.configuration.authority_configuration);
127
+ }
128
+
129
+ subscribeEvents(func):string {
130
+ const id = getRandomInt(9999999999999).toString();
131
+ this.events.push({ id, func });
132
+ return id;
133
+ }
134
+
135
+ removeEventSubscription(id) :void {
136
+ const newEvents = this.events.filter(e => e.id !== id);
137
+ this.events = newEvents;
138
+ }
139
+
140
+ publishEvent(eventName, data) {
141
+ this.events.forEach(event => {
142
+ event.func(eventName, data);
143
+ });
144
+ }
145
+
146
+ static getOrCreate = (getFetch : () => Fetch) => (configuration, name = 'default') => {
147
+ return oidcFactory(getFetch)(configuration, name);
148
+ };
149
+
150
+ static get(name = 'default') {
151
+ const isInsideBrowser = (typeof process === 'undefined');
152
+ if (!Object.prototype.hasOwnProperty.call(oidcDatabase, name) && isInsideBrowser) {
153
+ throw Error(`OIDC library does seem initialized.
154
+ Please checkout that you are using OIDC hook inside a <OidcProvider configurationName="${name}"></OidcProvider> compoment.`);
155
+ }
156
+ return oidcDatabase[name];
157
+ }
158
+
159
+ static eventNames = eventNames;
160
+
161
+ _silentLoginCallbackFromIFrame() {
162
+ if (this.configuration.silent_redirect_uri && this.configuration.silent_login_uri) {
163
+ const queryParams = getParseQueryStringFromLocation(window.location.href);
164
+ window.top.postMessage(`${this.configurationName}_oidc_tokens:${JSON.stringify({ tokens: this.tokens, sessionState: queryParams.session_state })}`, window.location.origin);
165
+ }
166
+ }
167
+
168
+ _silentLoginErrorCallbackFromIFrame() {
169
+ if (this.configuration.silent_redirect_uri && this.configuration.silent_login_uri) {
170
+ const queryParams = getParseQueryStringFromLocation(window.location.href);
171
+ window.top.postMessage(`${this.configurationName}_oidc_error:${JSON.stringify({ error: queryParams.error })}`, window.location.origin);
172
+ }
173
+ }
174
+
175
+ async silentLoginCallbackAsync() {
176
+ try {
177
+ await this.loginCallbackAsync(true);
178
+ this._silentLoginCallbackFromIFrame();
179
+ } catch (error) {
180
+ console.error(error);
181
+ this._silentLoginErrorCallbackFromIFrame();
182
+ }
183
+ }
184
+
185
+ initPromise = null;
186
+ async initAsync(authority:string, authorityConfiguration:AuthorityConfiguration) {
187
+ if (this.initPromise !== null) {
188
+ return this.initPromise;
189
+ }
190
+ const localFuncAsync = async () => {
191
+ if (authorityConfiguration != null) {
192
+ return new OidcAuthorizationServiceConfiguration({
193
+ authorization_endpoint: authorityConfiguration.authorization_endpoint,
194
+ end_session_endpoint: authorityConfiguration.end_session_endpoint,
195
+ revocation_endpoint: authorityConfiguration.revocation_endpoint,
196
+ token_endpoint: authorityConfiguration.token_endpoint,
197
+ userinfo_endpoint: authorityConfiguration.userinfo_endpoint,
198
+ check_session_iframe: authorityConfiguration.check_session_iframe,
199
+ issuer: authorityConfiguration.issuer,
200
+ });
201
+ }
202
+
203
+ const serviceWorker = await initWorkerAsync(this.configuration.service_worker_relative_url, this.configurationName);
204
+ const storage = serviceWorker ? window.localStorage : null;
205
+ return await fetchFromIssuer(this.getFetch())(authority, this.configuration.authority_time_cache_wellknowurl_in_second ?? 60 * 60, storage, this.configuration.authority_timeout_wellknowurl_in_millisecond);
206
+ };
207
+ this.initPromise = localFuncAsync();
208
+ return this.initPromise.then((result) => {
209
+ this.initPromise = null;
210
+ return result;
211
+ });
212
+ }
213
+
214
+ tryKeepExistingSessionPromise = null;
215
+ async tryKeepExistingSessionAsync() :Promise<boolean> {
216
+ if (this.tryKeepExistingSessionPromise !== null) {
217
+ return this.tryKeepExistingSessionPromise;
218
+ }
219
+ const funcAsync = async () => {
220
+ let serviceWorker;
221
+ if (this.tokens != null) {
222
+ return false;
223
+ }
224
+ this.publishEvent(eventNames.tryKeepExistingSessionAsync_begin, {});
225
+ try {
226
+ const configuration = this.configuration;
227
+ const oidcServerConfiguration = await this.initAsync(configuration.authority, configuration.authority_configuration);
228
+ serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
229
+ if (serviceWorker) {
230
+ const { tokens } = await serviceWorker.initAsync(oidcServerConfiguration, 'tryKeepExistingSessionAsync', configuration);
231
+ if (tokens) {
232
+ serviceWorker.startKeepAliveServiceWorker();
233
+ // @ts-ignore
234
+ this.tokens = tokens;
235
+ const getLoginParams = serviceWorker.getLoginParams(this.configurationName);
236
+ // @ts-ignore
237
+ this.timeoutId = autoRenewTokens(this, this.tokens.refreshToken, this.tokens.expiresAt, getLoginParams.extras);
238
+ const sessionState = await serviceWorker.getSessionStateAsync();
239
+ // @ts-ignore
240
+ await this.startCheckSessionAsync(oidcServerConfiguration.check_session_iframe, configuration.client_id, sessionState);
241
+ this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, {
242
+ success: true,
243
+ message: 'tokens inside ServiceWorker are valid',
244
+ });
245
+ return true;
246
+ }
247
+ this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, {
248
+ success: false,
249
+ message: 'no exiting session found',
250
+ });
251
+ } else {
252
+ if (configuration.service_worker_relative_url) {
253
+ this.publishEvent(eventNames.service_worker_not_supported_by_browser, {
254
+ message: 'service worker is not supported by this browser',
255
+ });
256
+ }
257
+ const session = initSession(this.configurationName, configuration.storage ?? sessionStorage);
258
+ const { tokens } = await session.initAsync();
259
+ if (tokens) {
260
+ // @ts-ignore
261
+ this.tokens = setTokens(tokens, null, configuration.token_renew_mode);
262
+ const getLoginParams = session.getLoginParams(this.configurationName);
263
+ // @ts-ignore
264
+ this.timeoutId = autoRenewTokens(this, tokens.refreshToken, this.tokens.expiresAt, getLoginParams.extras);
265
+ const sessionState = await session.getSessionStateAsync();
266
+ // @ts-ignore
267
+ await this.startCheckSessionAsync(oidcServerConfiguration.check_session_iframe, configuration.client_id, sessionState);
268
+ this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, {
269
+ success: true,
270
+ message: 'tokens inside storage are valid',
271
+ });
272
+ return true;
273
+ }
274
+ }
275
+ this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, {
276
+ success: false,
277
+ message: serviceWorker ? 'service worker sessions not retrieved' : 'session storage sessions not retrieved',
278
+ });
279
+ return false;
280
+ } catch (exception) {
281
+ console.error(exception);
282
+ if (serviceWorker) {
283
+ await serviceWorker.clearAsync();
284
+ }
285
+ this.publishEvent(eventNames.tryKeepExistingSessionAsync_error, 'tokens inside ServiceWorker are invalid');
286
+ return false;
287
+ }
288
+ };
289
+
290
+ this.tryKeepExistingSessionPromise = funcAsync();
291
+ return this.tryKeepExistingSessionPromise.then((result) => {
292
+ this.tryKeepExistingSessionPromise = null;
293
+ return result;
294
+ });
295
+ }
296
+
297
+ async startCheckSessionAsync(checkSessionIFrameUri, clientId, sessionState, isSilentSignin = false) {
298
+ await defaultStartCheckSessionAsync(this, oidcDatabase, this.configuration)(checkSessionIFrameUri, clientId, sessionState, isSilentSignin);
299
+ }
300
+
301
+ loginPromise: Promise<void> = null;
302
+ async loginAsync(callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined, silentLoginOnly = false) {
303
+ if (this.loginPromise !== null) {
304
+ return this.loginPromise;
305
+ }
306
+ if (silentLoginOnly) {
307
+ return defaultSilentLoginAsync(window, this.configurationName, this.configuration, this.publishEvent.bind(this), this)(extras, scope);
308
+ }
309
+ this.loginPromise = defaultLoginAsync(window, this.configurationName, this.configuration, this.publishEvent.bind(this), this.initAsync.bind(this))(callbackPath, extras, isSilentSignin, scope);
310
+ return this.loginPromise.then(result => {
311
+ this.loginPromise = null;
312
+ return result;
313
+ });
314
+ }
315
+
316
+ loginCallbackPromise : Promise<any> = null;
317
+ async loginCallbackAsync(isSilenSignin = false) {
318
+ if (this.loginCallbackPromise !== null) {
319
+ return this.loginCallbackPromise;
320
+ }
321
+
322
+ const loginCallbackLocalAsync = async():Promise<InternalLoginCallback> => {
323
+ const response = await loginCallbackAsync(this)(isSilenSignin);
324
+ // @ts-ignore
325
+ const parsedTokens = response.tokens;
326
+ // @ts-ignore
327
+ this.tokens = parsedTokens;
328
+ const serviceWorker = await initWorkerAsync(this.configuration.service_worker_relative_url, this.configurationName);
329
+ if (!serviceWorker) {
330
+ const session = initSession(this.configurationName, this.configuration.storage);
331
+ session.setTokens(parsedTokens);
332
+ }
333
+ this.publishEvent(Oidc.eventNames.token_aquired, parsedTokens);
334
+ // @ts-ignore
335
+ return { parsedTokens, state: response.state, callbackPath: response.callbackPath };
336
+ };
337
+ this.loginCallbackPromise = loginCallbackLocalAsync();
338
+ return this.loginCallbackPromise.then(result => {
339
+ this.loginCallbackPromise = null;
340
+ return result;
341
+ });
342
+ }
343
+
344
+ async synchroniseTokensAsync(refreshToken, index = 0, forceRefresh = false, extras:StringMap = null, updateTokens) {
345
+ while (!navigator.onLine && document.hidden) {
346
+ await sleepAsync(1000);
347
+ this.publishEvent(eventNames.refreshTokensAsync, { message: 'wait because navigator is offline and hidden' });
348
+ }
349
+ let numberTryOnline = 6;
350
+ while (!navigator.onLine && numberTryOnline > 0) {
351
+ await sleepAsync(1000);
352
+ numberTryOnline--;
353
+ this.publishEvent(eventNames.refreshTokensAsync, { message: `wait because navigator is offline try ${numberTryOnline}` });
354
+ }
355
+ let numberTryHidden = Math.floor(Math.random() * 15) + 10;
356
+ while (document.hidden && numberTryHidden > 0) {
357
+ await sleepAsync(1000);
358
+ numberTryHidden--;
359
+ this.publishEvent(eventNames.refreshTokensAsync, { message: `wait because navigator is hidden try ${numberTryHidden}` });
360
+ }
361
+ const isDocumentHidden = document.hidden;
362
+ const nextIndex = isDocumentHidden ? index : index + 1;
363
+ if (!extras) {
364
+ extras = {};
365
+ }
366
+ const configuration = this.configuration;
367
+
368
+ const silentLoginAsync = (extras: StringMap, state:string, scope:string = null) => {
369
+ return _silentLoginAsync(this.configurationName, this.configuration, this.publishEvent.bind(this))(extras, state, scope);
370
+ };
371
+ const localsilentLoginAsync = async () => {
372
+ try {
373
+ let loginParams;
374
+ const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
375
+ if (serviceWorker) {
376
+ loginParams = serviceWorker.getLoginParams(this.configurationName);
377
+ } else {
378
+ const session = initSession(this.configurationName, configuration.storage);
379
+ loginParams = session.getLoginParams(this.configurationName);
380
+ }
381
+ const silent_token_response = await silentLoginAsync({
382
+ ...loginParams.extras,
383
+ ...extras,
384
+ prompt: 'none',
385
+ }, loginParams.state);
386
+ if (silent_token_response) {
387
+ updateTokens(silent_token_response.tokens);
388
+ this.publishEvent(Oidc.eventNames.token_renewed, {});
389
+ return { tokens: silent_token_response.tokens, status: 'LOGGED' };
390
+ }
391
+ } catch (exceptionSilent: any) {
392
+ console.error(exceptionSilent);
393
+ this.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'exceptionSilent', exception: exceptionSilent.message });
394
+ if (exceptionSilent && exceptionSilent.message && exceptionSilent.message.startsWith('oidc')) {
395
+ updateTokens(null);
396
+ this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token silent' });
397
+ return { tokens: null, status: 'SESSION_LOST' };
398
+ }
399
+ }
400
+ this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token silent return' });
401
+ return await this.synchroniseTokensAsync(null, nextIndex, forceRefresh, extras, updateTokens);
402
+ };
403
+
404
+ if (index > 4) {
405
+ updateTokens(null);
406
+ this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token' });
407
+ return { tokens: null, status: 'SESSION_LOST' };
408
+ }
409
+ try {
410
+ const { status, tokens, nonce } = await this.syncTokensInfoAsync(configuration, this.configurationName, this.tokens, forceRefresh);
411
+ switch (status) {
412
+ case 'SESSION_LOST':
413
+ updateTokens(null);
414
+ this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token session lost' });
415
+ return { tokens: null, status: 'SESSION_LOST' };
416
+ case 'NOT_CONNECTED':
417
+ updateTokens(null);
418
+ return { tokens: null, status: null };
419
+ case 'TOKENS_VALID':
420
+ updateTokens(tokens);
421
+ return { tokens, status: 'LOGGED_IN' };
422
+ case 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID':
423
+ updateTokens(tokens);
424
+ this.publishEvent(Oidc.eventNames.token_renewed, { reason: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' });
425
+ return { tokens, status: 'LOGGED_IN' };
426
+ case 'LOGOUT_FROM_ANOTHER_TAB':
427
+ updateTokens(null);
428
+ this.publishEvent(eventNames.logout_from_another_tab, { status: 'session syncTokensAsync' });
429
+ return { tokens: null, status: 'LOGGED_OUT' };
430
+ case 'REQUIRE_SYNC_TOKENS':
431
+ this.publishEvent(eventNames.refreshTokensAsync_begin, { refreshToken, status, tryNumber: index });
432
+ return await localsilentLoginAsync();
433
+ default: {
434
+ this.publishEvent(eventNames.refreshTokensAsync_begin, { refreshToken, status, tryNumber: index });
435
+ if (!refreshToken) {
436
+ return await localsilentLoginAsync();
437
+ }
438
+
439
+ const clientId = configuration.client_id;
440
+ const redirectUri = configuration.redirect_uri;
441
+ const authority = configuration.authority;
442
+ const tokenExtras = configuration.token_request_extras ? configuration.token_request_extras : {};
443
+ const finalExtras = { ...tokenExtras };
444
+
445
+ for (const [key, value] of Object.entries(extras)) {
446
+ if (key.endsWith(':token_request')) {
447
+ finalExtras[key.replace(':token_request', '')] = value;
448
+ }
449
+ }
450
+ const localFunctionAsync = async () => {
451
+ const details = {
452
+ client_id: clientId,
453
+ redirect_uri: redirectUri,
454
+ grant_type: 'refresh_token',
455
+ refresh_token: tokens.refreshToken,
456
+ };
457
+ const oidcServerConfiguration = await this.initAsync(authority, configuration.authority_configuration);
458
+ const timeoutMs = document.hidden ? 10000 : 30000 * 10;
459
+ const tokenResponse = await performTokenRequestAsync(this.getFetch())(oidcServerConfiguration.tokenEndpoint, details, finalExtras, tokens, configuration.token_renew_mode, timeoutMs);
460
+ if (tokenResponse.success) {
461
+ const { isValid, reason } = isTokensOidcValid(tokenResponse.data, nonce.nonce, oidcServerConfiguration);
462
+ if (!isValid) {
463
+ updateTokens(null);
464
+ this.publishEvent(eventNames.refreshTokensAsync_error, { message: `refresh token return not valid tokens, reason: ${reason}` });
465
+ return { tokens: null, status: 'SESSION_LOST' };
466
+ }
467
+ updateTokens(tokenResponse.data);
468
+ this.publishEvent(eventNames.refreshTokensAsync_end, { success: tokenResponse.success });
469
+ this.publishEvent(Oidc.eventNames.token_renewed, { reason: 'REFRESH_TOKEN' });
470
+ return { tokens: tokenResponse.data, status: 'LOGGED_IN' };
471
+ } else {
472
+ this.publishEvent(eventNames.refreshTokensAsync_silent_error, {
473
+ message: 'bad request',
474
+ tokenResponse,
475
+ });
476
+ return await this.synchroniseTokensAsync(refreshToken, nextIndex, forceRefresh, extras, updateTokens);
477
+ }
478
+ };
479
+ return await localFunctionAsync();
480
+ }
481
+ }
482
+ } catch (exception: any) {
483
+ console.error(exception);
484
+ this.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'exception', exception: exception.message });
485
+ return this.synchroniseTokensAsync(refreshToken, nextIndex, forceRefresh, extras, updateTokens);
486
+ }
487
+ }
488
+
489
+ async syncTokensInfoAsync(configuration, configurationName, currentTokens, forceRefresh = false) {
490
+ // Service Worker can be killed by the browser (when it wants,for example after 10 seconds of inactivity, so we retreieve the session if it happen)
491
+ // const configuration = this.configuration;
492
+ const nullNonce = { nonce: null };
493
+ if (!currentTokens) {
494
+ return { tokens: null, status: 'NOT_CONNECTED', nonce: nullNonce };
495
+ }
496
+ let nonce = nullNonce;
497
+ const oidcServerConfiguration = await this.initAsync(configuration.authority, configuration.authority_configuration);
498
+ const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, configurationName);
499
+ if (serviceWorker) {
500
+ const { status, tokens } = await serviceWorker.initAsync(oidcServerConfiguration, 'syncTokensAsync', configuration);
501
+ if (status === 'LOGGED_OUT') {
502
+ return { tokens: null, status: 'LOGOUT_FROM_ANOTHER_TAB', nonce: nullNonce };
503
+ } else if (status === 'SESSIONS_LOST') {
504
+ return { tokens: null, status: 'SESSIONS_LOST', nonce: nullNonce };
505
+ } else if (!status || !tokens) {
506
+ return { tokens: null, status: 'REQUIRE_SYNC_TOKENS', nonce: nullNonce };
507
+ } else if (tokens.issuedAt !== currentTokens.issuedAt) {
508
+ const timeLeft = computeTimeLeft(configuration.refresh_time_before_tokens_expiration_in_second, tokens.expiresAt);
509
+ const status = (timeLeft > 0) ? 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' : 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID';
510
+ const nonce = await serviceWorker.getNonceAsync();
511
+ return { tokens, status, nonce };
512
+ }
513
+ nonce = await serviceWorker.getNonceAsync();
514
+ } else {
515
+ const session = initSession(configurationName, configuration.storage ?? sessionStorage);
516
+ const { tokens, status } = await session.initAsync();
517
+ if (!tokens) {
518
+ return { tokens: null, status: 'LOGOUT_FROM_ANOTHER_TAB', nonce: nullNonce };
519
+ } else if (status === 'SESSIONS_LOST') {
520
+ return { tokens: null, status: 'SESSIONS_LOST', nonce: nullNonce };
521
+ } else if (tokens.issuedAt !== currentTokens.issuedAt) {
522
+ const timeLeft = computeTimeLeft(configuration.refresh_time_before_tokens_expiration_in_second, tokens.expiresAt);
523
+ const status = (timeLeft > 0) ? 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' : 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID';
524
+ const nonce = await session.getNonceAsync();
525
+ return { tokens, status, nonce };
526
+ }
527
+ nonce = await session.getNonceAsync();
528
+ }
529
+
530
+ const timeLeft = computeTimeLeft(configuration.refresh_time_before_tokens_expiration_in_second, currentTokens.expiresAt);
531
+ const status = (timeLeft > 0) ? 'TOKENS_VALID' : 'TOKENS_INVALID';
532
+ if (forceRefresh) {
533
+ return { tokens: currentTokens, status: 'FORCE_REFRESH', nonce };
534
+ }
535
+ return { tokens: currentTokens, status, nonce };
536
+ }
537
+
538
+ loginCallbackWithAutoTokensRenewPromise:Promise<LoginCallback> = null;
539
+ loginCallbackWithAutoTokensRenewAsync():Promise<LoginCallback> {
540
+ if (this.loginCallbackWithAutoTokensRenewPromise !== null) {
541
+ return this.loginCallbackWithAutoTokensRenewPromise;
542
+ }
543
+ this.loginCallbackWithAutoTokensRenewPromise = loginCallbackWithAutoTokensRenewAsync(this);
544
+ return this.loginCallbackWithAutoTokensRenewPromise.then(result => {
545
+ this.loginCallbackWithAutoTokensRenewPromise = null;
546
+ return result;
547
+ });
548
+ }
549
+
550
+ userInfoPromise:Promise<any> = null;
551
+ userInfoAsync(noCache = false) {
552
+ if (this.userInfoPromise !== null) {
553
+ return this.userInfoPromise;
554
+ }
555
+ this.userInfoPromise = userInfoAsync(this)(noCache);
556
+ return this.userInfoPromise.then(result => {
557
+ this.userInfoPromise = null;
558
+ return result;
559
+ });
560
+ }
561
+
562
+ renewTokensPromise:Promise<any> = null;
563
+
564
+ async renewTokensAsync (extras:StringMap = null) {
565
+ if (this.renewTokensPromise !== null) {
566
+ return this.renewTokensPromise;
567
+ }
568
+ if (!this.timeoutId) {
569
+ return;
570
+ }
571
+ timer.clearTimeout(this.timeoutId);
572
+ // @ts-ignore
573
+ this.renewTokensPromise = renewTokensAndStartTimerAsync(this, this.tokens.refreshToken, true, extras);
574
+ return this.renewTokensPromise.then(result => {
575
+ this.renewTokensPromise = null;
576
+ return result;
577
+ });
578
+ }
579
+
580
+ async destroyAsync(status) {
581
+ return await destroyAsync(this)(status);
582
+ }
583
+
584
+ async logoutSameTabAsync(clientId: string, sub: any) {
585
+ // @ts-ignore
586
+ if (this.configuration.monitor_session && this.configuration.client_id === clientId && sub && this.tokens && this.tokens.idTokenPayload && this.tokens.idTokenPayload.sub === sub) {
587
+ this.publishEvent(eventNames.logout_from_same_tab, { message: sub });
588
+ await this.destroyAsync('LOGGED_OUT');
589
+ }
590
+ }
591
+
592
+ async logoutOtherTabAsync(clientId: string, sub: any) {
593
+ // @ts-ignore
594
+ if (this.configuration.monitor_session && this.configuration.client_id === clientId && sub && this.tokens && this.tokens.idTokenPayload && this.tokens.idTokenPayload.sub === sub) {
595
+ await this.destroyAsync('LOGGED_OUT');
596
+ this.publishEvent(eventNames.logout_from_another_tab, { message: 'SessionMonitor', sub });
597
+ }
598
+ }
599
+
600
+ logoutPromise:Promise<void> = null;
601
+ async logoutAsync(callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) {
602
+ if (this.logoutPromise) {
603
+ return this.logoutPromise;
604
+ }
605
+ this.logoutPromise = logoutAsync(this, oidcDatabase, this.getFetch(), window, console)(callbackPathOrUrl, extras);
606
+ return this.logoutPromise.then(result => {
607
+ this.logoutPromise = null;
608
+ return result;
609
+ });
610
+ }
611
+ }
612
+
613
+ export default Oidc;
@@ -0,0 +1,50 @@
1
+ import { describe, expect,it } from 'vitest';
2
+
3
+ import {getValidTokenAsync, isTokensOidcValid} from "./parseTokens";
4
+
5
+ describe('ParseTokens test Suite', () => {
6
+ const currentTimeUnixSecond = new Date().getTime() / 1000;
7
+ describe.each([
8
+ [currentTimeUnixSecond + 120, currentTimeUnixSecond - 10, true],
9
+ [currentTimeUnixSecond - 20, currentTimeUnixSecond - 50, false],
10
+ ])('getValidTokenAsync', (expiresAt, issuedAt, expectIsValidToken) => {
11
+ it('should getValidTokenAsync wait and return value', async () => {
12
+ const oidc = {
13
+ tokens: {
14
+ refreshToken: 'youhou',
15
+ idTokenPayload: null,
16
+ idToken: 'youhou',
17
+ accessTokenPayload: null,
18
+ accessToken: 'youhou',
19
+ expiresAt,
20
+ issuedAt,
21
+ },
22
+ };
23
+ const result = await getValidTokenAsync(oidc, 1, 1);
24
+ expect(result.isTokensValid).toEqual(expectIsValidToken);
25
+ });
26
+ });
27
+
28
+
29
+ const idTokenPayload = {iss: "toto", exp: currentTimeUnixSecond +900, iat: currentTimeUnixSecond -900, nonce: "nonce"};
30
+ const oidcServerConfiguration = {issuer:"toto"};
31
+ const idTokenPayloadExpired = {...idTokenPayload, exp: currentTimeUnixSecond-20};
32
+ const idTokenPayloadIssuedTooLongTimeAgo = {...idTokenPayload, iat: currentTimeUnixSecond-20000000};
33
+
34
+ describe.each([
35
+ [idTokenPayload, "nonce", oidcServerConfiguration, true, "success"],
36
+ [idTokenPayload, "other_nonce", oidcServerConfiguration, false, "bad nonce"],
37
+ [idTokenPayload, "nonce", {issuer:"tutu"}, false, "different issuer"],
38
+ [idTokenPayloadExpired, "nonce", oidcServerConfiguration, false, "id token expired issuer"],
39
+ [idTokenPayloadIssuedTooLongTimeAgo, "nonce", oidcServerConfiguration, false, "id token expired issuer"],
40
+ ])('isTokensOidcValid', (idTokenPayload, nonce, oidcServerConfiguration, expectIsValidToken, status) => {
41
+ it('should isTokensOidcValid return ' + status, async () => {
42
+ const oidc = {
43
+ idTokenPayload,
44
+ };
45
+ const {isValid} = await isTokensOidcValid(oidc, nonce, oidcServerConfiguration);
46
+ expect(isValid).toEqual(expectIsValidToken);
47
+ });
48
+ });
49
+
50
+ });