-
Notifications
You must be signed in to change notification settings - Fork 0
fe fix for passkey issue #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,11 @@ import { useNavigate } from "react-router-dom"; | |||||||||
| import { z } from "zod"; | ||||||||||
| import { BASE_URL } from "../config"; | ||||||||||
| import { useAuth } from "../context/AuthContext"; | ||||||||||
| import { base64urlToUint8Array, bufferToBase64url } from "../utils/webauthn"; | ||||||||||
| import { | ||||||||||
| base64urlToUint8Array, | ||||||||||
| bufferToBase64url, | ||||||||||
| validateWebAuthnOptionsRpId, | ||||||||||
| } from "../utils/webauthn"; | ||||||||||
|
|
||||||||||
| const schema = z.object({ | ||||||||||
| email: z.email({ error: "Invalid email" }), | ||||||||||
|
|
@@ -108,9 +112,16 @@ const Signup = () => { | |||||||||
| const rawOptions = (await optionsRes.json()) as { | ||||||||||
| challenge: string; | ||||||||||
| user: { id: string; name: string; displayName: string }; | ||||||||||
| rp?: { id?: string; name?: string }; | ||||||||||
| [key: string]: unknown; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const rpIdValidation = validateWebAuthnOptionsRpId(rawOptions); | ||||||||||
| if (!rpIdValidation.isValid) { | ||||||||||
| console.error("Invalid WebAuthn register RP ID", rpIdValidation); | ||||||||||
| throw new Error(rpIdValidation.message); | ||||||||||
|
||||||||||
| throw new Error(rpIdValidation.message); | |
| throw new Error( | |
| rpIdValidation.message ?? "Invalid WebAuthn register RP ID", | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { | ||
| formatWebAuthnRpIdError, | ||
| getWebAuthnRpId, | ||
| validateWebAuthnOptionsRpId, | ||
| validateWebAuthnRpId, | ||
| } from "./webauthn"; | ||
|
|
||
| describe("WebAuthn RP ID validation", () => { | ||
| it("accepts an RP ID that matches the current hostname", () => { | ||
| expect( | ||
| validateWebAuthnRpId( | ||
| "rc-store.benhalverson.dev", | ||
| "rc-store.benhalverson.dev", | ||
| ), | ||
| ).toMatchObject({ isValid: true, rpId: "rc-store.benhalverson.dev" }); | ||
| }); | ||
|
|
||
| it("accepts a parent-domain RP ID", () => { | ||
| expect( | ||
| validateWebAuthnRpId("benhalverson.dev", "rc-store.benhalverson.dev"), | ||
| ).toMatchObject({ isValid: true, rpId: "benhalverson.dev" }); | ||
| }); | ||
|
|
||
| it("rejects localhost when the current host is deployed", () => { | ||
| expect( | ||
| validateWebAuthnRpId("localhost", "rc-store.benhalverson.dev"), | ||
| ).toMatchObject({ | ||
| isValid: false, | ||
| rpId: "localhost", | ||
| message: formatWebAuthnRpIdError({ | ||
| rpId: "localhost", | ||
| currentHostname: "rc-store.benhalverson.dev", | ||
| }), | ||
| }); | ||
| }); | ||
|
|
||
| it("rejects URL-shaped RP IDs", () => { | ||
| expect( | ||
| validateWebAuthnRpId( | ||
| "https://rc-store.benhalverson.dev", | ||
| "rc-store.benhalverson.dev", | ||
| ), | ||
| ).toMatchObject({ | ||
| isValid: false, | ||
| rpId: "https://rc-store.benhalverson.dev", | ||
| }); | ||
| }); | ||
|
|
||
| it("extracts RP ID from either auth or registration payloads", () => { | ||
| expect(getWebAuthnRpId({ rpId: "example.com" })).toBe("example.com"); | ||
| expect(getWebAuthnRpId({ rp: { id: "example.com" } })).toBe( | ||
| "example.com", | ||
| ); | ||
| }); | ||
|
|
||
| it("validates nested registration RP IDs", () => { | ||
| expect( | ||
| validateWebAuthnOptionsRpId( | ||
| { rp: { id: "https://rc-store.benhalverson.dev" } }, | ||
| "rc-store.benhalverson.dev", | ||
| ), | ||
| ).toMatchObject({ | ||
| isValid: false, | ||
| rpId: "https://rc-store.benhalverson.dev", | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,3 +16,105 @@ export const bufferToBase64url = (buffer: ArrayBuffer): string => { | |||||||||||||||||||||||||||||||||||||||
| .replace(/\//g, "_") | ||||||||||||||||||||||||||||||||||||||||
| .replace(/=/g, ""); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| interface WebAuthnRpEntity { | ||||||||||||||||||||||||||||||||||||||||
| id?: string; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| interface WebAuthnRpIdCarrier { | ||||||||||||||||||||||||||||||||||||||||
| rpId?: string; | ||||||||||||||||||||||||||||||||||||||||
| rp?: WebAuthnRpEntity; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| interface WebAuthnRpIdValidationResult { | ||||||||||||||||||||||||||||||||||||||||
| isValid: boolean; | ||||||||||||||||||||||||||||||||||||||||
| message?: string; | ||||||||||||||||||||||||||||||||||||||||
| rpId?: string; | ||||||||||||||||||||||||||||||||||||||||
| currentHostname: string; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+34
|
||||||||||||||||||||||||||||||||||||||||
| interface WebAuthnRpIdValidationResult { | |
| isValid: boolean; | |
| message?: string; | |
| rpId?: string; | |
| currentHostname: string; | |
| } | |
| type WebAuthnRpIdValidationResult = | |
| | { | |
| isValid: true; | |
| currentHostname: string; | |
| rpId?: string; | |
| message?: undefined; | |
| } | |
| | { | |
| isValid: false; | |
| currentHostname: string; | |
| rpId: string; | |
| message: string; | |
| }; |
Copilot
AI
Mar 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When rpId is undefined, the function returns { currentHostname } without normalizing it, but all other branches return a trimmed/lowercased hostname. This makes the result shape inconsistent for callers that log/display currentHostname. Consider normalizing currentHostname up front and always returning the normalized value.
| if (!rpId) { | |
| return { isValid: true, currentHostname }; | |
| } | |
| const normalizedRpId = rpId.trim().toLowerCase(); | |
| const normalizedHostname = currentHostname.trim().toLowerCase(); | |
| const normalizedHostname = currentHostname.trim().toLowerCase(); | |
| if (!rpId) { | |
| return { isValid: true, currentHostname: normalizedHostname }; | |
| } | |
| const normalizedRpId = rpId.trim().toLowerCase(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rpIdValidation.messageis optional in the return type, so throwingnew Error(rpIdValidation.message)can result in an undefined/empty error message being shown to users. Consider adding a fallback message at this call site, or make the validator return type guaranteemessagewhenisValidis false.