Skip to content

feat: allow browser extension origins in auth requests - #3907

Open
apirJS wants to merge 2 commits into
HeyPuter:mainfrom
apirJS:feat/allow-requests-from-browser-ext
Open

apirJS wants to merge 2 commits into
HeyPuter:mainfrom
apirJS:feat/allow-requests-from-browser-ext

Conversation

@apirJS

@apirJS apirJS commented Sep 19, 2026

Copy link
Copy Markdown

feat: allow browser extension origins in auth requests

Problem

Browser extensions sending requests to /auth/get-user-app-token with their native origin (e.g. chrome-extension://cafneielldmiliebnkhaeaaibinihgpb) receive a 400:

{
  "error": "Invalid origin URL",
  "message": "Invalid origin URL",
  "code": "bad_request"
}

This happens because AuthService.#originFromUrl only accepted http: and https: protocols. Browser extensions are legitimate OAuth-style clients and should be able to obtain app tokens the same way web origins do.

Changes

Extend the protocol allow-list from ['http:', 'https:'] to include browser extension schemes:

Scheme Browser
chrome-extension: Chrome / Chromium
moz-extension: Firefox
safari-extension: Safari App Extensions (legacy)
safari-web-extension: Safari Web Extensions (modern)
extension: Generic / Chromium forks

The allow-list is used in three places - all three are updated consistently:

  • AuthService.#originFromUrl - gatekeeps appUidFromOrigin(), which is the entry point that returned the error
  • AppStore.createFromOrigin - validates origin before persisting it as index_url on bootstrap app rows
  • AppDriver.#validateInput - validates index_url on developer-created/updated app rows

DRY extraction

The protocol array was duplicated across all three files. This PR extracts it into a single exported constant:

// src/backend/util/validation.js
export const WEB_AND_EXTENSION_PROTOCOLS = [
    'http:', 'https:',
    'chrome-extension:', 'moz-extension:',
    'safari-extension:', 'safari-web-extension:',
    'extension:',
];

All three consumers now import and reference this constant.

Security

  • No weakening of existing constraints. Dangerous schemes (javascript:, data:, file:, vbscript:) remain rejected - the existing negative tests confirm this.
  • hostname guard. AuthService.#originFromUrl checks parsed.hostname is non-empty after the protocol check. All extension schemes produce a hostname (the extension ID), so this passes correctly while still blocking edge-case schemes that parse without a host.
  • Opaque origins. Browser extension schemes produce opaque origins per the WHATWG URL spec - they cannot read cookies, storage, or DOM from the parent frame when loaded in an iframe. An extension's ID is cryptographically tied to its signing key (Chrome/Firefox) or developer account (Safari), so the origin is not user-injectable.

How existing callers are unaffected

  • http: and https: origins work identically - the allow-list is a strict superset.
  • No changes to response shapes, error codes, or token format.
  • The validateUrl utility's default protocols parameter (['http:', 'https:']) is unchanged; only callers that explicitly pass the extended list are affected.

Tests

Layer File What's tested
Service AuthService.test.ts appUidFromOrigin resolves 5 extension origins to deterministic app UIDs
Store AppStore.test.js createFromOrigin accepts a chrome-extension:// origin and persists it
Controller AuthController.test.ts handleGetUserAppToken end-to-end with an extension origin -> token + bootstrap row

Add chrome-extension://, moz-extension://, safari-extension://, safari-web-extension://, and extension:// to the protocol allow-list so browser extensions can obtain app tokens via /auth/get-user-app-token.

Extract WEB_AND_EXTENSION_PROTOCOLS constant in validation.js so the allow-list is defined once and shared by AuthService, AppStore, and AppDriver.
@apirJS
apirJS requested a review from Salazareo as a code owner September 19, 2026 21:05
@CLAassistant

CLAassistant commented Sep 19, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Salazareo

Copy link
Copy Markdown
Member

looks sane, just reviewing now

Review follow-ups on the extension-origin change:

- Require a host in `validateUrl`. Only "special" schemes need an
  authority, so `chrome-extension:` parsed with an empty hostname and
  slipped past the reserved-system-host guard in AppDriver.
- Lowercase the host in `AuthService#normalizedOrigin`. `new URL()`
  lowercases http(s) hosts but leaves opaque ones alone, so one
  extension in two spellings resolved to two app uids — two AppData
  trees, two permission sets — and missed the origin blocklist.
- Drop `extension:`. No browser emits it, and it accepted
  `extension://evil.com` as an app origin.
- Freeze the allow-list and derive `validateUrl`'s http(s) default from
  `WEB_PROTOCOLS` so the two spellings can't drift apart.
- Pin the tests to the real uid derivation, use unique extension ids so
  a row left by another test can't mask the bootstrap path, and cover
  the host-less and near-miss schemes.
- Fix the prettier/eslint failure in AppStore.js.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants