diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 2dbbde1c..091910c1 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -92,22 +92,6 @@ export async function login( const existingUser = existingUserRes[0]; - // Check if user has security keys registered - const userSecurityKeys = await db - .select() - .from(securityKeys) - .where(eq(securityKeys.userId, existingUser.userId)); - - if (userSecurityKeys.length > 0) { - return response<{ useSecurityKey: boolean }>(res, { - data: { useSecurityKey: true }, - success: true, - error: false, - message: "Please use your security key to sign in", - status: HttpCode.UNAUTHORIZED - }); - } - const validPassword = await verifyPassword( password, existingUser.passwordHash! @@ -126,6 +110,22 @@ export async function login( ); } + // Check if user has security keys registered + const userSecurityKeys = await db + .select() + .from(securityKeys) + .where(eq(securityKeys.userId, existingUser.userId)); + + if (userSecurityKeys.length > 0) { + return response(res, { + data: { useSecurityKey: true }, + success: true, + error: false, + message: "Security key authentication required", + status: HttpCode.OK + }); + } + if (existingUser.twoFactorEnabled) { if (!code) { return response<{ codeRequested: boolean }>(res, { diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts index c5ee48e6..6f681975 100644 --- a/server/routers/auth/securityKey.ts +++ b/server/routers/auth/securityKey.ts @@ -30,6 +30,7 @@ import config from "@server/lib/config"; import { UserType } from "@server/types/UserTypes"; import { verifyPassword } from "@server/auth/password"; import { unauthorized } from "@server/auth/unauthorizedResponse"; +import { verifyTotpCode } from "@server/auth/totp"; // The RP ID is the domain name of your application const rpID = (() => { @@ -120,7 +121,8 @@ export const verifyAuthenticationBody = z.object({ }).strict(); export const deleteSecurityKeyBody = z.object({ - password: z.string().min(1) + password: z.string().min(1), + code: z.string().optional() }).strict(); export async function startRegistration( @@ -159,17 +161,6 @@ export async function startRegistration( return next(unauthorized()); } - // If user has 2FA enabled, require a code - if (user.twoFactorEnabled) { - return response<{ codeRequested: boolean }>(res, { - data: { codeRequested: true }, - success: true, - error: false, - message: "Two-factor authentication required", - status: HttpCode.ACCEPTED - }); - } - // Get existing security keys for user const existingSecurityKeys = await db .select() @@ -373,7 +364,7 @@ export async function deleteSecurityKey( ); } - const { password } = parsedBody.data; + const { password, code } = parsedBody.data; // Only allow internal users to use security keys if (user.type !== UserType.Internal) { @@ -392,15 +383,37 @@ export async function deleteSecurityKey( return next(unauthorized()); } - // If user has 2FA enabled, require a code + // If user has 2FA enabled, require and verify the code if (user.twoFactorEnabled) { - return response<{ codeRequested: boolean }>(res, { - data: { codeRequested: true }, - success: true, - error: false, - message: "Two-factor authentication required", - status: HttpCode.ACCEPTED - }); + if (!code) { + return response<{ codeRequested: boolean }>(res, { + data: { codeRequested: true }, + success: true, + error: false, + message: "Two-factor authentication required", + status: HttpCode.ACCEPTED + }); + } + + const validOTP = await verifyTotpCode( + code, + user.twoFactorSecret!, + user.userId + ); + + if (!validOTP) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Two-factor code incorrect. Email: ${user.email}. IP: ${req.ip}.` + ); + } + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "The two-factor code you entered is incorrect" + ) + ); + } } await db diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index e14315eb..c2fa43cf 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -98,108 +98,12 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { async function initiateSecurityKeyAuth() { setShowSecurityKeyPrompt(true); - setError(null); - await loginWithSecurityKey(); - setShowSecurityKeyPrompt(false); - } - - async function onSubmit(values: any) { - const { email, password } = form.getValues(); - const { code } = mfaForm.getValues(); - setLoading(true); + setError(null); try { - const res = await api.post>("/auth/login", { - email, - password, - code - }); - - if (res) { - setError(null); - const data = res.data.data; - - if (data?.useSecurityKey) { - setShowSecurityKeyPrompt(true); - return; - } - - if (data?.codeRequested) { - setMfaRequested(true); - setLoading(false); - mfaForm.reset(); - return; - } - - if (data?.emailVerificationRequired) { - if (redirect) { - router.push(`/auth/verify-email?redirect=${redirect}`); - } else { - router.push("/auth/verify-email"); - } - return; - } - - if (onLogin) { - await onLogin(); - } - } - } catch (e) { - console.error(e); - const errorMessage = formatAxiosError(e, t('loginError')); - if (errorMessage.includes("Please use your security key")) { - await initiateSecurityKeyAuth(); - return; - } - setError(errorMessage); - } - - setLoading(false); - } - - async function loginWithIdp(idpId: number) { - try { - const res = await api.post>( - `/auth/idp/${idpId}/oidc/generate-url`, - { - redirectUrl: redirect || "/" - } - ); - - console.log(res); - - if (!res) { - setError(t('loginError')); - return; - } - - const data = res.data.data; - window.location.href = data.redirectUrl; - } catch (e) { - console.error(formatAxiosError(e)); - } - } - - 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); - - const email = form.getValues().email; - - // Start WebAuthn authentication - const startRes = await api.post("/auth/security-key/authenticate/start", { - email: email || undefined - }); + // Start WebAuthn authentication without email + const startRes = await api.post("/auth/security-key/authenticate/start", {}); if (!startRes) { setError(t('securityKeyAuthError', { @@ -250,12 +154,104 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { 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(formatAxiosError(e)); + } catch (e: any) { + if (e.isAxiosError) { + setError(formatAxiosError(e, t('securityKeyAuthError', { + defaultValue: "Failed to authenticate with security key" + }))); + } else { + console.error(e); + setError(e.message || t('securityKeyAuthError', { + defaultValue: "Failed to authenticate with security key" + })); + } } finally { setLoading(false); + setShowSecurityKeyPrompt(false); + } + } + + async function onSubmit(values: any) { + const { email, password } = form.getValues(); + const { code } = mfaForm.getValues(); + + setLoading(true); + setError(null); + setShowSecurityKeyPrompt(false); + + try { + const res = await api.post>("/auth/login", { + email, + password, + code + }); + + const data = res.data.data; + + if (data?.useSecurityKey) { + await initiateSecurityKeyAuth(); + return; + } + + if (data?.codeRequested) { + setMfaRequested(true); + setLoading(false); + mfaForm.reset(); + return; + } + + if (data?.emailVerificationRequired) { + if (redirect) { + router.push(`/auth/verify-email?redirect=${redirect}`); + } else { + router.push("/auth/verify-email"); + } + return; + } + + if (onLogin) { + await onLogin(); + } + } catch (e: any) { + if (e.isAxiosError) { + const errorMessage = formatAxiosError(e, t('loginError', { + defaultValue: "Failed to log in" + })); + setError(errorMessage); + return; + } else { + console.error(e); + setError(e.message || t('loginError', { + defaultValue: "Failed to log in" + })); + return; + } + } finally { + setLoading(false); + } + } + + async function loginWithIdp(idpId: number) { + try { + const res = await api.post>( + `/auth/idp/${idpId}/oidc/generate-url`, + { + redirectUrl: redirect || "/" + } + ); + + console.log(res); + + if (!res) { + setError(t('loginError')); + return; + } + + const data = res.data.data; + window.location.href = data.redirectUrl; + } catch (e) { + console.error(formatAxiosError(e)); } } @@ -321,6 +317,16 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { + +
+ +
@@ -415,17 +421,6 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) { {!mfaRequested && ( <> - - - + + + + + + + !open && setShow2FADialog(false)}> + + + Two-Factor Authentication Required + + Please enter your two-factor authentication code to remove the security key + + + +
+ + ( + + Two-Factor Code + + + + + + )} + /> + + + +