const DEFAULT_TIMEOUT = 60 * 1000; export interface SilentLoginOptions { /** * The URL to navigate the iframe to for silent login. * Defaults to `${baseURL}/bff/silent-login`. */ authURL?: string; /** * The base URL of the microservice BFF. * Used to construct the default authURL. */ baseURL?: string; /** * The URL to fetch auth info from before initiating silent login. * Defaults to `/bff/authInfo`. */ authInfoURL?: string; /** * Timeout in milliseconds for the silent login iframe to respond. * Defaults to 60000 (1 minute). */ timeout?: number; /** * An AbortSignal to cancel the silent login. */ signal?: AbortSignal; } export class TokenServerAuthError extends Error { data: any; constructor(message: string, data: any) { super(message); this.name = 'TokenServerAuthError'; this.data = data; } } /** * Performs a silent login using a hidden iframe and postMessage protocol. * * 1. Fetches auth info from the authInfoURL endpoint * 2. Creates a hidden iframe pointed at the silent login URL * 3. Waits for a postMessage response from the iframe * 4. Resolves on success, rejects with TokenServerAuthError on failure * * This function has no React, axios, or other framework dependencies. */ export async function silentLogin(options: SilentLoginOptions = {}): Promise { const { baseURL = '', authURL = `${baseURL}/bff/silent-login`, authInfoURL = '/bff/authInfo', timeout = DEFAULT_TIMEOUT, signal, } = options; const abortError = () => new DOMException('The operation was aborted.', 'AbortError'); if (signal?.aborted) { throw abortError(); } return new Promise((resolve, reject) => { const iframe = document.createElement('iframe'); const iframeId = `silent-login-iframe-${Math.round(Math.random() * 1e10).toString(36)}`; iframe.setAttribute('hidden', ''); iframe.setAttribute('allow', 'local-network-access *'); document.body.appendChild(iframe); let timerId: number | undefined; const cleanup = () => { window.removeEventListener('message', onMessage); iframe.remove(); window.clearTimeout(timerId); if (signal) { signal.removeEventListener('abort', onAbort); } }; const onMessage = ( e: Omit & { data?: { source: string; isLoggedIn: boolean; initiator: string }; } ) => { if (e.data?.source === 'bff-silent-login' && e.data.initiator === iframeId) { cleanup(); if (e.data.isLoggedIn) { resolve(); } else { reject(new TokenServerAuthError('Not logged in', e.data)); } } }; const onAbort = () => { cleanup(); reject(abortError()); }; const initiateSilentLogin = (params: URLSearchParams = new URLSearchParams()) => { /** * setTimeout used to handle scenarios when the iframe doesn't return immediately (despite prompt=none). * This likely means the iframe is showing the error page at IdentityServer (typically due to client misconfiguration). */ timerId = window.setTimeout(() => { cleanup(); reject(new Error('Authentication timed out')); }, timeout); params.append('initiator', iframeId); iframe.src = `${authURL}?${params.toString()}`; }; window.addEventListener('message', onMessage); if (signal) { signal.addEventListener('abort', onAbort); } fetch(authInfoURL, { signal }) .then(response => { if (!response.ok) { throw new Error(response.statusText); } return response.json(); }) .then(data => { const { isAuthenticated, info } = data; if (isAuthenticated) { const { idp, sessionId, subject, tenantId } = info; const params = new URLSearchParams({ idp, sid: sessionId, subject, tenantId, }); initiateSilentLogin(params); } else { cleanup(); reject(new TokenServerAuthError('Not authenticated', data)); } }) .catch(error => { if (error instanceof DOMException && error.name === 'AbortError') { return; } initiateSilentLogin(); }); }); }