From 0d62b4af5528ae87cc89ea30a9958f647b100dc3 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 1 Nov 2025 13:42:22 +0100 Subject: [PATCH] Improve webauthn popup close robustness --- .../entrypoints/background/PasskeyHandler.ts | 21 +++++++++++++++++-- .../pages/passkeys/PasskeyAuthenticate.tsx | 10 +++------ .../popup/pages/passkeys/PasskeyCreate.tsx | 10 +++------ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts index 029ba8db8..0e726c43b 100644 --- a/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/PasskeyHandler.ts @@ -27,6 +27,10 @@ import { browser, storage } from '#imports'; const pendingRequests = new Map void; reject: (error: any) => void; + /** + * Store window ID in order to close the popup window from background script later. + */ + windowId?: number; }>(); // Store request data temporarily (to avoid URL length limits) @@ -90,7 +94,7 @@ export async function handleWebAuthnCreate(data: any): Promise { // Wait for response from popup return new Promise((resolve, reject) => { - pendingRequests.set(requestId, { resolve, reject }); + pendingRequests.set(requestId, { resolve, reject, windowId: popup.id }); // Clean up if popup is closed without response const checkClosed = setInterval(async () => { @@ -149,7 +153,7 @@ export async function handleWebAuthnGet(data: any): Promise { // Wait for response from popup return new Promise((resolve, reject) => { - pendingRequests.set(requestId, { resolve, reject }); + pendingRequests.set(requestId, { resolve, reject, windowId: popup.id }); // Clean up if popup is closed without response const checkClosed = setInterval(async () => { @@ -185,6 +189,19 @@ export async function handlePasskeyPopupResponse(data: any): Promise<{ success: return { success: false }; } + /** + * Close the popup window from background script to ensure it always works. + * Calling window.close() from the popup does not work in all browsers. + */ + if (request.windowId) { + try { + await browser.windows.remove(request.windowId); + } catch (error) { + // Window might already be closed, ignore error + console.debug('Failed to close popup window:', error); + } + } + // Clean up both maps pendingRequests.delete(requestId); pendingRequestData.delete(requestId); diff --git a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx index 1a7ea6761..1218bcc02 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyAuthenticate.tsx @@ -285,13 +285,11 @@ const PasskeyAuthenticate: React.FC = () => { }; // Send response back + // The background script will close the window (Safari-compatible) await sendMessage('PASSKEY_POPUP_RESPONSE', { requestId: request.requestId, credential }, 'background'); - - // Auto-close window on success - window.close(); } catch (error) { console.error('PasskeyAuthenticate: Error during authentication', error); setLoading(false); @@ -328,12 +326,11 @@ const PasskeyAuthenticate: React.FC = () => { // For 'once', we don't store anything - just bypass this one time // Tell background to use native implementation + // The background script will close the window (Safari-compatible) await sendMessage('PASSKEY_POPUP_RESPONSE', { requestId: request.requestId, fallback: true }, 'background'); - - window.close(); }; /** @@ -345,12 +342,11 @@ const PasskeyAuthenticate: React.FC = () => { } // Tell background user cancelled + // The background script will close the window (Safari-compatible) await sendMessage('PASSKEY_POPUP_RESPONSE', { requestId: request.requestId, cancelled: true }, 'background'); - - window.close(); }; if (!request) { diff --git a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx index 60a301b04..b19b0c59b 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/passkeys/PasskeyCreate.tsx @@ -398,13 +398,11 @@ const PasskeyCreate: React.FC = () => { }; // Send response back to background + // The background script will close the window (Safari-compatible) await sendMessage('PASSKEY_POPUP_RESPONSE', { requestId: request.requestId, credential: flattenedCredential }, 'background'); - - // Auto-close window on success - window.close(); }, /** * onError @@ -450,12 +448,11 @@ const PasskeyCreate: React.FC = () => { // For 'once', we don't store anything - just bypass this one time // Tell background to use native implementation + // The background script will close the window (Safari-compatible) await sendMessage('PASSKEY_POPUP_RESPONSE', { requestId: request.requestId, fallback: true }, 'background'); - - window.close(); }; /** @@ -467,12 +464,11 @@ const PasskeyCreate: React.FC = () => { } // Tell background user cancelled + // The background script will close the window (Safari-compatible) await sendMessage('PASSKEY_POPUP_RESPONSE', { requestId: request.requestId, cancelled: true }, 'background'); - - window.close(); }; if (!request) {