UNPKG

@axa-fr/oidc-client

Version:

OpenID Connect & OAuth authentication using native javascript only, compatible with angular, react, vue, svelte, next, etc.

475 lines (450 loc) 17.4 kB
import { eventNames } from './events'; import { initSession } from './initSession.js'; import { initWorkerAsync, sleepAsync } from './initWorker.js'; import Oidc from './oidc.js'; import { computeTimeLeft, isTokensOidcValid, setTokens, Tokens } from './parseTokens.js'; import { performTokenRequestAsync } from './requests'; import { _silentLoginAsync } from './silentLogin'; import timer from './timer.js'; import { OidcConfiguration, StringMap, TokenAutomaticRenewMode } from './types.js'; async function syncTokens( oidc: Oidc, forceRefresh: boolean, extras: StringMap, scope: string = null, ) { const updateTokens = tokens => { oidc.tokens = tokens; }; const { tokens, status } = await synchroniseTokensAsync(oidc)( updateTokens, 0, forceRefresh, extras, scope, ); const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName); if (!serviceWorker) { const session = initSession(oidc.configurationName, oidc.configuration.storage); await session.setTokens(oidc.tokens); } if (!oidc.tokens) { await oidc.destroyAsync(status); return null; } return tokens; } export async function renewTokensAndStartTimerAsync( oidc, forceRefresh = false, extras: StringMap = null, scope: string = null, ) { const configuration = oidc.configuration; const lockResourcesName = `${configuration.client_id}_${oidc.configurationName}_${configuration.authority}`; let tokens: null; const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName); if ((configuration?.storage === window?.sessionStorage && !serviceWorker) || !navigator.locks) { tokens = await syncTokens(oidc, forceRefresh, extras, scope); } else { let status: any = 'retry'; while (status === 'retry') { status = await navigator.locks.request( lockResourcesName, { ifAvailable: true }, async lock => { if (!lock) { oidc.publishEvent(Oidc.eventNames.syncTokensAsync_lock_not_available, { lock: 'lock not available', }); return 'retry'; } return await syncTokens(oidc, forceRefresh, extras, scope); }, ); } tokens = status; } if (!tokens) { return null; } if (oidc.timeoutId) { // @ts-ignore oidc.timeoutId = autoRenewTokens(oidc, oidc.tokens.expiresAt, extras, scope); } return oidc.tokens; } export const autoRenewTokens = ( oidc: Oidc, expiresAt, extras: StringMap = null, scope: string = null, ) => { const refreshTimeBeforeTokensExpirationInSecond = oidc.configuration.refresh_time_before_tokens_expiration_in_second; if (oidc.timeoutId) { timer.clearTimeout(oidc.timeoutId); } return timer.setTimeout(async () => { const timeLeft = computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond, expiresAt); const timeInfo = { timeLeft }; oidc.publishEvent(Oidc.eventNames.token_timer, timeInfo); await renewTokensAndStartTimerAsync(oidc, false, extras, scope); }, 1000); }; export const synchroniseTokensStatus = { FORCE_REFRESH: 'FORCE_REFRESH', SESSION_LOST: 'SESSION_LOST', NOT_CONNECTED: 'NOT_CONNECTED', TOKENS_VALID: 'TOKENS_VALID', TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID', LOGOUT_FROM_ANOTHER_TAB: 'LOGOUT_FROM_ANOTHER_TAB', REQUIRE_SYNC_TOKENS: 'REQUIRE_SYNC_TOKENS', }; export const syncTokensInfoAsync = (oidc: Oidc) => async ( configuration: OidcConfiguration, configurationName: string, currentTokens: Tokens, forceRefresh = false, ) => { // 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) // const configuration = this.configuration; const nullNonce = { nonce: null }; if (!currentTokens) { return { tokens: null, status: 'NOT_CONNECTED', nonce: nullNonce }; } let nonce = nullNonce; const oidcServerConfiguration = await oidc.initAsync( configuration.authority, configuration.authority_configuration, ); const serviceWorker = await initWorkerAsync(configuration, configurationName); if (serviceWorker) { const { status, tokens } = await serviceWorker.initAsync( oidcServerConfiguration, 'syncTokensAsync', configuration, ); if (status === 'LOGGED_OUT') { return { tokens: null, status: 'LOGOUT_FROM_ANOTHER_TAB', nonce: nullNonce }; } else if (status === 'SESSIONS_LOST') { return { tokens: null, status: 'SESSIONS_LOST', nonce: nullNonce }; } else if (!status || !tokens) { return { tokens: null, status: 'REQUIRE_SYNC_TOKENS', nonce: nullNonce }; } else if (tokens.issuedAt !== currentTokens.issuedAt) { const timeLeft = computeTimeLeft( configuration.refresh_time_before_tokens_expiration_in_second, tokens.expiresAt, ); const status = timeLeft > 0 ? 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' : 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID'; const nonce = await serviceWorker.getNonceAsync(); return { tokens, status, nonce }; } nonce = await serviceWorker.getNonceAsync(); } else { const session = initSession(configurationName, configuration.storage ?? sessionStorage); const initAsyncResponse = await session.initAsync(); let { tokens } = initAsyncResponse; const { status } = initAsyncResponse; if (tokens) { tokens = setTokens(tokens, oidc.tokens, configuration.token_renew_mode); } if (!tokens) { return { tokens: null, status: 'LOGOUT_FROM_ANOTHER_TAB', nonce: nullNonce }; } else if (status === 'SESSIONS_LOST') { return { tokens: null, status: 'SESSIONS_LOST', nonce: nullNonce }; } else if (tokens.issuedAt !== currentTokens.issuedAt) { const timeLeft = computeTimeLeft( configuration.refresh_time_before_tokens_expiration_in_second, tokens.expiresAt, ); const status = timeLeft > 0 ? 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' : 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID'; const nonce = await session.getNonceAsync(); return { tokens, status, nonce }; } nonce = await session.getNonceAsync(); } const timeLeft = computeTimeLeft( configuration.refresh_time_before_tokens_expiration_in_second, currentTokens.expiresAt, ); const status = timeLeft > 0 ? 'TOKENS_VALID' : 'TOKENS_INVALID'; if (forceRefresh) { return { tokens: currentTokens, status: 'FORCE_REFRESH', nonce }; } return { tokens: currentTokens, status, nonce }; }; const synchroniseTokensAsync = (oidc: Oidc) => async ( updateTokens, index = 0, forceRefresh = false, extras: StringMap = null, scope: string = null, ) => { if (!navigator.onLine && document.hidden) { return { tokens: oidc.tokens, status: 'GIVE_UP' }; } let numberTryOnline = 6; while (!navigator.onLine && numberTryOnline > 0) { await sleepAsync({ milliseconds: 1000 }); numberTryOnline--; oidc.publishEvent(eventNames.refreshTokensAsync, { message: `wait because navigator is offline try ${numberTryOnline}`, }); } const isDocumentHidden = document.hidden; const nextIndex = isDocumentHidden ? index : index + 1; if (index > 4) { if (isDocumentHidden) { return { tokens: oidc.tokens, status: 'GIVE_UP' }; } else { updateTokens(null); oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token' }); return { tokens: null, status: 'SESSION_LOST' }; } } if (!extras) { extras = {}; } const configuration = oidc.configuration; const silentLoginAsync = (extras: StringMap, state: string = null, scope: string = null) => { return _silentLoginAsync( oidc.configurationName, oidc.configuration, oidc.publishEvent.bind(oidc), )(extras, state, scope); }; const localSilentLoginAsync = async () => { try { let loginParams; const serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); if (serviceWorker) { loginParams = serviceWorker.getLoginParams(); } else { const session = initSession(oidc.configurationName, configuration.storage); loginParams = session.getLoginParams(); } const silent_token_response = await silentLoginAsync({ ...loginParams.extras, ...extras, prompt: 'none', scope, }); if (!silent_token_response) { updateTokens(null); oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token silent not active', }); return { tokens: null, status: 'SESSION_LOST' }; } if (silent_token_response.error) { updateTokens(null); oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token silent', }); return { tokens: null, status: 'SESSION_LOST' }; } updateTokens(silent_token_response.tokens); oidc.publishEvent(Oidc.eventNames.token_renewed, {}); return { tokens: silent_token_response.tokens, status: 'LOGGED' }; } catch (exceptionSilent: any) { console.error(exceptionSilent); oidc.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'exceptionSilent', exception: exceptionSilent.message, }); return await synchroniseTokensAsync(oidc)( updateTokens, nextIndex, forceRefresh, extras, scope, ); } }; try { const { status, tokens, nonce } = await syncTokensInfoAsync(oidc)( configuration, oidc.configurationName, oidc.tokens, forceRefresh, ); switch (status) { case synchroniseTokensStatus.SESSION_LOST: updateTokens(null); oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token session lost', }); return { tokens: null, status: 'SESSION_LOST' }; case synchroniseTokensStatus.NOT_CONNECTED: updateTokens(null); return { tokens: null, status: null }; case synchroniseTokensStatus.TOKENS_VALID: updateTokens(tokens); return { tokens, status: 'LOGGED_IN' }; case synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID: updateTokens(tokens); oidc.publishEvent(Oidc.eventNames.token_renewed, { reason: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID', }); return { tokens, status: 'LOGGED_IN' }; case synchroniseTokensStatus.LOGOUT_FROM_ANOTHER_TAB: updateTokens(null); oidc.publishEvent(eventNames.logout_from_another_tab, { status: 'session syncTokensAsync', }); return { tokens: null, status: 'LOGGED_OUT' }; case synchroniseTokensStatus.REQUIRE_SYNC_TOKENS: if ( configuration.token_automatic_renew_mode == TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted && synchroniseTokensStatus.FORCE_REFRESH !== status ) { oidc.publishEvent(eventNames.tokensInvalidAndWaitingActionsToRefresh, {}); return { tokens: oidc.tokens, status: 'GIVE_UP' }; } oidc.publishEvent(eventNames.refreshTokensAsync_begin, { tryNumber: index }); return await localSilentLoginAsync(); default: { if ( configuration.token_automatic_renew_mode == TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted && synchroniseTokensStatus.FORCE_REFRESH !== status ) { oidc.publishEvent(eventNames.tokensInvalidAndWaitingActionsToRefresh, {}); return { tokens: oidc.tokens, status: 'GIVE_UP' }; } oidc.publishEvent(eventNames.refreshTokensAsync_begin, { refreshToken: tokens.refreshToken, status, tryNumber: index, }); if (!tokens.refreshToken) { return await localSilentLoginAsync(); } const clientId = configuration.client_id; const redirectUri = configuration.redirect_uri; const authority = configuration.authority; const tokenExtras = configuration.token_request_extras ? configuration.token_request_extras : {}; const finalExtras = { ...tokenExtras }; for (const [key, value] of Object.entries(extras)) { if (key.endsWith(':token_request')) { finalExtras[key.replace(':token_request', '')] = value; } } const localFunctionAsync = async () => { const details = { client_id: clientId, redirect_uri: redirectUri, grant_type: 'refresh_token', refresh_token: tokens.refreshToken, }; const oidcServerConfiguration = await oidc.initAsync( authority, configuration.authority_configuration, ); const timeoutMs = document.hidden ? 10000 : 30000 * 10; const url = oidcServerConfiguration.tokenEndpoint; const headersExtras = {}; if (configuration.demonstrating_proof_of_possession) { headersExtras['DPoP'] = await oidc.generateDemonstrationOfProofOfPossessionAsync( tokens.accessToken, url, 'POST', ); } const tokenResponse = await performTokenRequestAsync(oidc.getFetch())( url, details, finalExtras, tokens, headersExtras, configuration.token_renew_mode, timeoutMs, ); if (tokenResponse.success) { const { isValid, reason } = isTokensOidcValid( tokenResponse.data, nonce.nonce, oidcServerConfiguration, ); if (!isValid) { updateTokens(null); oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: `refresh token return not valid tokens, reason: ${reason}`, }); return { tokens: null, status: 'SESSION_LOST' }; } updateTokens(tokenResponse.data); if (tokenResponse.demonstratingProofOfPossessionNonce) { const serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); if (serviceWorker) { await serviceWorker.setDemonstratingProofOfPossessionNonce( tokenResponse.demonstratingProofOfPossessionNonce, ); } else { const session = initSession(oidc.configurationName, configuration.storage); await session.setDemonstratingProofOfPossessionNonce( tokenResponse.demonstratingProofOfPossessionNonce, ); } } oidc.publishEvent(eventNames.refreshTokensAsync_end, { success: tokenResponse.success, }); oidc.publishEvent(Oidc.eventNames.token_renewed, { reason: 'REFRESH_TOKEN' }); return { tokens: tokenResponse.data, status: 'LOGGED_IN' }; } else { oidc.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'bad request', tokenResponse, }); if (tokenResponse.status >= 400 && tokenResponse.status < 500) { updateTokens(null); oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: `session lost: ${tokenResponse.status}`, }); return { tokens: null, status: 'SESSION_LOST' }; } return await synchroniseTokensAsync(oidc)( updateTokens, nextIndex, forceRefresh, extras, scope, ); } }; return await localFunctionAsync(); } } } catch (exception: any) { console.error(exception); oidc.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'exception', exception: exception.message, }); // we need to break the loop or errors, as direct call of synchroniseTokensAsync // inside of synchroniseTokensAsync will cause an infinite loop and kill the browser stack // so we need to brake calls chain and delay next call return new Promise((resolve, reject) => { setTimeout(() => { synchroniseTokensAsync(oidc)(updateTokens, nextIndex, forceRefresh, extras, scope) .then(resolve) .catch(reject); }, 1000); }); } };