1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 18:33:50 +00:00

Extension token URL service.

This commit is contained in:
Miles Blackwood
2025-11-05 16:26:06 -05:00
parent b13f1e6dcf
commit 9499fead1e
7 changed files with 117 additions and 2 deletions

View File

@@ -53,6 +53,7 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
// eslint-disable-next-line no-restricted-imports
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { BrowserApi } from "../../platform/browser/browser-api";
import { ExtensionUrlTokenService } from "../../platform/services/extension-url-token.service";
// FIXME (PM-22628): Popup imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
import {
@@ -236,6 +237,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private accountService: AccountService,
private generatePasswordCallback: () => Promise<string>,
private addPasswordCallback: (password: string) => Promise<void>,
private extensionUrlTokenService: ExtensionUrlTokenService,
) {
this.initOverlayEventObservables();
}
@@ -2947,7 +2949,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.postMessageToPort(port, {
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
iframeUrl: chrome.runtime.getURL(
iframeUrl: this.extensionUrlTokenService.createTokenUrl(
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
),
pageTitle: chrome.i18n.getMessage(

View File

@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { validateExtensionUrl } from "@bitwarden/browser/platform/utils/extension-url-token.utils";
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { setElementStyles } from "../../../../utils";
@@ -60,7 +61,14 @@ export class AutofillInlineMenuContainer {
*
* @param message - The message containing the iframe url and page title.
*/
private handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) {
private async handleInitInlineMenuIframe(message: InitAutofillInlineMenuElementMessage) {
const isValidUrl = await validateExtensionUrl(message.iframeUrl);
if (!isValidUrl) {
// eslint-disable-next-line no-console
console.error("Token not found");
return;
}
this.defaultIframeAttributes.src = message.iframeUrl;
this.defaultIframeAttributes.title = message.pageTitle;
this.portName = message.portName;

View File

@@ -317,6 +317,7 @@ import BrowserInitialInstallService from "../platform/services/browser-initial-i
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
import { ExtensionUrlTokenService } from "../platform/services/extension-url-token.service";
import I18nService from "../platform/services/i18n.service";
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
@@ -452,6 +453,7 @@ export default class MainBackground {
syncServiceListener: SyncServiceListener;
browserInitialInstallService: BrowserInitialInstallService;
backgroundSyncService: BackgroundSyncService;
extensionUrlTokenService: ExtensionUrlTokenService;
webPushConnectionService: WorkerWebPushConnectionService | UnsupportedWebPushConnectionService;
themeStateService: DefaultThemeStateService;
@@ -1140,6 +1142,8 @@ export default class MainBackground {
this.browserInitialInstallService = new BrowserInitialInstallService(this.stateProvider);
this.extensionUrlTokenService = new ExtensionUrlTokenService();
if (BrowserApi.isManifestVersion(3)) {
const registration = (self as unknown as { registration: ServiceWorkerRegistration })
?.registration;
@@ -1962,6 +1966,7 @@ export default class MainBackground {
this.accountService,
() => this.generatePassword(),
(password) => this.addPasswordToHistory(password),
this.extensionUrlTokenService,
);
this.autofillBadgeUpdaterService = new AutofillBadgeUpdaterService(

View File

@@ -80,6 +80,9 @@ export default class RuntimeBackground {
BiometricsCommands.GetBiometricsStatusForUser,
BiometricsCommands.CanEnableBiometricUnlock,
"getUserPremiumStatus",
"createTokenExtensionUrl",
"validateExtensionUrl",
"revokeExtensionUrlToken",
];
if (messagesWithResponse.includes(msg.command)) {
@@ -213,6 +216,16 @@ export default class RuntimeBackground {
);
return result;
}
case "createTokenExtensionUrl": {
return this.main.extensionUrlTokenService.createTokenUrl(msg.path);
}
case "validateExtensionUrl": {
return this.main.extensionUrlTokenService.validateUrl(msg.url);
}
case "revokeExtensionUrlToken": {
this.main.extensionUrlTokenService.revokeToken(msg.url);
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
export abstract class ExtensionUrlTokenService {
abstract generateToken(): string;
abstract createTokenUrl(path: string): string;
abstract validateUrl(url: string): boolean;
abstract revokeToken(url: string): void;
abstract extractToken(url: string): string | null;
}

View File

@@ -0,0 +1,40 @@
import { ExtensionUrlTokenService as ExtensionUrlTokenServiceAbstraction } from "./abstractions/extension-url-token.service";
export class ExtensionUrlTokenService implements ExtensionUrlTokenServiceAbstraction {
private validTokens = new Set<string>();
generateToken(): string {
const token = crypto.randomUUID();
this.validTokens.add(token);
return token;
}
createTokenUrl(path: string): string {
const token = this.generateToken();
const baseUrl = chrome.runtime.getURL(path);
const url = new URL(baseUrl);
url.searchParams.set("token", token);
return url.toString();
}
validateUrl(url: string): boolean {
const token = this.extractToken(url);
return token !== null && this.validTokens.has(token);
}
revokeToken(url: string): void {
const token = this.extractToken(url);
if (token) {
this.validTokens.delete(token);
}
}
extractToken(url: string): string | null {
try {
const urlObj = new URL(url);
return urlObj.searchParams.get("token");
} catch {
return null;
}
}
}

View File

@@ -0,0 +1,36 @@
export async function createTokenExtensionUrl(path: string): Promise<string> {
const response = await chrome.runtime.sendMessage({
command: "createTokenExtensionUrl",
path: path,
});
if (response?.error) {
throw new Error(response.error.message || "Failed to create token URL");
}
return response.result;
}
export async function validateExtensionUrl(url: string): Promise<boolean> {
const response = await chrome.runtime.sendMessage({
command: "validateExtensionUrl",
url: url,
});
if (response?.error) {
throw new Error(response.error.message || "Failed to validate URL");
}
return response.result;
}
export async function revokeExtensionUrlToken(url: string): Promise<void> {
const response = await chrome.runtime.sendMessage({
command: "revokeExtensionUrlToken",
url: url,
});
if (response?.error) {
throw new Error(response.error.message || "Failed to revoke token");
}
}