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.
This commit is contained in:
2026-02-24 01:20:41 +10:30
parent e854bf801e
commit 5c2971e38d
5 changed files with 27 additions and 3 deletions
+5
View File
@@ -8,6 +8,11 @@
*/ */
export type OAuthFlow = 'login' | 'register' | 'link'; export type OAuthFlow = 'login' | 'register' | 'link';
/**
* Supported OAuth providers.
*/
export type OAuthProvider = 'google' | 'github' | 'microsoft';
/** /**
* Parameters for storing OAuth state. * Parameters for storing OAuth state.
*/ */
+18 -1
View File
@@ -45,6 +45,19 @@ export async function isPlatformAuthenticatorAvailable(): Promise<boolean> {
} }
} }
// 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 // Types for WebAuthn API responses
export interface WebAuthnRegistrationOptions { export interface WebAuthnRegistrationOptions {
rp: { rp: {
@@ -107,6 +120,10 @@ export async function createRegistrationCredential(
): Promise<PublicKeyCredential> { ): Promise<PublicKeyCredential> {
const publicKeyOptions: PublicKeyCredentialCreationOptions = { const publicKeyOptions: PublicKeyCredentialCreationOptions = {
...options, ...options,
rp: {
...options.rp,
id: ensureValidRpId(options.rp.id),
},
challenge: base64ToBuffer(options.challenge), challenge: base64ToBuffer(options.challenge),
user: { user: {
id: base64ToBuffer(options.user.id), id: base64ToBuffer(options.user.id),
@@ -157,7 +174,7 @@ export async function createLoginAssertion(
const publicKeyOptions: PublicKeyCredentialRequestOptions = { const publicKeyOptions: PublicKeyCredentialRequestOptions = {
challenge: base64ToBuffer(options.challenge), challenge: base64ToBuffer(options.challenge),
timeout: options.timeout, timeout: options.timeout,
rpId: options.rpId, rpId: ensureValidRpId(options.rpId),
allowCredentials: options.allowCredentials.map((cred) => ({ allowCredentials: options.allowCredentials.map((cred) => ({
...cred, ...cred,
id: base64ToBuffer(cred.id), id: base64ToBuffer(cred.id),
+2 -2
View File
@@ -218,7 +218,7 @@ export default function LoginPage() {
try { try {
// Step 1: Get login options from server // 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 // Step 2: Create assertion using browser WebAuthn API
const assertion = await createLoginAssertion(options); const assertion = await createLoginAssertion(options);
@@ -286,7 +286,7 @@ export default function LoginPage() {
try { try {
// Step 1: Get login options from server // 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 // Step 2: Create assertion using browser WebAuthn API
const assertion = await createLoginAssertion(options); const assertion = await createLoginAssertion(options);
+1
View File
@@ -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"}
+1
View File
@@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.8.3"}