import { type UserManager as OidcClientTsUserManager, type User as OidcClientTsUser, ErrorTimeout, ErrorResponse } from "../vendor/frontend/oidc-client-ts"; import { Deferred } from "../tools/Deferred"; import { assert } from "../tools/tsafe/assert"; import { id } from "../tools/tsafe/id"; import { noUndefined } from "../tools/tsafe/noUndefined"; import { getStateData, clearStateStore, type StateData } from "./StateData"; import { getDownlinkAndRtt } from "../tools/getDownlinkAndRtt"; import { getIsDev } from "../tools/isDev"; import { type AuthResponse } from "./AuthResponse"; import { addOrUpdateSearchParam } from "../tools/urlSearchParams"; import { getIsOnline } from "../tools/getIsOnline"; import type { Evt } from "../tools/Evt"; type ResultOfLoginSilent = | { outcome: "got auth response from iframe"; authResponse: AuthResponse; } | { outcome: "timeout"; } | { outcome: "token refreshed using refresh token"; oidcClientTsUser: OidcClientTsUser; } | { outcome: "got error auth response using refresh token"; authResponse: { error: string; error_description: string | undefined; }; } | { outcome: "other error"; error: Error; }; export function createLoginSilent(params: { getEvtIframeAuthResponse: () => Evt; oidcClientTsUserManager: OidcClientTsUserManager; stateUrlParamValue_instance: string; configId: string; transformUrlBeforeRedirect: | ((params: { authorizationUrl: string; isSilent: true }) => string) | undefined; getExtraQueryParams: | ((params: { isSilent: true; url: string }) => Record) | undefined; getExtraTokenParams: (() => Record) | undefined; autoLogin: boolean; log: typeof console.log | undefined; }) { const { getEvtIframeAuthResponse, oidcClientTsUserManager, stateUrlParamValue_instance, configId, transformUrlBeforeRedirect, getExtraQueryParams, getExtraTokenParams, autoLogin, log } = params; async function loginSilent(params: { extraTokenParams: Record | undefined; }): Promise { const { extraTokenParams } = params; delay_until_online: { const { isOnline, prOnline } = getIsOnline(); if (isOnline) { break delay_until_online; } log?.( "The browser seems offline, waiting to get back a connection before proceeding to login" ); await prOnline; } const dResult = new Deferred(); const timeoutDelayMs: number = (() => { const isDev = getIsDev(); const downlinkAndRtt = getDownlinkAndRtt(); // Base delay is the minimum delay we should wait in any case const BASE_DELAY_MS = isDev ? 9_000 : autoLogin ? 25_000 : 7_000; if (downlinkAndRtt === undefined) { return BASE_DELAY_MS; } const { downlink, rtt } = downlinkAndRtt; // Calculate dynamic delay based on RTT and downlink // Add 1 to downlink to avoid division by zero const dynamicDelay = rtt * 2.5 + BASE_DELAY_MS / (downlink + 1); return Math.max(BASE_DELAY_MS, dynamicDelay); })(); let clearTimeouts: (params: { wasSuccess: boolean }) => void; { let hasLoggedWarningMessage = false; const timeouts = [ setTimeout(() => { clearStateStore({ stateUrlParamValue: stateUrlParamValue_instance }); unsubscribe_evtIframeAuthResponse(); dResult.resolve({ outcome: "timeout" }); }, timeoutDelayMs), setTimeout(() => { console.warn( [ "oidc-spa: Session restoration is taking longer than expected.", "This likely indicates a misconfiguration.", `Waiting ${Math.floor( timeoutDelayMs / 1_000 )} seconds before running diagnostics.`, "Once the timeout expires, helpful debugging information will be printed to the console." ].join(" ") ); hasLoggedWarningMessage = true; }, 2_000) ]; clearTimeouts = ({ wasSuccess }) => { unsubscribe_evtIframeAuthResponse(); timeouts.forEach(clearTimeout); if (wasSuccess && hasLoggedWarningMessage) { console.log( [ "oidc-spa: Never mind, the auth server was just slow to respond.", "You can safely ignore the previous warning." ].join(" ") ); } }; } const { unsubscribe: unsubscribe_evtIframeAuthResponse } = getEvtIframeAuthResponse().subscribe( authResponse => { if (authResponse.state !== stateUrlParamValue_instance) { return; } unsubscribe_evtIframeAuthResponse(); const stateData = getStateData({ stateUrlParamValue: authResponse.state }); assert(stateData !== undefined, "765645"); assert(stateData.context === "iframe", "250711"); assert(stateData.configId === configId, "4922732"); clearTimeouts({ wasSuccess: true }); dResult.resolve({ outcome: "got auth response from iframe", authResponse }); } ); const transformUrl_oidcClientTs = (url: string) => { add_extra_query_params: { if (getExtraQueryParams === undefined) { break add_extra_query_params; } const extraQueryParams = getExtraQueryParams({ isSilent: true, url }); for (const [name, value] of Object.entries(extraQueryParams)) { if (value === undefined) { continue; } url = addOrUpdateSearchParam({ url, name, value, encodeMethod: "www-form" }); } } apply_transform_url: { if (transformUrlBeforeRedirect === undefined) { break apply_transform_url; } url = transformUrlBeforeRedirect({ authorizationUrl: url, isSilent: true }); } return url; }; oidcClientTsUserManager .signinSilent({ state: id({ context: "iframe", configId }), silentRequestTimeoutInSeconds: timeoutDelayMs / 1000, extraTokenParams: noUndefined({ ...getExtraTokenParams?.(), ...extraTokenParams }), transformUrl: transformUrl_oidcClientTs }) .then( oidcClientTsUser => { assert( oidcClientTsUser !== null, "oidcClientTsUser is not supposed to be null here" ); clearTimeouts({ wasSuccess: true }); dResult.resolve({ outcome: "token refreshed using refresh token", oidcClientTsUser }); }, error => { assert(error instanceof Error); if (error instanceof ErrorTimeout) { // NOTE: This is the expected successful outcome // oidc-spa is handling the iframe's response // oidc-client-ts never sees it. By design. return; } clearTimeouts({ wasSuccess: false }); if (error instanceof ErrorResponse) { assert(error.error !== null, "4033"); dResult.resolve({ outcome: "got error auth response using refresh token", authResponse: { error: error.error, error_description: error.error_description ?? undefined } }); return; } log?.(`loginSilent error: ${error.message}`); dResult.resolve({ outcome: "other error", error }); } ); return dResult.pr; } return { loginSilent }; }