diff --git a/apps/browser/src/content/webauthn/content-script.ts b/apps/browser/src/content/webauthn/content-script.ts index 29489164d3..eafb3106d7 100644 --- a/apps/browser/src/content/webauthn/content-script.ts +++ b/apps/browser/src/content/webauthn/content-script.ts @@ -1,6 +1,22 @@ +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + // eslint-disable-next-line no-console console.log("content-script loaded"); const s = document.createElement("script"); s.src = chrome.runtime.getURL("content/webauthn/page-script.js"); (document.head || document.documentElement).appendChild(s); + +const messenger = Messenger.createInExtensionContext(window, chrome.runtime.connect()); + +messenger.addHandler(async (message) => { + if (message.type === MessageType.CredentialCreationRequest) { + return { + type: MessageType.CredentialCreationResponse, + approved: true, + }; + } + + return undefined; +}); diff --git a/apps/browser/src/content/webauthn/messaging/message.ts b/apps/browser/src/content/webauthn/messaging/message.ts new file mode 100644 index 0000000000..9b0a8065fb --- /dev/null +++ b/apps/browser/src/content/webauthn/messaging/message.ts @@ -0,0 +1,42 @@ +export enum MessageType { + CredentialCreationRequest, + CredentialCreationResponse, + CredentialGetRequest, + CredentialGetResponse, + AbortRequest, + AbortResponse, +} + +export type CredentialCreationRequest = { + type: MessageType.CredentialCreationRequest; + rpId: string; +}; + +export type CredentialCreationResponse = { + type: MessageType.CredentialCreationResponse; + approved: boolean; +}; + +export type CredentialGetRequest = { + type: MessageType.CredentialGetRequest; +}; + +export type CredentialGetResponse = { + type: MessageType.CredentialGetResponse; +}; + +export type AbortRequest = { + type: MessageType.AbortRequest; +}; + +export type AbortResponse = { + type: MessageType.AbortResponse; +}; + +export type Message = + | CredentialCreationRequest + | CredentialCreationResponse + | CredentialGetRequest + | CredentialGetResponse + | AbortRequest + | AbortResponse; diff --git a/apps/browser/src/content/webauthn/messaging/messenger.ts b/apps/browser/src/content/webauthn/messaging/messenger.ts new file mode 100644 index 0000000000..bbba5d067a --- /dev/null +++ b/apps/browser/src/content/webauthn/messaging/messenger.ts @@ -0,0 +1,83 @@ +import { concatMap, filter, firstValueFrom, Observable } from "rxjs"; + +import { Message } from "./message"; + +type PostMessageFunction = Window["postMessage"] | chrome.runtime.Port["postMessage"]; + +type Channel = { + messages$: Observable; + postMessage: PostMessageFunction; +}; + +type Metadata = { requestId: string }; +type MessageWithMetadata = Message & { metadata: Metadata }; + +// TODO: This class probably duplicates functionality but I'm not especially familiar with +// the inner workings of the browser extension yet. +// If you see this in a code review please comment on it! + +export class Messenger { + static createInPageContext(window: Window) { + return new Messenger({ + postMessage: window.postMessage.bind(window), + messages$: new Observable((subscriber) => { + const eventListener = (event: MessageEvent) => { + subscriber.next(event.data); + }; + + window.addEventListener("message", eventListener); + + return () => window.removeEventListener("message", eventListener); + }), + }); + } + + static createInExtensionContext(window: Window, port: chrome.runtime.Port) { + return new Messenger({ + postMessage: window.postMessage.bind(window), + messages$: new Observable((subscriber) => { + const eventListener = (event: MessageEvent) => { + subscriber.next(event.data); + }; + + window.addEventListener("message", eventListener); + + return () => window.removeEventListener("message", eventListener); + }), + }); + } + + private constructor(private channel: Channel) {} + + request(request: Message): Promise { + const requestId = Date.now().toString(); + const metadata: Metadata = { requestId }; + + const promise = firstValueFrom( + this.channel.messages$.pipe( + filter((m) => m.metadata.requestId === requestId && m.type !== request.type) + ) + ); + + this.channel.postMessage({ ...request, metadata }); + + return promise; + } + + addHandler(handler: (message: Message) => Promise) { + this.channel.messages$ + .pipe( + concatMap(async (message) => { + const handlerResponse = await handler(message); + + if (handlerResponse === undefined) { + return; + } + + const metadata: Metadata = { requestId: message.metadata.requestId }; + this.channel.postMessage({ ...handlerResponse, metadata }); + }) + ) + .subscribe(); + } +} diff --git a/apps/browser/src/content/webauthn/page-script.ts b/apps/browser/src/content/webauthn/page-script.ts index 6a6e013387..b501b6abde 100644 --- a/apps/browser/src/content/webauthn/page-script.ts +++ b/apps/browser/src/content/webauthn/page-script.ts @@ -1,3 +1,6 @@ +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + // eslint-disable-next-line no-console console.log("page-script loaded"); @@ -6,16 +9,17 @@ const browserCredentials = { get: navigator.credentials.get.bind(navigator.credentials), }; -// Intercept +const messenger = Messenger.createInPageContext(window); navigator.credentials.create = async (options?: CredentialCreationOptions): Promise => { - alert("Intercepted: create"); + await messenger.request({ + type: MessageType.CredentialCreationRequest, + rpId: options.publicKey.rp.id, + }); return await browserCredentials.create(options); }; navigator.credentials.get = async (options?: CredentialRequestOptions): Promise => { - alert("Intercepted: get"); - return await browserCredentials.get(options); };