diff --git a/packages/env/server.ts b/packages/env/server.ts index e360b13330b..624aecf57e3 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -80,6 +80,12 @@ function createServerEnv() { .string() .optional() .describe("Chrome Web Store extension id allowed to receive auth keys"), + CAP_FIREFOX_EXTENSION_ID: z + .string() + .optional() + .describe( + "Firefox identity redirect subdomain (the in https://.extensions.allizom.org, derived from the gecko extension id) allowed to receive auth keys", + ), CAP_ALLOWED_SIGNUP_DOMAINS: z .string() .optional() diff --git a/packages/web-backend/src/Extension/Http.ts b/packages/web-backend/src/Extension/Http.ts index f920710f871..ff0b8b16523 100644 --- a/packages/web-backend/src/Extension/Http.ts +++ b/packages/web-backend/src/Extension/Http.ts @@ -20,7 +20,18 @@ import { handleDomainError } from "../Http/Errors.ts"; import { Videos } from "../Videos/index.ts"; import { Extensions } from "./Extensions.ts"; -const CHROMIUM_IDENTITY_HOST_SUFFIX = ".chromiumapp.org"; +// Each browser's identity.launchWebAuthFlow intercepts redirects to its own +// synthetic host; the leading label identifies the extension installation. +const IDENTITY_REDIRECT_HOSTS = [ + { + suffix: ".chromiumapp.org", + getConfiguredExtensionId: () => serverEnv().CAP_CHROME_EXTENSION_ID, + }, + { + suffix: ".extensions.allizom.org", + getConfiguredExtensionId: () => serverEnv().CAP_FIREFOX_EXTENSION_ID, + }, +] as const; const validateExtensionRedirectUri = (redirectUri: string) => Effect.gen(function* () { @@ -29,18 +40,15 @@ const validateExtensionRedirectUri = (redirectUri: string) => catch: () => new HttpApiError.BadRequest(), }); - if ( - url.protocol !== "https:" || - !url.hostname.endsWith(CHROMIUM_IDENTITY_HOST_SUFFIX) - ) { + const identityHost = IDENTITY_REDIRECT_HOSTS.find(({ suffix }) => + url.hostname.endsWith(suffix), + ); + if (url.protocol !== "https:" || !identityHost) { return yield* new HttpApiError.BadRequest(); } - const extensionId = url.hostname.slice( - 0, - -CHROMIUM_IDENTITY_HOST_SUFFIX.length, - ); - const configuredExtensionId = serverEnv().CAP_CHROME_EXTENSION_ID; + const extensionId = url.hostname.slice(0, -identityHost.suffix.length); + const configuredExtensionId = identityHost.getConfiguredExtensionId(); if (configuredExtensionId) { if (extensionId !== configuredExtensionId) { @@ -53,7 +61,8 @@ const validateExtensionRedirectUri = (redirectUri: string) => // signed-in user's auth key through this flow. The only deployment // where accepting an arbitrary id is safe is localhost development; // every reachable deployment (staging, previews, self-hosted) must set - // CAP_CHROME_EXTENSION_ID regardless of NODE_ENV. + // CAP_CHROME_EXTENSION_ID / CAP_FIREFOX_EXTENSION_ID regardless of + // NODE_ENV. const webHostname = new URL(serverEnv().WEB_URL).hostname; const isLocalDevelopment = serverEnv().NODE_ENV !== "production" && @@ -151,7 +160,7 @@ const renderConsentPage = ({
-

Connect the Cap Chrome extension

+

Connect the Cap browser extension

The Cap extension is asking for access to your Cap account to create and upload recordings on your behalf.

Only continue if you opened this page from the Cap extension.