1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-21 16:18:28 +01:00

[PM-12548] Fido2 scripts should not load when user is logged out (#11444)

* [PM-12548] Fido2 scripts should not load when user is logged out

* [PM-12548] Fido2 scripts should not load when user is logged out
This commit is contained in:
Cesar Gonzalez 2024-10-08 16:02:49 -05:00 committed by GitHub
parent fdfbe66513
commit a5c1a5a42f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 82 additions and 50 deletions

View File

@ -1484,9 +1484,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
/**
* Gets the user's authentication status from the auth service. If the user's authentication
* status has changed, the inline menu button's authentication status will be updated
* and the inline menu list's ciphers will be updated.
* Gets the user's authentication status from the auth service.
*/
private async getAuthStatus() {
return await firstValueFrom(this.authService.activeAccountStatus$);

View File

@ -45,7 +45,6 @@ type Fido2BackgroundExtensionMessageHandlers = {
interface Fido2Background {
init(): void;
injectFido2ContentScriptsInAllTabs(): Promise<void>;
}
export {

View File

@ -1,6 +1,8 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import {
@ -59,6 +61,8 @@ describe("Fido2Background", () => {
let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>;
let configServiceMock!: MockProxy<ConfigService>;
let enablePasskeysMock$!: BehaviorSubject<boolean>;
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
let authServiceMock!: MockProxy<AuthService>;
let fido2Background!: Fido2Background;
beforeEach(() => {
@ -81,6 +85,9 @@ describe("Fido2Background", () => {
vaultSettingsService.enablePasskeys$ = enablePasskeysMock$;
fido2ActiveRequestManager = mock<Fido2ActiveRequestManager>();
fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true);
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
authServiceMock = mock<AuthService>();
authServiceMock.activeAccountStatus$ = activeAccountStatusMock$;
fido2Background = new Fido2Background(
logService,
fido2ActiveRequestManager,
@ -88,6 +95,7 @@ describe("Fido2Background", () => {
vaultSettingsService,
scriptInjectorServiceMock,
configServiceMock,
authServiceMock,
);
fido2Background["abortManager"] = abortManagerMock;
abortManagerMock.runWithAbortController.mockImplementation((_requestId, runner) =>
@ -101,55 +109,31 @@ describe("Fido2Background", () => {
jest.clearAllMocks();
});
describe("injectFido2ContentScriptsInAllTabs", () => {
it("does not inject any FIDO2 content scripts when no tabs have a secure url protocol", async () => {
const insecureTab = mock<chrome.tabs.Tab>({ id: 789, url: "http://example.com" });
tabsQuerySpy.mockResolvedValueOnce([insecureTab]);
describe("handleAuthStatusUpdate", () => {
let updateContentScriptRegistrationSpy: jest.SpyInstance;
await fido2Background.injectFido2ContentScriptsInAllTabs();
expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled();
beforeEach(() => {
updateContentScriptRegistrationSpy = jest
.spyOn(fido2Background as any, "updateContentScriptRegistration")
.mockImplementation();
});
it("only injects the FIDO2 content script into tabs that contain a secure url protocol", async () => {
const secondTabMock = mock<chrome.tabs.Tab>({ id: 456, url: "https://example.com" });
const insecureTab = mock<chrome.tabs.Tab>({ id: 789, url: "http://example.com" });
const noUrlTab = mock<chrome.tabs.Tab>({ id: 101, url: undefined });
tabsQuerySpy.mockResolvedValueOnce([tabMock, secondTabMock, insecureTab, noUrlTab]);
it("skips triggering the passkeys settings update if the user is logged out", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
await fido2Background.injectFido2ContentScriptsInAllTabs();
fido2Background.init();
await flushPromises();
expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({
tabId: tabMock.id,
injectDetails: contentScriptDetails,
});
expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({
tabId: secondTabMock.id,
injectDetails: contentScriptDetails,
});
expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({
tabId: insecureTab.id,
injectDetails: contentScriptDetails,
});
expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({
tabId: noUrlTab.id,
injectDetails: contentScriptDetails,
});
expect(updateContentScriptRegistrationSpy).not.toHaveBeenCalled();
});
it("injects the `page-script.js` content script into the provided tab", async () => {
tabsQuerySpy.mockResolvedValueOnce([tabMock]);
it("triggers the passkeys setting update if the user is logged in", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
await fido2Background.injectFido2ContentScriptsInAllTabs();
fido2Background.init();
await flushPromises();
expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({
tabId: tabMock.id,
injectDetails: sharedScriptInjectionDetails,
mv2Details: { file: Fido2ContentScript.PageScriptAppend },
mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" },
});
expect(updateContentScriptRegistrationSpy).toHaveBeenCalled();
});
});
@ -157,6 +141,7 @@ describe("Fido2Background", () => {
let portMock!: MockProxy<chrome.runtime.Port>;
beforeEach(() => {
jest.spyOn(fido2Background as any, "handleAuthStatusUpdate").mockImplementation();
fido2Background.init();
jest.spyOn(BrowserApi, "registerContentScriptsMv2");
jest.spyOn(BrowserApi, "registerContentScriptsMv3");
@ -168,6 +153,15 @@ describe("Fido2Background", () => {
tabsQuerySpy.mockResolvedValue([tabMock]);
});
it("skips handling the passkey update if the user is logged out", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
enablePasskeysMock$.next(true);
expect(portMock.disconnect).not.toHaveBeenCalled();
expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled();
});
it("does not destroy and re-inject the content scripts when triggering `handleEnablePasskeysUpdate` with an undefined currentEnablePasskeysSetting property", async () => {
await flushPromises();
@ -421,6 +415,7 @@ describe("Fido2Background", () => {
let portMock!: MockProxy<chrome.runtime.Port>;
beforeEach(() => {
jest.spyOn(fido2Background as any, "handleAuthStatusUpdate").mockImplementation();
fido2Background.init();
portMock = createPortSpyMock(Fido2PortName.InjectedScript);
triggerRuntimeOnConnectEvent(portMock);

View File

@ -1,6 +1,8 @@
import { firstValueFrom, startWith } from "rxjs";
import { firstValueFrom, startWith, Subscription } from "rxjs";
import { pairwise } from "rxjs/operators";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
@ -29,6 +31,7 @@ import {
} from "./abstractions/fido2.background";
export class Fido2Background implements Fido2BackgroundInterface {
private currentAuthStatus$: Subscription;
private abortManager = new AbortManager();
private fido2ContentScriptPortsSet = new Set<chrome.runtime.Port>();
private registeredContentScripts: browser.contentScripts.RegisteredContentScript;
@ -55,6 +58,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
private vaultSettingsService: VaultSettingsService,
private scriptInjectorService: ScriptInjectorService,
private configService: ConfigService,
private authService: AuthService,
) {}
/**
@ -68,12 +72,32 @@ export class Fido2Background implements Fido2BackgroundInterface {
this.vaultSettingsService.enablePasskeys$
.pipe(startWith(undefined), pairwise())
.subscribe(([previous, current]) => this.handleEnablePasskeysUpdate(previous, current));
this.currentAuthStatus$ = this.authService.activeAccountStatus$
.pipe(startWith(undefined), pairwise())
.subscribe(([_previous, current]) => this.handleAuthStatusUpdate(current));
}
/**
* Handles initializing the FIDO2 content scripts based on the current
* authentication status. We only want to inject the FIDO2 content scripts
* if the user is logged in.
*
* @param authStatus - The current authentication status.
*/
private async handleAuthStatusUpdate(authStatus: AuthenticationStatus) {
if (authStatus === AuthenticationStatus.LoggedOut) {
return;
}
const enablePasskeys = await this.isPasskeySettingEnabled();
await this.handleEnablePasskeysUpdate(enablePasskeys, enablePasskeys);
this.currentAuthStatus$.unsubscribe();
}
/**
* Injects the FIDO2 content and page script into all existing browser tabs.
*/
async injectFido2ContentScriptsInAllTabs() {
private async injectFido2ContentScriptsInAllTabs() {
const tabs = await BrowserApi.tabsQuery({});
for (let index = 0; index < tabs.length; index++) {
@ -85,6 +109,13 @@ export class Fido2Background implements Fido2BackgroundInterface {
}
}
/**
* Gets the user's authentication status from the auth service.
*/
private async getAuthStatus() {
return await firstValueFrom(this.authService.activeAccountStatus$);
}
/**
* Handles reacting to the enablePasskeys setting being updated. If the setting
* is enabled, the FIDO2 content scripts are injected into all tabs. If the setting
@ -98,13 +129,17 @@ export class Fido2Background implements Fido2BackgroundInterface {
previousEnablePasskeysSetting: boolean,
enablePasskeys: boolean,
) {
this.fido2ActiveRequestManager.removeAllActiveRequests();
await this.updateContentScriptRegistration();
if ((await this.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
return;
}
if (previousEnablePasskeysSetting === undefined) {
return;
}
this.fido2ActiveRequestManager.removeAllActiveRequests();
await this.updateContentScriptRegistration();
this.destroyLoadedFido2ContentScripts();
if (enablePasskeys) {
void this.injectFido2ContentScriptsInAllTabs();

View File

@ -9,6 +9,7 @@
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
script.async = false;
const scriptInsertionPoint =
globalContext.document.head || globalContext.document.documentElement;

View File

@ -9,6 +9,7 @@
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
script.async = false;
// We are ensuring that the script injection is delayed in the event that we are loading
// within an iframe element. This prevents an issue with web mail clients that load content

View File

@ -4,6 +4,12 @@ import { MessageType } from "./messaging/message";
import { Messenger } from "./messaging/messenger";
(function (globalContext) {
if (globalContext.document.currentScript) {
globalContext.document.currentScript.parentNode.removeChild(
globalContext.document.currentScript,
);
}
const shouldExecuteContentScript =
globalContext.document.contentType === "text/html" &&
(globalContext.document.location.protocol === "https:" ||

View File

@ -1103,6 +1103,7 @@ export default class MainBackground {
this.vaultSettingsService,
this.scriptInjectorService,
this.configService,
this.authService,
);
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
@ -1118,7 +1119,6 @@ export default class MainBackground {
this.messagingService,
this.logService,
this.configService,
this.fido2Background,
messageListener,
this.accountService,
lockService,

View File

@ -21,7 +21,6 @@ import {
openTwoFactorAuthPopout,
} from "../auth/popup/utils/auth-popout-window";
import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
import { Fido2Background } from "../autofill/fido2/background/abstractions/fido2.background";
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../platform/browser/browser-api";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
@ -46,7 +45,6 @@ export default class RuntimeBackground {
private messagingService: MessagingService,
private logService: LogService,
private configService: ConfigService,
private fido2Background: Fido2Background,
private messageListener: MessageListener,
private accountService: AccountService,
private readonly lockService: LockService,
@ -365,7 +363,6 @@ export default class RuntimeBackground {
private async checkOnInstalled() {
setTimeout(async () => {
void this.fido2Background.injectFido2ContentScriptsInAllTabs();
void this.autofillService.loadAutofillScriptsOnInstall();
if (this.onInstalledReason != null) {