From a3b04cb2b400ce8cf1f57c1c92071d654cf9909d Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:45:01 +0000 Subject: [PATCH] fix(auth): Wait for document focus before triggering WebAuthn 2FA --- .../components/webAuthn2faMethod.spec.tsx | 37 ++++++++++++++++++- .../components/webAuthn2faMethod.tsx | 15 +++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/static/app/views/authV2/authLogin/components/webAuthn2faMethod.spec.tsx b/static/app/views/authV2/authLogin/components/webAuthn2faMethod.spec.tsx index 713893becb9d..39aa5cbe7f67 100644 --- a/static/app/views/authV2/authLogin/components/webAuthn2faMethod.spec.tsx +++ b/static/app/views/authV2/authLogin/components/webAuthn2faMethod.spec.tsx @@ -1,4 +1,4 @@ -import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; +import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import * as webAuthnHandlers from 'sentry/components/webAuthn/handlers'; @@ -15,6 +15,7 @@ describe('WebAuthn2FAMethod', () => { configurable: true, value: function PublicKeyCredential() {}, }); + jest.spyOn(document, 'hasFocus').mockReturnValue(true); }); afterEach(() => { @@ -146,4 +147,38 @@ describe('WebAuthn2FAMethod', () => { await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); expect(handleSign).toHaveBeenCalledTimes(1); }); + + it('waits for the document to be focused before requesting an assertion', async () => { + jest.spyOn(document, 'hasFocus').mockReturnValue(false); + const handleSign = jest + .spyOn(webAuthnHandlers, 'handleSign') + .mockReturnValue(new Promise(() => {})); + const challengeRequest = MockApiClient.addMockResponse({ + url: '/auth/2fa/challenge/', + method: 'POST', + body: { + method: 'u2f', + challenge: {webAuthnAuthenticationData: 'challenge'}, + }, + }); + render( + + ); + + await waitFor(() => expect(challengeRequest).toHaveBeenCalledTimes(1)); + expect(handleSign).not.toHaveBeenCalled(); + + act(() => { + window.dispatchEvent(new Event('focus')); + }); + + await waitFor(() => expect(handleSign).toHaveBeenCalledTimes(1)); + }); }); diff --git a/static/app/views/authV2/authLogin/components/webAuthn2faMethod.tsx b/static/app/views/authV2/authLogin/components/webAuthn2faMethod.tsx index 24aad4a284bc..5fddb5c9c87a 100644 --- a/static/app/views/authV2/authLogin/components/webAuthn2faMethod.tsx +++ b/static/app/views/authV2/authLogin/components/webAuthn2faMethod.tsx @@ -100,10 +100,23 @@ export function WebAuthn2FAMethod({ } } - void authenticate(); + // Browsers like Safari reject navigator.credentials APIs when the document + // is not focused, so defer the WebAuthn prompt until the window has focus. + if (document.hasFocus()) { + void authenticate(); + return () => { + cancelled = true; + }; + } + + const handleFocus = () => { + void authenticate(); + }; + window.addEventListener('focus', handleFocus, {once: true}); return () => { cancelled = true; + window.removeEventListener('focus', handleFocus); }; // oxlint-disable-next-line react/exhaustive-effect-dependencies }, [attempt, challenge.result, isActive]);