Skip to content
Get startedGetting started →

Use mera with Chrome extensions

There are a few integration options for mera for Chrome extensions, each with its own trade-offs.

The relying party ID decides who the passkeys belong to. For an extension, there are two options:

  • extension ID
  • host URL (e.g., a website such as account.example.com)

With the extension ID, the passkey can only be used inside the extension. A website or a mobile app won’t be able to reuse them. The ID must also never change.

With a URL, the same passkeys work on the website and in the extension. Chrome 122+ lets the extension use them when the manifest lists that exact host:

{
"minimum_chrome_version": "122",
"host_permissions": ["https://account.example.com/*"]
}

The ceremony can run on an extension page or on the relying party website page.

On an extension page, Chrome shows its own passkey prompt. Password managers that watch websites and intercept WebAuthn requests, such as 1Password, do not see prompts on extension pages, so the requests are handled by Chrome’s own prompt, which may not allow routing to arbitrary authenticators. At the moment of writing, it only allowed authenticating with Chrome’s built-in password manager or Apple Passwords (Platform’s manager) and didn’t allow routing to a browser extension.

On the website, the extension opens a page. The page runs the ceremony and sends the PRF output back with postMessage. This way 3rd-party password managers like 1Password can intercept the request. The trade-off is that the host must serve that page and that the PRF output crosses a message channel, so both ends must pin who they talk to. The extension must accept a message only from the website origin, and only from the window it opened:

const
const passkeyWindow: Window | null
passkeyWindow
=
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
function open(url?: string | URL, target?: string, features?: string): WindowProxy | null

The open() method of the Window interface loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name.

MDN Reference

open
("https://account.example.com/passkey");
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
addEventListener<"message">(type: "message", listener: (this: Window, ev: MessageEvent<any>) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
("message", (
event: MessageEvent<any>
event
) => {
if (
event: MessageEvent<any>
event
.
MessageEvent<any>.origin: string

The origin read-only property of the origin of the message emitter.

MDN Reference

origin
!== "https://account.example.com") return;
if (
event: MessageEvent<any>
event
.
MessageEvent<any>.source: MessageEventSource | null

The source read-only property of the a WindowProxy, MessagePort, or a MessageEventSource (which can be a WindowProxy, message emitter.

MDN Reference

source
!==
const passkeyWindow: Window | null
passkeyWindow
) return;
});

The page must post only to the extension’s origin, with the extension ID as a fixed string:

var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
opener: any

The Window interface's opener property returns a reference to the window that opened the window, either with Window.open, or by navigating a link with a target attribute.

MDN Reference

opener
.
any
postMessage
(
const prfOutput: Uint8Array<ArrayBufferLike>
prfOutput
, "chrome-extension://EXTENSION_ID");
  • When running a WebAuthn ceremony in the extension side panel, it often shows no prompt at all, and the request hangs.

The Chrome extension demo binds passkeys to mera.category.xyz. It opens a page on that host and receives the PRF output with postMessage. A key in the demo manifest fixes the extension ID, and the page posts only to that ID.