3aafd13289fb

pass through GET requests with no matching Peach credential instead of rejecting NotAllowedError

time
2026-05-30 21:09 UTC · 4 months ago
machine
koh2
agent
claude-sonnet-4-6
session
passkey google create fix
parent
ea3d3ceb520a
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey-chrome-ceremony.test.ts+12 −8src/background/passkey.ts+23 −10
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach Password Manager",5-  "version": "3.6.140",5+  "version": "3.6.141",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "3.6.140",4+  "version": "3.6.141",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.140",3+  "version": "3.6.141",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/passkey-chrome-ceremony.test.ts+12 −8
@@ -512,13 +512,15 @@512512     return { entry, credentialId };513513   }514514 515-  it("get returns NotAllowedError when no stored credential matches the rpId", async () => {515+  it("get passes through to platform when no stored credential matches the rpId", async () => {516516     const vault = await import("./vault");517517     await vault.createVault("correct-pass-12345");518518 519519     fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 200);520520     const done = await waitForGetComplete(200);521-    expect(done.error).toMatchObject({ name: "NotAllowedError" });521+    // Pass-through: no error and no responseJson — Chrome routes to next authenticator.522+    expect(done.error).toBeUndefined();523+    expect(done.responseJson).toBeUndefined();522524   });523525 524526   it("get encodes the active tab page origin into clientDataJSON (not https://<rpId>)", async () => {@@ -552,7 +554,7 @@552554     expect(after!.passkey!.lastUsedAt).toBeDefined();553555   });554556 555-  it("get rejects with NotAllowedError when the stored passkey has no private key (Firefox-created)", async () => {557+  it("get passes through to platform when the stored passkey has no private key (Firefox-created)", async () => {556558     const vault = await import("./vault");557559     await vault.createVault("correct-pass-12345");558560     const { base64urlEncode, generatePasskeyKeypair } = await import("./passkey-crypto");@@ -583,7 +585,9 @@583585 584586     fireGet({ rpId: "webauthn.io", challenge: "Y2g", allowCredentials: [] }, 203);585587     const done = await waitForGetComplete(203);586-    expect(done.error).toMatchObject({ name: "NotAllowedError" });588+    // Pass-through: no error and no responseJson — Chrome routes to next authenticator.589+    expect(done.error).toBeUndefined();590+    expect(done.responseJson).toBeUndefined();587591   });588592 589593   it("get on a locked vault parks the request and proceeds after unlock", async () => {@@ -618,19 +622,19 @@618622   // "Invalid requestId" survival — conditional-UI gets cancel fast.619623   // ─────────────────────────────────────────────────────────────────────620624 621-  it("rejectGet swallows 'Invalid requestId' from a cancelled conditional-UI get without throwing", async () => {625+  it("pass-through swallows 'Invalid requestId' from a cancelled conditional-UI get without throwing", async () => {622626     const vault = await import("./vault");623627     await vault.createVault("correct-pass-12345");624628 625-    // Simulate Chrome cancelling the request between handleGetRequest626-    // running and rejectGet calling completeGetRequest.629+    // Simulate Chrome cancelling the request between handleGetRequest running630+    // and the pass-through completeGetRequest call landing.627631     const ce = (globalThis as { chrome: typeof chrome }).chrome.webAuthenticationProxy628632       .completeGetRequest as unknown as ReturnType<typeof vi.fn>;629633     ce.mockImplementationOnce(async () => {630634       throw new Error("Invalid requestId");631635     });632636 633-    // No matching passkey for this rpId → goes straight to rejectGet,637+    // No matching passkey for this rpId → pass-through completeGetRequest call,634638     // which must NOT propagate the Invalid requestId error.635639     let resolved = false;636640     proxyListeners.get!({

view file

src/background/passkey.ts+23 −10
@@ -675,21 +675,34 @@675675   });676676 677677   if (!match) {678-    await rejectGet(request.requestId, "NotAllowedError", "No matching passkey found.");678+    // No Peach passkey for this RP — pass through to the platform authenticator.679+    // completeGetRequest with no error and no responseJson is the webAuthenticationProxy680+    // signal for "I'm not handling this"; Chrome routes the request to the next681+    // authenticator in the chain (the built-in browser passkey manager etc.).682+    // Rejecting with NotAllowedError would propagate as an error to the page and683+    // break RPs (e.g. Google) that issue a GET as part of their create-passkey flow.684+    console.log("[Peach passkey] no matching credential — passing through to platform", { requestId: request.requestId });685+    try {686+      await chrome.webAuthenticationProxy.completeGetRequest({ requestId: request.requestId });687+    } catch (err) {688+      if (isInvalidRequestIdError(err)) return;689+      throw err;690+    }679691     return;680692   }681693 682694   const credential = match.passkey;683-  // Chrome's webAuthenticationProxy path requires Peach-held private key684-  // material to sign the assertion. Firefox-created passkeys delegate to the685-  // platform authenticator — the browser will route to the platform UI686-  // automatically when we reject, which is the right outcome.695+  // Chrome's webAuthenticationProxy path requires Peach-held private key material.696+  // Passkeys created on Firefox have no privateKeyJwk — pass through so Chrome697+  // can route to a capable authenticator instead of surfacing an error to the page.687698   if (!credential.privateKeyJwk) {688-    await rejectGet(689-      request.requestId,690-      "NotAllowedError",691-      "This passkey was created on Firefox; sign in via the platform authenticator."692-    );699+    console.log("[Peach passkey] no private key (Firefox passkey) — passing through to platform", { requestId: request.requestId });700+    try {701+      await chrome.webAuthenticationProxy.completeGetRequest({ requestId: request.requestId });702+    } catch (err) {703+      if (isInvalidRequestIdError(err)) return;704+      throw err;705+    }693706     return;694707   }695708 

view file