From fdb2f8b55375e2ba2682e09d71db457e5eec4c93 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 14 Nov 2025 12:44:32 -0500 Subject: [PATCH] [PM-4903] - If you back out of autofill flow from locked vault screen, credentials autofilled on normal unlock (#17283) * PM-4903- added a check for auth status and popout tabs, if no popup tab and auth is locked, abandon autofill * add test * clear all notifications if unlock popout closed * add more tests and use tabid for performance optimization --- .../notification.background.spec.ts | 58 +++++++++++++++++++ .../background/notification.background.ts | 44 +++++++++++++- .../src/background/runtime.background.ts | 3 + 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index f9e2e1c534f..8df21bc66ef 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1530,5 +1530,63 @@ describe("NotificationBackground", () => { expect(environmentServiceSpy).toHaveBeenCalled(); }); }); + + describe("handleUnlockPopoutClosed", () => { + let onRemovedListeners: Array<(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) => void>; + let tabsQuerySpy: jest.SpyInstance; + + beforeEach(() => { + onRemovedListeners = []; + chrome.tabs.onRemoved.addListener = jest.fn((listener) => { + onRemovedListeners.push(listener); + }); + chrome.runtime.getURL = jest.fn().mockReturnValue("chrome-extension://id/popup/index.html"); + notificationBackground.init(); + }); + + const triggerTabRemoved = async (tabId: number) => { + onRemovedListeners[0](tabId, mock()); + await flushPromises(); + }; + + it("sends abandon message when unlock popout is closed and vault is locked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]); + + await triggerTabRemoved(1); + + expect(tabsQuerySpy).toHaveBeenCalled(); + expect(messagingService.send).toHaveBeenCalledWith("abandonAutofillPendingNotifications"); + }); + + it("uses tracked tabId for fast lookup when available", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([ + { + id: 123, + url: "chrome-extension://id/popup/index.html?singleActionPopout=auth_unlockExtension", + } as chrome.tabs.Tab, + ]); + + await triggerTabRemoved(999); + tabsQuerySpy.mockClear(); + messagingService.send.mockClear(); + + await triggerTabRemoved(123); + + expect(tabsQuerySpy).not.toHaveBeenCalled(); + expect(messagingService.send).toHaveBeenCalledWith("abandonAutofillPendingNotifications"); + }); + + it("returns early when vault is unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]); + + await triggerTabRemoved(1); + + expect(tabsQuerySpy).not.toHaveBeenCalled(); + expect(messagingService.send).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index e27b50f13cd..de1514f0342 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -45,7 +45,7 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports -import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; +import { AuthPopoutType, openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports @@ -89,6 +89,7 @@ export default class NotificationBackground { ExtensionCommand.AutofillCard, ExtensionCommand.AutofillIdentity, ]); + private unlockPopoutTabId?: number; private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), @@ -146,6 +147,7 @@ export default class NotificationBackground { } this.setupExtensionMessageListener(); + this.setupUnlockPopoutCloseListener(); this.cleanupNotificationQueue(); } @@ -1163,6 +1165,7 @@ export default class NotificationBackground { message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ): Promise { + this.unlockPopoutTabId = undefined; const messageData = message.data as LockedVaultPendingNotificationsData; const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType; if (this.allowedRetryCommands.has(retryCommand)) { @@ -1313,4 +1316,43 @@ export default class NotificationBackground { const tabDomain = Utils.getDomain(tab.url); return tabDomain === queueMessage.domain || tabDomain === Utils.getDomain(queueMessage.tab.url); } + + private setupUnlockPopoutCloseListener() { + chrome.tabs.onRemoved.addListener(async (tabId: number) => { + await this.handleUnlockPopoutClosed(tabId); + }); + } + + /** + * If the unlock popout is closed while the vault + * is still locked and there are pending autofill notifications, abandon them. + */ + private async handleUnlockPopoutClosed(removedTabId: number) { + const authStatus = await this.getAuthStatus(); + if (authStatus >= AuthenticationStatus.Unlocked) { + this.unlockPopoutTabId = undefined; + return; + } + + if (this.unlockPopoutTabId === removedTabId) { + this.unlockPopoutTabId = undefined; + this.messagingService.send("abandonAutofillPendingNotifications"); + return; + } + + if (this.unlockPopoutTabId) { + return; + } + + const extensionUrl = chrome.runtime.getURL("popup/index.html"); + const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter( + (tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`), + ); + + if (unlockPopoutTabs.length === 0) { + this.messagingService.send("abandonAutofillPendingNotifications"); + } else if (unlockPopoutTabs[0].id) { + this.unlockPopoutTabId = unlockPopoutTabs[0].id; + } + } } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index de0d79a89db..798a7583f85 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -256,6 +256,9 @@ export default class RuntimeBackground { case "addToLockedVaultPendingNotifications": this.lockedVaultPendingNotifications.push(msg.data); break; + case "abandonAutofillPendingNotifications": + this.lockedVaultPendingNotifications = []; + break; case "lockVault": await this.lockService.lock(msg.userId); break;