1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 07:23:45 +00:00

[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
This commit is contained in:
Daniel Riera
2025-11-14 12:44:32 -05:00
committed by GitHub
parent 3b97093338
commit fdb2f8b553
3 changed files with 104 additions and 1 deletions

View File

@@ -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<chrome.tabs.OnRemovedInfo>());
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();
});
});
});
});

View File

@@ -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<void> {
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;
}
}
}

View File

@@ -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;