mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-27821]Add validation of extension origin for uses of window.postMessage (#17476)
* PM-27821 - Replace chrome.runtime.getURL() with BrowserApi.getRuntimeURL() for consistency - Add extension origin validation for all window.postMessage calls - Implement token-based authentication for inline menu communications - Add message source validation (event.source === globalThis.parent) - Add command presence validation (- Update notification bar to validate message origins and commands - Add extensionOrigin property to services using postMessage - Generate session tokens for inline menu containers (32-char random) - Validate tokens in message handlers to prevent unauthorized commands * Add explicit token validation * only set when receiving the trusted initNotificationBar message * await windowmessageorigin before posting to parent * fix tests * the parent must include its origin in the message for notification bar race condition * reduce if statements to one block and comment * extract parentOrigin from the URL and set windoMessageOrigin accordingly * consolidate if statements * add bar.spec file * fix merge conflict
This commit is contained in:
@@ -1344,7 +1344,7 @@ export default class NotificationBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensionUrl = chrome.runtime.getURL("popup/index.html");
|
const extensionUrl = BrowserApi.getRuntimeURL("popup/index.html");
|
||||||
const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter(
|
const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter(
|
||||||
(tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`),
|
(tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2949,13 +2949,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
(await this.checkFocusedFieldHasValue(port.sender.tab)) &&
|
(await this.checkFocusedFieldHasValue(port.sender.tab)) &&
|
||||||
(await this.shouldShowSaveLoginInlineMenuList(port.sender.tab));
|
(await this.shouldShowSaveLoginInlineMenuList(port.sender.tab));
|
||||||
|
|
||||||
const iframeUrl = chrome.runtime.getURL(
|
const iframeUrl = BrowserApi.getRuntimeURL(
|
||||||
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
|
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
|
||||||
);
|
);
|
||||||
const styleSheetUrl = chrome.runtime.getURL(
|
const styleSheetUrl = BrowserApi.getRuntimeURL(
|
||||||
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
|
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
|
||||||
);
|
);
|
||||||
const extensionOrigin = new URL(iframeUrl).origin;
|
const extensionOrigin = iframeUrl ? new URL(iframeUrl).origin : null;
|
||||||
|
|
||||||
this.postMessageToPort(port, {
|
this.postMessageToPort(port, {
|
||||||
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
|
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ describe("ContentMessageHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends an authResult message", () => {
|
it("sends an authResult message", () => {
|
||||||
postWindowMessage({ command: "authResult", lastpass: true, code: "code", state: "state" });
|
postWindowMessage(
|
||||||
|
{ command: "authResult", lastpass: true, code: "code", state: "state" },
|
||||||
|
"https://localhost/",
|
||||||
|
window,
|
||||||
|
);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||||
command: "authResult",
|
command: "authResult",
|
||||||
@@ -68,7 +72,11 @@ describe("ContentMessageHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends a webAuthnResult message", () => {
|
it("sends a webAuthnResult message", () => {
|
||||||
postWindowMessage({ command: "webAuthnResult", data: "data", remember: true });
|
postWindowMessage(
|
||||||
|
{ command: "webAuthnResult", data: "data", remember: true },
|
||||||
|
"https://localhost/",
|
||||||
|
window,
|
||||||
|
);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||||
command: "webAuthnResult",
|
command: "webAuthnResult",
|
||||||
@@ -82,7 +90,7 @@ describe("ContentMessageHandler", () => {
|
|||||||
const mockCode = "mockCode";
|
const mockCode = "mockCode";
|
||||||
const command = "duoResult";
|
const command = "duoResult";
|
||||||
|
|
||||||
postWindowMessage({ command: command, code: mockCode });
|
postWindowMessage({ command: command, code: mockCode }, "https://localhost/", window);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||||
command: command,
|
command: command,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type NotificationBarWindowMessage = {
|
|||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
initData?: NotificationBarIframeInitData;
|
initData?: NotificationBarIframeInitData;
|
||||||
|
parentOrigin?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NotificationBarWindowMessageHandlers = {
|
type NotificationBarWindowMessageHandlers = {
|
||||||
|
|||||||
121
apps/browser/src/autofill/notification/bar.spec.ts
Normal file
121
apps/browser/src/autofill/notification/bar.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { postWindowMessage } from "../spec/testing-utils";
|
||||||
|
|
||||||
|
import { NotificationBarWindowMessage } from "./abstractions/notification-bar";
|
||||||
|
import "./bar";
|
||||||
|
|
||||||
|
jest.mock("lit", () => ({ render: jest.fn() }));
|
||||||
|
jest.mock("@lit-labs/signals", () => ({
|
||||||
|
signal: jest.fn((testValue) => ({ get: (): typeof testValue => testValue })),
|
||||||
|
}));
|
||||||
|
jest.mock("../content/components/notification/container", () => ({
|
||||||
|
NotificationContainer: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("NotificationBar iframe handleWindowMessage security", () => {
|
||||||
|
const trustedOrigin = "http://localhost";
|
||||||
|
const maliciousOrigin = "https://malicious.com";
|
||||||
|
|
||||||
|
const createMessage = (
|
||||||
|
overrides: Partial<NotificationBarWindowMessage> = {},
|
||||||
|
): NotificationBarWindowMessage => ({
|
||||||
|
command: "initNotificationBar",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Object.defineProperty(globalThis, "location", {
|
||||||
|
value: { search: `?parentOrigin=${encodeURIComponent(trustedOrigin)}` },
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "parent", {
|
||||||
|
value: mock<Window>(),
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
globalThis.dispatchEvent(new Event("load"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
description: "not from parent window",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => mock<Window>(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "with mismatched origin",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: maliciousOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "without command field",
|
||||||
|
message: () => ({}),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "initNotificationBar with mismatched parentOrigin",
|
||||||
|
message: () => createMessage({ parentOrigin: maliciousOrigin }),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "when windowMessageOrigin is not set",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: "different-origin",
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
resetOrigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "with null source",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: (): null => null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "with unknown command",
|
||||||
|
message: () => createMessage({ command: "unknownCommand" }),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
])("should reject messages $description", ({ message, origin, source, resetOrigin }) => {
|
||||||
|
if (resetOrigin) {
|
||||||
|
Object.defineProperty(globalThis, "location", {
|
||||||
|
value: { search: "" },
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||||
|
postWindowMessage(message(), origin, source());
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept and handle valid trusted messages", () => {
|
||||||
|
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||||
|
spy.mockClear();
|
||||||
|
|
||||||
|
const validMessage = createMessage({
|
||||||
|
parentOrigin: trustedOrigin,
|
||||||
|
initData: {
|
||||||
|
type: "change",
|
||||||
|
isVaultLocked: false,
|
||||||
|
removeIndividualVault: false,
|
||||||
|
importType: null,
|
||||||
|
launchTimestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
postWindowMessage(validMessage, trustedOrigin, globalThis.parent);
|
||||||
|
expect(validMessage.command).toBe("initNotificationBar");
|
||||||
|
expect(validMessage.parentOrigin).toBe(trustedOrigin);
|
||||||
|
expect(validMessage.initData).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,6 +24,13 @@ import {
|
|||||||
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
|
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
|
||||||
let windowMessageOrigin: string;
|
let windowMessageOrigin: string;
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
const trustedParentOrigin = urlParams.get("parentOrigin");
|
||||||
|
|
||||||
|
if (trustedParentOrigin) {
|
||||||
|
windowMessageOrigin = trustedParentOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
||||||
initNotificationBar: ({ message }) => initNotificationBar(message),
|
initNotificationBar: ({ message }) => initNotificationBar(message),
|
||||||
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message),
|
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message),
|
||||||
@@ -395,15 +402,27 @@ function setupWindowMessageListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowMessage(event: MessageEvent) {
|
function handleWindowMessage(event: MessageEvent) {
|
||||||
if (!windowMessageOrigin) {
|
if (event?.source !== globalThis.parent) {
|
||||||
windowMessageOrigin = event.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.origin !== windowMessageOrigin) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = event.data as NotificationBarWindowMessage;
|
const message = event.data as NotificationBarWindowMessage;
|
||||||
|
if (!message?.command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!windowMessageOrigin || event.origin !== windowMessageOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.command === "initNotificationBar" &&
|
||||||
|
message.parentOrigin &&
|
||||||
|
message.parentOrigin !== event.origin
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handler = notificationBarWindowMessageHandlers[message.command];
|
const handler = notificationBarWindowMessageHandlers[message.command];
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return;
|
||||||
@@ -431,5 +450,8 @@ function getResolvedTheme(theme: Theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function postMessageToParent(message: NotificationBarWindowMessage) {
|
function postMessageToParent(message: NotificationBarWindowMessage) {
|
||||||
globalThis.parent.postMessage(message, windowMessageOrigin || "*");
|
if (!windowMessageOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalThis.parent.postMessage(message, windowMessageOrigin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type InitAutofillInlineMenuButtonMessage = UpdateAuthStatusMessage & {
|
|||||||
styleSheetUrl: string;
|
styleSheetUrl: string;
|
||||||
translations: Record<string, string>;
|
translations: Record<string, string>;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutofillInlineMenuButtonWindowMessageHandlers = {
|
export type AutofillInlineMenuButtonWindowMessageHandlers = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { InlineMenuCipherData } from "../../../background/abstractions/overlay.b
|
|||||||
export type AutofillInlineMenuContainerMessage = {
|
export type AutofillInlineMenuContainerMessage = {
|
||||||
command: string;
|
command: string;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
token?: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & {
|
export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
|
|||||||
showInlineMenuAccountCreation?: boolean;
|
showInlineMenuAccountCreation?: boolean;
|
||||||
showPasskeysLabels?: boolean;
|
showPasskeysLabels?: boolean;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
|
token: string;
|
||||||
generatedPassword?: string;
|
generatedPassword?: string;
|
||||||
showSaveLoginMenu?: boolean;
|
showSaveLoginMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||||
).toHaveBeenCalledWith(message, "*");
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
|
it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
|
||||||
@@ -217,7 +217,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||||
).toHaveBeenCalledWith(message, "*");
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||||
).toHaveBeenCalledWith(message, "*");
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets a light theme based on the user's system preferences", () => {
|
it("sets a light theme based on the user's system preferences", () => {
|
||||||
@@ -262,7 +262,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Light,
|
theme: ThemeType.Light,
|
||||||
},
|
},
|
||||||
"*",
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Dark,
|
theme: ThemeType.Dark,
|
||||||
},
|
},
|
||||||
"*",
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
command: "updateAutofillInlineMenuColorScheme",
|
command: "updateAutofillInlineMenuColorScheme",
|
||||||
colorScheme: "normal",
|
colorScheme: "normal",
|
||||||
},
|
},
|
||||||
"*",
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
||||||
import {
|
import {
|
||||||
BackgroundPortMessageHandlers,
|
BackgroundPortMessageHandlers,
|
||||||
@@ -15,6 +16,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||||
private port: chrome.runtime.Port | null = null;
|
private port: chrome.runtime.Port | null = null;
|
||||||
private portKey: string;
|
private portKey: string;
|
||||||
|
private readonly extensionOrigin: string;
|
||||||
private iframeMutationObserver: MutationObserver;
|
private iframeMutationObserver: MutationObserver;
|
||||||
private iframe: HTMLIFrameElement;
|
private iframe: HTMLIFrameElement;
|
||||||
private ariaAlertElement: HTMLDivElement;
|
private ariaAlertElement: HTMLDivElement;
|
||||||
@@ -69,6 +71,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
private iframeTitle: string,
|
private iframeTitle: string,
|
||||||
private ariaAlert?: string,
|
private ariaAlert?: string,
|
||||||
) {
|
) {
|
||||||
|
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||||
this.iframeMutationObserver = new MutationObserver(this.handleMutations);
|
this.iframeMutationObserver = new MutationObserver(this.handleMutations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
* that is declared.
|
* that is declared.
|
||||||
*/
|
*/
|
||||||
initMenuIframe() {
|
initMenuIframe() {
|
||||||
this.defaultIframeAttributes.src = chrome.runtime.getURL("overlay/menu.html");
|
this.defaultIframeAttributes.src = BrowserApi.getRuntimeURL("overlay/menu.html");
|
||||||
this.defaultIframeAttributes.title = this.iframeTitle;
|
this.defaultIframeAttributes.title = this.iframeTitle;
|
||||||
|
|
||||||
this.iframe = globalThis.document.createElement("iframe");
|
this.iframe = globalThis.document.createElement("iframe");
|
||||||
@@ -259,7 +262,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private postMessageToIFrame(message: any) {
|
private postMessageToIFrame(message: any) {
|
||||||
this.iframe.contentWindow?.postMessage({ portKey: this.portKey, ...message }, "*");
|
this.iframe.contentWindow?.postMessage(
|
||||||
|
{ portKey: this.portKey, ...message },
|
||||||
|
this.extensionOrigin,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import { createInitAutofillInlineMenuButtonMessageMock } from "../../../../spec/autofill-mocks";
|
import { createInitAutofillInlineMenuButtonMessageMock } from "../../../../spec/autofill-mocks";
|
||||||
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
||||||
|
|
||||||
@@ -10,11 +11,11 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
|
|
||||||
let autofillInlineMenuButton: AutofillInlineMenuButton;
|
let autofillInlineMenuButton: AutofillInlineMenuButton;
|
||||||
const portKey: string = "inlineMenuButtonPortKey";
|
const portKey: string = "inlineMenuButtonPortKey";
|
||||||
|
const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.body.innerHTML = `<autofill-inline-menu-button></autofill-inline-menu-button>`;
|
document.body.innerHTML = `<autofill-inline-menu-button></autofill-inline-menu-button>`;
|
||||||
autofillInlineMenuButton = document.querySelector("autofill-inline-menu-button");
|
autofillInlineMenuButton = document.querySelector("autofill-inline-menu-button");
|
||||||
autofillInlineMenuButton["messageOrigin"] = "https://localhost/";
|
|
||||||
jest.spyOn(globalThis.document, "createElement");
|
jest.spyOn(globalThis.document, "createElement");
|
||||||
jest.spyOn(globalThis.parent, "postMessage");
|
jest.spyOn(globalThis.parent, "postMessage");
|
||||||
});
|
});
|
||||||
@@ -56,8 +57,8 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
autofillInlineMenuButton["buttonElement"].click();
|
autofillInlineMenuButton["buttonElement"].click();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "autofillInlineMenuButtonClicked", portKey },
|
{ command: "autofillInlineMenuButtonClicked", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -70,7 +71,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
it("does not post a message to close the autofill inline menu if the element is focused during the focus check", async () => {
|
it("does not post a message to close the autofill inline menu if the element is focused during the focus check", async () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
||||||
@@ -84,7 +85,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
||||||
@@ -98,7 +99,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||||
@@ -113,12 +114,12 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null);
|
jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "triggerDelayedAutofillInlineMenuClosure", portKey },
|
{ command: "triggerDelayedAutofillInlineMenuClosure", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuButtonAuthStatus",
|
command: "updateAutofillInlineMenuButtonAuthStatus",
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -143,6 +145,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuColorScheme",
|
command: "updateAutofillInlineMenuColorScheme",
|
||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||||
import {
|
import {
|
||||||
createAutofillOverlayCipherDataMock,
|
createAutofillOverlayCipherDataMock,
|
||||||
@@ -23,6 +24,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
|
|
||||||
let autofillInlineMenuList: AutofillInlineMenuList | null;
|
let autofillInlineMenuList: AutofillInlineMenuList | null;
|
||||||
const portKey: string = "inlineMenuListPortKey";
|
const portKey: string = "inlineMenuListPortKey";
|
||||||
|
const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id";
|
||||||
const events: { eventName: any; callback: any }[] = [];
|
const events: { eventName: any; callback: any }[] = [];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -67,8 +69,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
unlockButton.dispatchEvent(new Event("click"));
|
unlockButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "unlockVault", portKey },
|
{ command: "unlockVault", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -134,8 +136,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
addVaultItemButton.dispatchEvent(new Event("click"));
|
addVaultItemButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
{
|
||||||
"*",
|
command: "addNewVaultItem",
|
||||||
|
portKey,
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -324,8 +331,9 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
inlineMenuCipherId: "1",
|
inlineMenuCipherId: "1",
|
||||||
usePasskey: false,
|
usePasskey: false,
|
||||||
portKey,
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
},
|
},
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -492,8 +500,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
viewCipherButton.dispatchEvent(new Event("click"));
|
viewCipherButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "viewSelectedCipher", inlineMenuCipherId: "1", portKey },
|
{
|
||||||
"*",
|
command: "viewSelectedCipher",
|
||||||
|
inlineMenuCipherId: "1",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -581,8 +594,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
newVaultItemButtonSpy.dispatchEvent(new Event("click"));
|
newVaultItemButtonSpy.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
{
|
||||||
"*",
|
command: "addNewVaultItem",
|
||||||
|
portKey,
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -826,8 +844,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
fillGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
fillGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "fillGeneratedPassword", portKey },
|
{ command: "fillGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -843,7 +861,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||||
{ command: "fillGeneratedPassword", portKey },
|
{ command: "fillGeneratedPassword", portKey },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -857,8 +875,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "fillGeneratedPassword", portKey },
|
{ command: "fillGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -896,8 +914,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
refreshGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
refreshGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "refreshGeneratedPassword", portKey },
|
{ command: "refreshGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -913,7 +931,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||||
{ command: "refreshGeneratedPassword", portKey },
|
{ command: "refreshGeneratedPassword", portKey },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -927,8 +945,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "refreshGeneratedPassword", portKey },
|
{ command: "refreshGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -972,7 +990,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => {
|
it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -983,7 +1001,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -994,7 +1012,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||||
@@ -1010,11 +1028,11 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(null);
|
.mockReturnValue(null);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "checkAutofillInlineMenuButtonFocused", portKey },
|
{ command: "checkAutofillInlineMenuButtonFocused", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1022,7 +1040,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems");
|
const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems");
|
||||||
|
|
||||||
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers" });
|
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers", token: "test-token" });
|
||||||
|
|
||||||
expect(updateCiphersSpy).toHaveBeenCalled();
|
expect(updateCiphersSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1062,7 +1080,10 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword" });
|
postWindowMessage({
|
||||||
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
|
token: "test-token",
|
||||||
|
});
|
||||||
|
|
||||||
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1074,6 +1095,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
generatedPassword,
|
generatedPassword,
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(buildPasswordGeneratorSpy).toHaveBeenCalled();
|
expect(buildPasswordGeneratorSpy).toHaveBeenCalled();
|
||||||
@@ -1090,6 +1112,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
generatedPassword,
|
generatedPassword,
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1);
|
expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1);
|
||||||
@@ -1115,7 +1138,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled();
|
expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1124,7 +1147,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled();
|
expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1143,7 +1166,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
"setAttribute",
|
"setAttribute",
|
||||||
);
|
);
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
|
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
|
||||||
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
|
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
|
||||||
@@ -1161,7 +1184,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
|
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
|
||||||
jest.spyOn(unlockButton as HTMLElement, "focus");
|
jest.spyOn(unlockButton as HTMLElement, "focus");
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect((unlockButton as HTMLElement).focus).toBeCalled();
|
expect((unlockButton as HTMLElement).focus).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -1173,7 +1196,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
|
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
|
||||||
jest.spyOn(newItemButton as HTMLElement, "focus");
|
jest.spyOn(newItemButton as HTMLElement, "focus");
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect((newItemButton as HTMLElement).focus).toBeCalled();
|
expect((newItemButton as HTMLElement).focus).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -1184,7 +1207,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
|
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
|
||||||
jest.spyOn(firstCipherItem as HTMLElement, "focus");
|
jest.spyOn(firstCipherItem as HTMLElement, "focus");
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect((firstCipherItem as HTMLElement).focus).toBeCalled();
|
expect((firstCipherItem as HTMLElement).focus).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -1197,8 +1220,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
globalThis.dispatchEvent(new Event("blur"));
|
globalThis.dispatchEvent(new Event("blur"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "autofillInlineMenuBlurred", portKey },
|
{ command: "autofillInlineMenuBlurred", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1220,8 +1243,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "previous", portKey },
|
{
|
||||||
"*",
|
command: "redirectAutofillInlineMenuFocusOut",
|
||||||
|
direction: "previous",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1229,8 +1257,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
|
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "next", portKey },
|
{
|
||||||
"*",
|
command: "redirectAutofillInlineMenuFocusOut",
|
||||||
|
direction: "next",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1238,8 +1271,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
|
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "current", portKey },
|
{
|
||||||
"*",
|
command: "redirectAutofillInlineMenuFocusOut",
|
||||||
|
direction: "current",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1274,8 +1312,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
|
autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "updateAutofillInlineMenuListHeight", styles: { height: "300px" }, portKey },
|
{
|
||||||
"*",
|
command: "updateAutofillInlineMenuListHeight",
|
||||||
|
styles: { height: "300px" },
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import { generateRandomChars, setElementStyles } from "../../../../utils";
|
import { generateRandomChars, setElementStyles } from "../../../../utils";
|
||||||
import {
|
import {
|
||||||
InitAutofillInlineMenuElementMessage,
|
InitAutofillInlineMenuElementMessage,
|
||||||
@@ -73,7 +74,7 @@ export class AutofillInlineMenuContainer {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.token = generateRandomChars(32);
|
this.token = generateRandomChars(32);
|
||||||
this.extensionOrigin = chrome.runtime.getURL("").slice(0, -1);
|
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||||
globalThis.addEventListener("message", this.handleWindowMessage);
|
globalThis.addEventListener("message", this.handleWindowMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +204,9 @@ export class AutofillInlineMenuContainer {
|
|||||||
*/
|
*/
|
||||||
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
|
if (!message?.command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.isForeignWindowMessage(event)) {
|
if (this.isForeignWindowMessage(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -287,7 +291,10 @@ export class AutofillInlineMenuContainer {
|
|||||||
* every time the inline menu container is recreated.
|
* every time the inline menu container is recreated.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private isValidSessionToken(message: { token?: string }): boolean {
|
private isValidSessionToken(message: { token: string }): boolean {
|
||||||
|
if (!this.token || !message?.token || !message?.token.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return message.token === this.token;
|
return message.token === this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,8 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
styleSheetUrl: string,
|
styleSheetUrl: string,
|
||||||
translations: Record<string, string>,
|
translations: Record<string, string>,
|
||||||
portKey: string,
|
portKey: string,
|
||||||
token?: string,
|
|
||||||
): Promise<HTMLLinkElement> {
|
): Promise<HTMLLinkElement> {
|
||||||
this.portKey = portKey;
|
this.portKey = portKey;
|
||||||
if (token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.translations = translations;
|
this.translations = translations;
|
||||||
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
||||||
@@ -63,11 +59,16 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
* @param message - The message to post
|
* @param message - The message to post
|
||||||
*/
|
*/
|
||||||
protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) {
|
protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) {
|
||||||
const messageWithAuth: Record<string, unknown> = { portKey: this.portKey, ...message };
|
// never send messages containing authentication tokens without a valid token and an established messageOrigin
|
||||||
if (this.token) {
|
if (!this.token || !this.messageOrigin) {
|
||||||
messageWithAuth.token = this.token;
|
return;
|
||||||
}
|
}
|
||||||
globalThis.parent.postMessage(messageWithAuth, "*");
|
const messageWithAuth: Record<string, unknown> = {
|
||||||
|
portKey: this.portKey,
|
||||||
|
...message,
|
||||||
|
token: this.token,
|
||||||
|
};
|
||||||
|
globalThis.parent.postMessage(messageWithAuth, this.messageOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,6 +106,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.source !== globalThis.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.messageOrigin) {
|
if (!this.messageOrigin) {
|
||||||
this.messageOrigin = event.origin;
|
this.messageOrigin = event.origin;
|
||||||
}
|
}
|
||||||
@@ -115,12 +120,23 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
|
|
||||||
const message = event?.data;
|
const message = event?.data;
|
||||||
|
|
||||||
if (
|
if (!message?.command) {
|
||||||
message?.token &&
|
return;
|
||||||
(message?.command === "initAutofillInlineMenuButton" ||
|
}
|
||||||
message?.command === "initAutofillInlineMenuList")
|
|
||||||
) {
|
const isInitCommand =
|
||||||
|
message.command === "initAutofillInlineMenuButton" ||
|
||||||
|
message.command === "initAutofillInlineMenuList";
|
||||||
|
|
||||||
|
if (isInitCommand) {
|
||||||
|
if (!message?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.token = message.token;
|
this.token = message.token;
|
||||||
|
} else {
|
||||||
|
if (!this.token || !message?.token || message.token !== this.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = this.windowMessageHandlers[message?.command];
|
const handler = this.windowMessageHandlers[message?.command];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ exports[`OverlayNotificationsContentService opening the notification bar creates
|
|||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
id="bit-notification-bar-iframe"
|
id="bit-notification-bar-iframe"
|
||||||
src="chrome-extension://id/notification/bar.html"
|
src="chrome-extension://id/notification/bar.html?parentOrigin=http%3A%2F%2Flocalhost"
|
||||||
style="width: 100% !important; height: 100% !important; border: 0px !important; display: block !important; position: relative !important; transition: transform 0.15s ease-out, opacity 0.15s ease !important; border-radius: 4px !important; color-scheme: auto !important; transform: translateX(0) !important; opacity: 0;"
|
style="width: 100% !important; height: 100% !important; border: 0px !important; display: block !important; position: relative !important; transition: transform 0.15s ease-out, opacity 0.15s ease !important; border-radius: 4px !important; color-scheme: auto !important; transform: translateX(0) !important; opacity: 0;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -155,8 +155,9 @@ describe("OverlayNotificationsContentService", () => {
|
|||||||
{
|
{
|
||||||
command: "initNotificationBar",
|
command: "initNotificationBar",
|
||||||
initData: expect.any(Object),
|
initData: expect.any(Object),
|
||||||
|
parentOrigin: expect.any(String),
|
||||||
},
|
},
|
||||||
"*",
|
overlayNotificationsContentService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -257,7 +258,7 @@ describe("OverlayNotificationsContentService", () => {
|
|||||||
|
|
||||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||||
{ command: "saveCipherAttemptCompleted", error: undefined },
|
{ command: "saveCipherAttemptCompleted", error: undefined },
|
||||||
"*",
|
overlayNotificationsContentService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
import {
|
import {
|
||||||
NotificationBarIframeInitData,
|
NotificationBarIframeInitData,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@@ -22,6 +23,7 @@ export class OverlayNotificationsContentService
|
|||||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||||
private notificationBarShadowRoot: ShadowRoot | null = null;
|
private notificationBarShadowRoot: ShadowRoot | null = null;
|
||||||
private currentNotificationBarType: NotificationType | null = null;
|
private currentNotificationBarType: NotificationType | null = null;
|
||||||
|
private readonly extensionOrigin: string;
|
||||||
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||||
height: "400px",
|
height: "400px",
|
||||||
width: "430px",
|
width: "430px",
|
||||||
@@ -61,6 +63,7 @@ export class OverlayNotificationsContentService
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||||
void sendExtensionMessage("checkNotificationQueue");
|
void sendExtensionMessage("checkNotificationQueue");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +184,10 @@ export class OverlayNotificationsContentService
|
|||||||
this.currentNotificationBarType = initData.type;
|
this.currentNotificationBarType = initData.type;
|
||||||
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
||||||
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
||||||
this.notificationBarIframeElement.src = chrome.runtime.getURL("notification/bar.html");
|
const parentOrigin = globalThis.location.origin;
|
||||||
|
const iframeUrl = new URL(BrowserApi.getRuntimeURL("notification/bar.html"));
|
||||||
|
iframeUrl.searchParams.set("parentOrigin", parentOrigin);
|
||||||
|
this.notificationBarIframeElement.src = iframeUrl.toString();
|
||||||
setElementStyles(
|
setElementStyles(
|
||||||
this.notificationBarIframeElement,
|
this.notificationBarIframeElement,
|
||||||
{
|
{
|
||||||
@@ -254,7 +260,11 @@ export class OverlayNotificationsContentService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendMessageToNotificationBarIframe({ command: "initNotificationBar", initData });
|
this.sendMessageToNotificationBarIframe({
|
||||||
|
command: "initNotificationBar",
|
||||||
|
initData,
|
||||||
|
parentOrigin: globalThis.location.origin,
|
||||||
|
});
|
||||||
globalThis.removeEventListener("message", handleInitNotificationBarMessage);
|
globalThis.removeEventListener("message", handleInitNotificationBarMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,7 +313,7 @@ export class OverlayNotificationsContentService
|
|||||||
*/
|
*/
|
||||||
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
||||||
if (this.notificationBarIframeElement) {
|
if (this.notificationBarIframeElement) {
|
||||||
this.notificationBarIframeElement.contentWindow.postMessage(message, "*");
|
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export function createInitAutofillInlineMenuButtonMessageMock(
|
|||||||
styleSheetUrl: "https://jest-testing-website.com",
|
styleSheetUrl: "https://jest-testing-website.com",
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
portKey: "portKey",
|
portKey: "portKey",
|
||||||
|
token: "test-token",
|
||||||
...customFields,
|
...customFields,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -212,6 +213,7 @@ export function createInitAutofillInlineMenuListMessageMock(
|
|||||||
theme: ThemeTypes.Light,
|
theme: ThemeTypes.Light,
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
portKey: "portKey",
|
portKey: "portKey",
|
||||||
|
token: "test-token",
|
||||||
inlineMenuFillType: CipherType.Login,
|
inlineMenuFillType: CipherType.Login,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
createAutofillOverlayCipherDataMock(1, {
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
|
||||||
export function triggerTestFailure() {
|
export function triggerTestFailure() {
|
||||||
expect(true).toBe("Test has failed.");
|
expect(true).toBe("Test has failed.");
|
||||||
}
|
}
|
||||||
@@ -11,7 +13,11 @@ export function flushPromises() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postWindowMessage(data: any, origin = "https://localhost/", source = window) {
|
export function postWindowMessage(
|
||||||
|
data: any,
|
||||||
|
origin: string = BrowserApi.getRuntimeURL("")?.slice(0, -1),
|
||||||
|
source: Window | MessageEventSource | null = window,
|
||||||
|
) {
|
||||||
globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source }));
|
globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user