1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-06-20 09:35:22 +02:00

[PM-2147] [BEEEP] Open login form used to unlock extension in a separate window instead of a tab (#5384)

* [PM-1796] The autofill keyboard shortcut does not prompt a user to unlock a locked extension within an incongito browsing session

* [PM-1796] Implementing fixes for how we handle focus redirection when logging a user in and attempting to autofill within the Firefox Workspaces addon

* [PM-1796] Removing the `openerTab` value from the createNewTab method within brwoserApi.ts

* [PM-1796] Removing async declaration from createNewTab

* [PM-1796] Removing unnecessary param from the call to openBitwardenExtrensionTab

* [PM-2147] [BEEEP] Open login form used to unlock extension in a separate window instead of a tab

* [PM-2147] [BEEEP] Open login form used to unlock extension in a separate window instead of a tab

* [PM-2147] [BEEEP] Modifying the position where the window opens and starting cleanup of comments within implementation

* [PM-2147] [BEEEP] Cleaning up comments within implementation

* [PM-2147] [BEEEP] Removing unnecessary method

* [PM-2147] [BEEEP] Removing package-lock changes

* [PM-2147] [BEEEP] Cleaning up implementation

* [PM-2147] [BEEEP] Reverting addition to the whitelist-capital-letters filter and updating named file

* [PM-2147] [BEEEP] Reverting addition to the whitelist-capital-letters filter and updating named file

* [PM-2147] [BEEEP] Adjusting implementation of notifications bar to trigger presentation on lock only when not adding a new vault item

* [PM-2147] [BEEEP] Adjusting implementation of how we open a login prompt window to ensure we are showing the address bar to the user

* [PM-2147] [BEEEP] Modifying the method closeBitwardenLoginPromptWindow to not check for a popup type window

* [PM-2147] [BEEEP] Fixing bug where notification bar does not close when unlocking vault

* [PM-2147] [BEEEP] Adjusting placement of method BrowserApi.getWindow to have it present closer to getTab

* [PM-2147] [BEEEP] Implementing a sepearate service BrowserPopoutService that will maintain the most recently created popouts and selectively remove those when re-opening the login prompt

* [PM-2147] [BEEEP] Modifying position of BrowserPopoutWindowService

* [PM-2147] [BEEEP] Modifying position of BrowserPopoutWindowService

* [PM-2147] [BEEEP] Modifying how we handle identifying a single use popout
This commit is contained in:
Cesar Gonzalez 2023-08-07 16:06:25 -05:00 committed by GitHub
parent a05b4fd094
commit 50b3e40a05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 220 additions and 12 deletions

View File

@ -630,6 +630,12 @@
"notificationChangeSave": {
"message": "Update"
},
"notificationUnlockDesc": {
"message": "Unlock your Bitwarden vault to complete the auto-fill request."
},
"notificationUnlock": {
"message": "Unlock"
},
"enableContextMenuItem": {
"message": "Show context menu options"
},

View File

@ -30,6 +30,7 @@ export default class ContextMenusBackground {
msg.data.commandToRetry.msg.data,
msg.data.commandToRetry.sender.tab
);
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
}
}
);

View File

@ -12,6 +12,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import AddUnlockVaultQueueMessage from "../../background/models/add-unlock-vault-queue-message";
import AddChangePasswordQueueMessage from "../../background/models/addChangePasswordQueueMessage";
import AddLoginQueueMessage from "../../background/models/addLoginQueueMessage";
import AddLoginRuntimeMessage from "../../background/models/addLoginRuntimeMessage";
@ -23,7 +24,11 @@ import { BrowserStateService } from "../../platform/services/abstractions/browse
import { AutofillService } from "../services/abstractions/autofill.service";
export default class NotificationBackground {
private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = [];
private notificationQueue: (
| AddLoginQueueMessage
| AddChangePasswordQueueMessage
| AddUnlockVaultQueueMessage
)[] = [];
constructor(
private autofillService: AutofillService,
@ -53,10 +58,7 @@ export default class NotificationBackground {
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
switch (msg.command) {
case "unlockCompleted":
if (msg.data.target !== "notification.background") {
return;
}
await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender);
await this.handleUnlockCompleted(msg.data, sender);
break;
case "bgGetDataForTab":
await this.getDataForTab(sender.tab, msg.responseCommand);
@ -82,7 +84,9 @@ export default class NotificationBackground {
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: msg,
msg: {
command: msg,
},
sender: sender,
},
target: "notification.background",
@ -114,6 +118,9 @@ export default class NotificationBackground {
break;
}
break;
case "promptForLogin":
await this.unlockVault(sender.tab);
break;
default:
break;
}
@ -181,6 +188,14 @@ export default class NotificationBackground {
webVaultURL: await this.environmentService.getWebVaultUrl(),
},
});
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.UnlockVault) {
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
type: "unlock",
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
theme: await this.getCurrentTheme(),
},
});
}
break;
}
@ -305,6 +320,20 @@ export default class NotificationBackground {
}
}
private async unlockVault(tab: chrome.tabs.Tab) {
const currentAuthStatus = await this.authService.getAuthStatus();
if (currentAuthStatus !== AuthenticationStatus.Locked || this.notificationQueue.length) {
return;
}
const loginDomain = Utils.getDomain(tab.url);
if (!loginDomain) {
return;
}
this.pushUnlockVaultToQueue(loginDomain, tab);
}
private async pushChangePasswordToQueue(
cipherId: string,
loginDomain: string,
@ -327,6 +356,20 @@ export default class NotificationBackground {
await this.checkNotificationQueue(tab);
}
private async pushUnlockVaultToQueue(loginDomain: string, tab: chrome.tabs.Tab) {
this.removeTabFromNotificationQueue(tab);
const message: AddUnlockVaultQueueMessage = {
type: NotificationQueueMessageType.UnlockVault,
domain: loginDomain,
tabId: tab.id,
expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds
wasVaultLocked: true,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
this.removeTabFromNotificationQueue(tab);
}
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
@ -463,4 +506,22 @@ export default class NotificationBackground {
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
);
}
private async handleUnlockCompleted(
messageData: LockedVaultPendingNotificationsItem,
sender: chrome.runtime.MessageSender
): Promise<void> {
if (messageData.commandToRetry.msg.command === "autofill_login") {
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
}
if (messageData.target !== "notification.background") {
return;
}
await this.processMessage(
messageData.commandToRetry.msg.command,
messageData.commandToRetry.sender
);
}
}

View File

@ -51,4 +51,13 @@
</div>
</div>
</template>
<template id="template-unlock">
<div class="inner-wrapper">
<div id="unlock-text"></div>
<div>
<button type="button" id="unlock-vault" class="primary"></button>
</div>
</div>
</template>
</html>

View File

@ -28,6 +28,8 @@ function load() {
notificationEdit: chrome.i18n.getMessage("edit"),
notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"),
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
};
const logoLink = document.getElementById("logo-link") as HTMLAnchorElement;
@ -72,6 +74,13 @@ function load() {
changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
unlockButton.textContent = i18n.notificationUnlock;
unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
// i18n for body content
const closeButton = document.getElementById("close-button");
closeButton.title = i18n.close;
@ -80,6 +89,8 @@ function load() {
handleTypeAdd();
} else if (getQueryVariable("type") === "change") {
handleTypeChange();
} else if (getQueryVariable("type") === "unlock") {
handleTypeUnlock();
}
closeButton.addEventListener("click", (e) => {
@ -172,6 +183,17 @@ function handleTypeChange() {
});
}
function handleTypeUnlock() {
setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
const unlockButton = document.getElementById("unlock-vault");
unlockButton.addEventListener("click", (e) => {
sendPlatformMessage({
command: "bgReopenPromptForLogin",
});
});
}
function setContent(template: HTMLTemplateElement) {
const content = document.getElementById("content");
while (content.firstChild) {

View File

@ -114,6 +114,7 @@ import { Account } from "../models/account";
import { BrowserApi } from "../platform/browser/browser-api";
import { flagEnabled } from "../platform/flags";
import { UpdateBadge } from "../platform/listeners/update-badge";
import BrowserPopoutWindowService from "../platform/popup/browser-popout-window.service";
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
@ -195,6 +196,7 @@ export default class MainBackground {
cipherContextMenuHandler: CipherContextMenuHandler;
configService: ConfigServiceAbstraction;
configApiService: ConfigApiServiceAbstraction;
browserPopoutWindowService: BrowserPopoutWindowService;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
backgroundWindow = window;
@ -512,6 +514,7 @@ export default class MainBackground {
this.authService,
this.environmentService
);
this.browserPopoutWindowService = new BrowserPopoutWindowService();
const systemUtilsServiceReloadCallback = () => {
const forceWindowReload =
@ -543,7 +546,8 @@ export default class MainBackground {
this.environmentService,
this.messagingService,
this.logService,
this.configService
this.configService,
this.browserPopoutWindowService
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.cryptoService,

View File

@ -0,0 +1,6 @@
import NotificationQueueMessage from "./notificationQueueMessage";
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
export default class AddUnlockVaultQueueMessage extends NotificationQueueMessage {
type: NotificationQueueMessageType.UnlockVault;
}

View File

@ -1,6 +1,9 @@
export default class LockedVaultPendingNotificationsItem {
commandToRetry: {
msg: any;
msg: {
command: string;
data?: any;
};
sender: chrome.runtime.MessageSender;
};
target: string;

View File

@ -1,4 +1,5 @@
export enum NotificationQueueMessageType {
AddLogin = 0,
ChangePassword = 1,
UnlockVault = 2,
}

View File

@ -8,6 +8,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../platform/browser/browser-api";
import { BrowserPopoutWindowService } from "../platform/popup/abstractions/browser-popout-window.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
@ -30,7 +31,8 @@ export default class RuntimeBackground {
private environmentService: BrowserEnvironmentService,
private messagingService: MessagingService,
private logService: LogService,
private configService: ConfigServiceAbstraction
private configService: ConfigServiceAbstraction,
private browserPopoutWindowService: BrowserPopoutWindowService
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
@ -66,7 +68,7 @@ export default class RuntimeBackground {
if (this.lockedVaultPendingNotifications?.length > 0) {
item = this.lockedVaultPendingNotifications.pop();
BrowserApi.closeBitwardenExtensionTab();
await this.browserPopoutWindowService.closeLoginPrompt();
}
await this.main.refreshBadge();
@ -105,7 +107,8 @@ export default class RuntimeBackground {
await this.main.openPopup();
break;
case "promptForLogin":
BrowserApi.openBitwardenExtensionTab("popup/index.html", true);
case "bgReopenPromptForLogin":
await this.browserPopoutWindowService.openLoginPrompt(sender.tab?.windowId);
break;
case "openAddEditCipher": {
const addEditCipherUrl =

View File

@ -17,6 +17,24 @@ export class BrowserApi {
return chrome.runtime.getManifest().manifest_version;
}
static getWindow(windowId?: number): Promise<chrome.windows.Window> | void {
if (!windowId) {
return;
}
return new Promise((resolve) =>
chrome.windows.get(windowId, { populate: true }, (window) => resolve(window))
);
}
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
return new Promise((resolve) =>
chrome.windows.create(options, (window) => {
resolve(window);
})
);
}
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
return await BrowserApi.tabsQueryFirst({
active: true,
@ -105,6 +123,10 @@ export class BrowserApi {
chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback);
}
static async removeTab(tabId: number) {
await chrome.tabs.remove(tabId);
}
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
return (await browser.windows.getAll()).filter((win) => win.incognito);
}
@ -165,7 +187,7 @@ export class BrowserApi {
}
const tabToClose = tabs[tabs.length - 1];
chrome.tabs.remove(tabToClose.id);
BrowserApi.removeTab(tabToClose.id);
}
private static registeredMessageListeners: any[] = [];

View File

@ -0,0 +1,6 @@
interface BrowserPopoutWindowService {
openLoginPrompt(senderWindowId: number): Promise<void>;
closeLoginPrompt(): Promise<void>;
}
export { BrowserPopoutWindowService };

View File

@ -0,0 +1,64 @@
import { BrowserApi } from "../browser/browser-api";
import { BrowserPopoutWindowService as BrowserPopupWindowServiceInterface } from "./abstractions/browser-popout-window.service";
class BrowserPopoutWindowService implements BrowserPopupWindowServiceInterface {
private singleActionPopoutTabIds: Record<string, number> = {};
private defaultPopoutWindowOptions: chrome.windows.CreateData = {
type: "normal",
focused: true,
width: 500,
height: 800,
};
async openLoginPrompt(senderWindowId: number) {
await this.closeLoginPrompt();
await this.openPopoutWindow(
senderWindowId,
"popup/index.html?uilocation=popout",
"loginPrompt"
);
}
async closeLoginPrompt() {
await this.closeSingleActionPopout("loginPrompt");
}
private async openPopoutWindow(
senderWindowId: number,
popupWindowURL: string,
singleActionPopoutKey: string
) {
const senderWindow = senderWindowId && (await BrowserApi.getWindow(senderWindowId));
const url = chrome.extension.getURL(popupWindowURL);
const offsetRight = 15;
const offsetTop = 90;
const popupWidth = this.defaultPopoutWindowOptions.width;
const windowOptions = senderWindow
? {
...this.defaultPopoutWindowOptions,
url,
left: senderWindow.left + senderWindow.width - popupWidth - offsetRight,
top: senderWindow.top + offsetTop,
}
: { ...this.defaultPopoutWindowOptions, url };
const popupWindow = await BrowserApi.createWindow(windowOptions);
if (!singleActionPopoutKey) {
return;
}
this.singleActionPopoutTabIds[singleActionPopoutKey] = popupWindow?.tabs[0].id;
}
private async closeSingleActionPopout(popoutKey: string) {
const tabId = this.singleActionPopoutTabIds[popoutKey];
if (!tabId) {
return;
}
await BrowserApi.removeTab(tabId);
this.singleActionPopoutTabIds[popoutKey] = null;
}
}
export default BrowserPopoutWindowService;