Update security key error handling and user feedback. Improve user guidance for security key interactions and Implement proper error handling for permission denials and timing issues.

This commit is contained in:
Adrian Astles 2025-07-05 18:56:32 +08:00
parent 3994b25a71
commit 6ccc05b183
4 changed files with 127 additions and 31 deletions

View file

@ -1152,5 +1152,11 @@
"securityKeyAuthError": "Failed to authenticate with security key", "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.", "securityKeyRecommendation": "Consider registering another security key on a different device to ensure you don't get locked out of your account.",
"registering": "Registering...", "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."
} }

View file

@ -32,7 +32,15 @@ import { verifyPassword } from "@server/auth/password";
import { unauthorized } from "@server/auth/unauthorizedResponse"; import { unauthorized } from "@server/auth/unauthorizedResponse";
// The RP ID is the domain name of your application // 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 rpName = "Pangolin";
const origin = config.getRawConfig().app.dashboard_url; const origin = config.getRawConfig().app.dashboard_url;

View file

@ -183,6 +183,14 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
async function loginWithSecurityKey() { async function loginWithSecurityKey() {
try { 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); setLoading(true);
setError(null); setError(null);
@ -203,29 +211,49 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
const { tempSessionId, ...options } = startRes.data.data; const { tempSessionId, ...options } = startRes.data.data;
// Perform WebAuthn authentication // 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 if (verifyRes) {
const verifyRes = await api.post( if (onLogin) {
"/auth/passkey/authenticate/verify", await onLogin();
{ credential },
{
headers: {
'X-Temp-Session-Id': tempSessionId
} }
} }
); } catch (error: any) {
if (error.name === 'NotAllowedError') {
if (verifyRes) { if (error.message.includes('denied permission')) {
if (onLogin) { setError(t('securityKeyPermissionDenied', {
await onLogin(); 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) { } catch (e) {
console.error(e); console.error(formatAxiosError(e));
setError(formatAxiosError(e, t('securityKeyAuthError', {
defaultValue: "Security key authentication failed"
})));
} finally { } finally {
setLoading(false); setLoading(false);
} }

View file

@ -120,6 +120,17 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
const handleRegisterSecurityKey = async (values: RegisterFormValues) => { const handleRegisterSecurityKey = async (values: RegisterFormValues) => {
try { 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); setIsRegistering(true);
const startRes = await api.post("/auth/passkey/register/start", { const startRes = await api.post("/auth/passkey/register/start", {
name: values.name, name: values.name,
@ -129,29 +140,72 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
if (startRes.status === 202) { if (startRes.status === 202) {
toast({ toast({
variant: "destructive", 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; return;
} }
const options = startRes.data.data; const options = startRes.data.data;
const credential = await startRegistration(options);
try {
const credential = await startRegistration(options);
await api.post("/auth/passkey/register/verify", { await api.post("/auth/passkey/register/verify", {
credential, credential,
}); });
toast({ toast({
description: t('securityKeyRegisterSuccess') description: t('securityKeyRegisterSuccess', {
}); defaultValue: "Security key registered successfully"
})
});
registerForm.reset(); registerForm.reset();
setShowRegisterDialog(false); setShowRegisterDialog(false);
await loadSecurityKeys(); 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) { } catch (error) {
console.error('Security key registration error:', error);
toast({ toast({
variant: "destructive", variant: "destructive",
description: formatAxiosError(error, t('securityKeyRegisterError')), description: formatAxiosError(error, t('securityKeyRegisterError', {
defaultValue: "Failed to register security key"
}))
}); });
} finally { } finally {
setIsRegistering(false); setIsRegistering(false);