diff --git a/messages/en-US.json b/messages/en-US.json index fda1a590..4a68fb17 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1152,5 +1152,11 @@ "securityKeyAuthError": "Failed to authenticate with security key", "securityKeyRecommendation": "Consider registering another security key on a different device to ensure you don't get locked out of your account.", "registering": "Registering...", - "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready." + "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.", + "securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.", + "securityKeyPermissionDenied": "Please allow access to your security key to continue signing in.", + "securityKeyRemovedTooQuickly": "Please keep your security key connected until the sign-in process completes.", + "securityKeyNotSupported": "Your security key may not be compatible. Please try a different security key.", + "securityKeyUnknownError": "There was a problem using your security key. Please try again.", + "twoFactorRequired": "Two-factor authentication is required to register a security key." } diff --git a/server/routers/auth/passkey.ts b/server/routers/auth/passkey.ts index 835b9c8b..268f2144 100644 --- a/server/routers/auth/passkey.ts +++ b/server/routers/auth/passkey.ts @@ -32,7 +32,15 @@ import { verifyPassword } from "@server/auth/password"; import { unauthorized } from "@server/auth/unauthorizedResponse"; // The RP ID is the domain name of your application -const rpID = new URL(config.getRawConfig().app.dashboard_url).hostname; +const rpID = (() => { + const url = new URL(config.getRawConfig().app.dashboard_url); + // For localhost, we must use 'localhost' without port + if (url.hostname === 'localhost') { + return 'localhost'; + } + return url.hostname; +})(); + const rpName = "Pangolin"; const origin = config.getRawConfig().app.dashboard_url; diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index f806f77d..ac435043 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -183,6 +183,14 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { async function loginWithSecurityKey() { try { + // Check browser compatibility first + if (!window.PublicKeyCredential) { + setError(t('securityKeyBrowserNotSupported', { + defaultValue: "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari." + })); + return; + } + setLoading(true); setError(null); @@ -203,29 +211,49 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { const { tempSessionId, ...options } = startRes.data.data; // Perform WebAuthn authentication - const credential = await startAuthentication(options); + try { + const credential = await startAuthentication(options); + + // Verify authentication + const verifyRes = await api.post( + "/auth/passkey/authenticate/verify", + { credential }, + { + headers: { + 'X-Temp-Session-Id': tempSessionId + } + } + ); - // Verify authentication - const verifyRes = await api.post( - "/auth/passkey/authenticate/verify", - { credential }, - { - headers: { - 'X-Temp-Session-Id': tempSessionId + if (verifyRes) { + if (onLogin) { + await onLogin(); } } - ); - - if (verifyRes) { - if (onLogin) { - await onLogin(); + } catch (error: any) { + if (error.name === 'NotAllowedError') { + if (error.message.includes('denied permission')) { + setError(t('securityKeyPermissionDenied', { + defaultValue: "Please allow access to your security key to continue signing in." + })); + } else { + setError(t('securityKeyRemovedTooQuickly', { + defaultValue: "Please keep your security key connected until the sign-in process completes." + })); + } + } else if (error.name === 'NotSupportedError') { + setError(t('securityKeyNotSupported', { + defaultValue: "Your security key may not be compatible. Please try a different security key." + })); + } else { + setError(t('securityKeyUnknownError', { + defaultValue: "There was a problem using your security key. Please try again." + })); } + throw error; // Re-throw to be caught by outer catch } } catch (e) { - console.error(e); - setError(formatAxiosError(e, t('securityKeyAuthError', { - defaultValue: "Security key authentication failed" - }))); + console.error(formatAxiosError(e)); } finally { setLoading(false); } diff --git a/src/components/SecurityKeyForm.tsx b/src/components/SecurityKeyForm.tsx index 73b82c3a..cc90f64b 100644 --- a/src/components/SecurityKeyForm.tsx +++ b/src/components/SecurityKeyForm.tsx @@ -120,6 +120,17 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps) const handleRegisterSecurityKey = async (values: RegisterFormValues) => { try { + // Check browser compatibility first + if (!window.PublicKeyCredential) { + toast({ + variant: "destructive", + description: t('securityKeyBrowserNotSupported', { + defaultValue: "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari." + }) + }); + return; + } + setIsRegistering(true); const startRes = await api.post("/auth/passkey/register/start", { name: values.name, @@ -129,29 +140,72 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps) if (startRes.status === 202) { toast({ variant: "destructive", - description: "Two-factor authentication is required to register a security key.", + description: t('twoFactorRequired', { + defaultValue: "Two-factor authentication is required to register a security key." + }) }); return; } const options = startRes.data.data; - const credential = await startRegistration(options); + + try { + const credential = await startRegistration(options); - await api.post("/auth/passkey/register/verify", { - credential, - }); + await api.post("/auth/passkey/register/verify", { + credential, + }); - toast({ - description: t('securityKeyRegisterSuccess') - }); + toast({ + description: t('securityKeyRegisterSuccess', { + defaultValue: "Security key registered successfully" + }) + }); - registerForm.reset(); - setShowRegisterDialog(false); - await loadSecurityKeys(); + registerForm.reset(); + setShowRegisterDialog(false); + await loadSecurityKeys(); + } catch (error: any) { + if (error.name === 'NotAllowedError') { + if (error.message.includes('denied permission')) { + toast({ + variant: "destructive", + description: t('securityKeyPermissionDenied', { + defaultValue: "Please allow access to your security key to continue registration." + }) + }); + } else { + toast({ + variant: "destructive", + description: t('securityKeyRemovedTooQuickly', { + defaultValue: "Please keep your security key connected until the registration process completes." + }) + }); + } + } else if (error.name === 'NotSupportedError') { + toast({ + variant: "destructive", + description: t('securityKeyNotSupported', { + defaultValue: "Your security key may not be compatible. Please try a different security key." + }) + }); + } else { + toast({ + variant: "destructive", + description: t('securityKeyUnknownError', { + defaultValue: "There was a problem registering your security key. Please try again." + }) + }); + } + throw error; // Re-throw to be caught by outer catch + } } catch (error) { + console.error('Security key registration error:', error); toast({ variant: "destructive", - description: formatAxiosError(error, t('securityKeyRegisterError')), + description: formatAxiosError(error, t('securityKeyRegisterError', { + defaultValue: "Failed to register security key" + })) }); } finally { setIsRegistering(false);