From 5c2971e38d984c688c5d3a57864e043ad875d5f6 Mon Sep 17 00:00:00 2001 From: Cory Hawkvelt Date: Tue, 24 Feb 2026 01:20:41 +1030 Subject: [PATCH] fix(auth): validate WebAuthn rp.id against current host Add ensureValidRpId helper to validate and correct rp.id for WebAuthn operations, preventing authentication failures when the configured rp.id doesn't match the current hostname. Also add OAuthProvider type and fix type casting in LoginPage. --- src/lib/oauth.ts | 5 +++++ src/lib/webauthn.ts | 19 ++++++++++++++++++- src/pages/auth/LoginPage.tsx | 4 ++-- tsconfig.app.tsbuildinfo | 1 + tsconfig.node.tsbuildinfo | 1 + 5 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 tsconfig.app.tsbuildinfo create mode 100644 tsconfig.node.tsbuildinfo diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 117d863..5976e4b 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -8,6 +8,11 @@ */ export type OAuthFlow = 'login' | 'register' | 'link'; +/** + * Supported OAuth providers. + */ +export type OAuthProvider = 'google' | 'github' | 'microsoft'; + /** * Parameters for storing OAuth state. */ diff --git a/src/lib/webauthn.ts b/src/lib/webauthn.ts index 463bee6..9ab979b 100644 --- a/src/lib/webauthn.ts +++ b/src/lib/webauthn.ts @@ -45,6 +45,19 @@ export async function isPlatformAuthenticatorAvailable(): Promise { } } +// Helper to ensure rp.id is valid for the current origin +function ensureValidRpId(rpId: string): string { + const currentHost = window.location.hostname; + // valid if rpId matches host or is a parent domain (e.g. host ends with .rpId) + const isValid = currentHost === rpId || currentHost.endsWith('.' + rpId); + + if (!isValid) { + console.warn(`[WebAuthn] Invalid rp.id "${rpId}" for current host "${currentHost}". Overriding with "${currentHost}".`); + return currentHost; + } + return rpId; +} + // Types for WebAuthn API responses export interface WebAuthnRegistrationOptions { rp: { @@ -107,6 +120,10 @@ export async function createRegistrationCredential( ): Promise { const publicKeyOptions: PublicKeyCredentialCreationOptions = { ...options, + rp: { + ...options.rp, + id: ensureValidRpId(options.rp.id), + }, challenge: base64ToBuffer(options.challenge), user: { id: base64ToBuffer(options.user.id), @@ -157,7 +174,7 @@ export async function createLoginAssertion( const publicKeyOptions: PublicKeyCredentialRequestOptions = { challenge: base64ToBuffer(options.challenge), timeout: options.timeout, - rpId: options.rpId, + rpId: ensureValidRpId(options.rpId), allowCredentials: options.allowCredentials.map((cred) => ({ ...cred, id: base64ToBuffer(cred.id), diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index d27709f..84c3a24 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -218,7 +218,7 @@ export default function LoginPage() { try { // Step 1: Get login options from server - const options = await api.webauthn.beginLogin(emailToUse) as WebAuthnLoginOptions; + const options = await api.webauthn.beginLogin(emailToUse) as unknown as WebAuthnLoginOptions; // Step 2: Create assertion using browser WebAuthn API const assertion = await createLoginAssertion(options); @@ -286,7 +286,7 @@ export default function LoginPage() { try { // Step 1: Get login options from server - const options = await api.webauthn.beginLogin(email) as WebAuthnLoginOptions; + const options = await api.webauthn.beginLogin(email) as unknown as WebAuthnLoginOptions; // Step 2: Create assertion using browser WebAuthn API const assertion = await createLoginAssertion(options); diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..1de9171 --- /dev/null +++ b/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/App.tsx","./src/config.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/NavLink.tsx","./src/components/auth/BannerAlert.tsx","./src/components/auth/ComplianceBanner.tsx","./src/components/auth/PasswordStrengthMeter.tsx","./src/components/branding/GatehouseLogo.tsx","./src/components/dev/ApiDevTools.tsx","./src/components/layouts/AuthenticatedLayout.tsx","./src/components/layouts/MfaEnforcementLayout.tsx","./src/components/layouts/ProtectedLayout.tsx","./src/components/layouts/PublicLayout.tsx","./src/components/navigation/AppSidebar.tsx","./src/components/navigation/TopBar.tsx","./src/components/security/AddPasskeyWizard.tsx","./src/components/security/TotpEnrollmentWizard.tsx","./src/components/security/TotpRemoveDialog.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/ui/toaster.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-toast.ts","./src/contexts/AuthContext.tsx","./src/hooks/use-mobile.tsx","./src/hooks/use-toast.ts","./src/hooks/useOrganizations.ts","./src/lib/api.ts","./src/lib/encoding.ts","./src/lib/oauth.ts","./src/lib/utils.ts","./src/lib/webauthn.ts","./src/pages/Index.tsx","./src/pages/NotFound.tsx","./src/pages/auth/ForgotPasswordPage.tsx","./src/pages/auth/InviteAcceptPage.tsx","./src/pages/auth/LoginPage.tsx","./src/pages/auth/OAuthCallbackPage.tsx","./src/pages/auth/OIDCConsentPage.tsx","./src/pages/auth/OIDCErrorPage.tsx","./src/pages/auth/RegisterPage.tsx","./src/pages/auth/ResetPasswordPage.tsx","./src/pages/auth/VerifyEmailPage.tsx","./src/pages/org/CompliancePage.tsx","./src/pages/org/MembersPage.tsx","./src/pages/org/OIDCClientsPage.tsx","./src/pages/org/OrgAuditPage.tsx","./src/pages/org/OrgOverviewPage.tsx","./src/pages/org/PoliciesPage.tsx","./src/pages/user/ActivityPage.tsx","./src/pages/user/LinkedAccountsPage.tsx","./src/pages/user/ProfilePage.tsx","./src/pages/user/SecurityPage.tsx"],"errors":true,"version":"5.8.3"} \ No newline at end of file diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..3015526 --- /dev/null +++ b/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.8.3"} \ No newline at end of file