diff --git a/src/routes/(auth)/password/reset/+page.svelte b/src/routes/(auth)/password/reset/+page.svelte index a79e380..9a178e4 100644 --- a/src/routes/(auth)/password/reset/+page.svelte +++ b/src/routes/(auth)/password/reset/+page.svelte @@ -57,7 +57,7 @@
- Email + Email - Enter the token that was sent to your email + Enter the token that was sent to your email diff --git a/src/routes/(auth)/totp/+page.server.ts b/src/routes/(auth)/totp/+page.server.ts index 48d79c8..5892599 100644 --- a/src/routes/(auth)/totp/+page.server.ts +++ b/src/routes/(auth)/totp/+page.server.ts @@ -11,7 +11,7 @@ import db from '../../../db'; import { lucia } from '$lib/server/auth'; import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth'; import { users, twoFactor, recoveryCodes } from '$db/schema'; -import type { PageServerLoad } from './$types'; +import type {PageServerLoad, RequestEvent} from './$types'; import { notSignedInMessage } from '$lib/flashMessages'; import env from '../../../env'; @@ -92,112 +92,77 @@ const limiter = new RateLimiter({ }); export const actions: Actions = { - default: async (event) => { - if (await limiter.isLimited(event)) { - throw error(429); - } - + validateTotp: async (event) => { const { cookies, locals } = event; const session = locals.session; const user = locals.user; + if (await limiter.isLimited(event)) { + throw error(429); + } + if (!user || !session) { throw fail(401); } - const dbUser = await db.query.users.findFirst({ - where: eq(users.username, user.username), - }); + const { dbUser, twoFactorDetails } = await validateUserData(event, locals); - if (!dbUser) { - throw fail(401); - } + const totpForm = await superValidate(event, zod(totpSchema)); - const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated; - const twoFactorDetails = await db.query.twoFactor.findFirst({ - where: eq(twoFactor.userId, dbUser!.id!), - }); - - if (!twoFactorDetails) { - const message = { type: 'error', message: 'Unable to process request' } as const; - throw redirect(302, '/login', message, event); - } - - if (isTwoFactorAuthenticated && twoFactorDetails.enabled && twoFactorDetails.secret !== '') { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); - } - - const form = await superValidate(event, zod(totpSchema)); - - if (!form.valid) { - form.data.totpToken = ''; - return fail(400, { - form, - }); + if (!totpForm.valid) { + totpForm.data.totpToken = ''; + return fail(400, { totpForm }); } let sessionCookie; - try { - const totpToken = form?.data?.totpToken; + const totpToken = totpForm?.data?.totpToken; - const twoFactorSecretPopulated = - twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null; - if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !totpToken) { - return fail(400, { - form, + const twoFactorSecretPopulated = + twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null; + if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !totpToken) { + return fail(400, { totpForm }); + } else if (twoFactorSecretPopulated && totpToken) { + // Check if two factor started less than TWO_FACTOR_TIMEOUT + const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()); + if (totpElapsed) { + await lucia.invalidateSession(session!.id!); + const sessionCookie = lucia.createBlankSessionCookie(); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes, }); - } else if (twoFactorSecretPopulated && totpToken) { - // Check if two factor started less than TWO_FACTOR_TIMEOUT - const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()); - if (totpElapsed) { - await lucia.invalidateSession(session!.id!); - const sessionCookie = lucia.createBlankSessionCookie(); - cookies.set(sessionCookie.name, sessionCookie.value, { - path: '.', - ...sessionCookie.attributes, - }); - const message = { - type: 'error', - message: 'Two factor authentication has expired', - } as const; - redirect(302, '/login', message, event); - } - - console.log('totpToken', totpToken); - const validOTP = await new TOTPController().verify( - totpToken, - decodeHex(twoFactorDetails.secret ?? ''), - ); - console.log('validOTP', validOTP); - - if (!validOTP) { - console.log('invalid TOTP code check for recovery codes'); - const usedRecoveryCode = await checkRecoveryCode(totpToken, dbUser.id); - if (!usedRecoveryCode) { - console.log('invalid TOTP code'); - form.data.totpToken = ''; - return setError(form, 'totpToken', 'Invalid code.'); - } - } + const message = { + type: 'error', + message: 'Two factor authentication has expired', + } as const; + redirect(302, '/login', message, event); + } + + console.log('totpToken', totpToken); + const validOTP = await new TOTPController().verify( + totpToken, + decodeHex(twoFactorDetails.secret ?? ''), + ); + console.log('validOTP', validOTP); + + if (!validOTP) { + console.log('invalid TOTP code'); + totpForm.data.totpToken = ''; + return setError(totpForm, 'totpToken', 'Invalid code.'); } - console.log('ip', locals.ip); - console.log('country', locals.country); - await lucia.invalidateSession(session.id); - const newSession = await lucia.createSession(dbUser.id, { - ip_country: locals.country, - ip_address: locals.ip, - twoFactorAuthEnabled: true, - isTwoFactorAuthenticated: true, - }); - console.log('logging in session', newSession); - sessionCookie = lucia.createSessionCookie(newSession.id); - console.log('logging in session cookie', sessionCookie); - } catch (e) { - // TODO: need to return error message to the client - console.error(e); - return setError(form, 'totpToken', 'Error verifying TOTP code.'); } + console.log('ip', locals.ip); + console.log('country', locals.country); + await lucia.invalidateSession(session.id); + const newSession = await lucia.createSession(dbUser.id, { + ip_country: locals.country, + ip_address: locals.ip, + twoFactorAuthEnabled: true, + isTwoFactorAuthenticated: true, + }); + console.log('logging in session', newSession); + sessionCookie = lucia.createSessionCookie(newSession.id); + console.log('logging in session cookie', sessionCookie); console.log('setting session cookie', sessionCookie); event.cookies.set(sessionCookie.name, sessionCookie.value, { @@ -205,12 +170,124 @@ export const actions: Actions = { ...sessionCookie.attributes, }); - form.data.totpToken = ''; + totpForm.data.totpToken = ''; const message = { type: 'success', message: 'Signed In!' } as const; redirect(302, '/', message, event); }, + validateRecoveryCode: async (event) => { + const { cookies, locals } = event; + const session = locals.session; + const user = locals.user; + + if (await limiter.isLimited(event)) { + throw error(429); + } + + if (!user || !session) { + throw fail(401); + } + + const { dbUser, twoFactorDetails } = await validateUserData(event, locals); + + const recoveryCodeForm = await superValidate(event, zod(recoveryCodeSchema)); + if (!recoveryCodeForm.valid) { + return fail(400, { + form: recoveryCodeForm, + }); + } + + let sessionCookie; + const recoveryCode = recoveryCodeForm?.data?.recoveryCode; + + const twoFactorSecretPopulated = + twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null; + if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !recoveryCode) { + return fail(400, { recoveryCodeForm }); + } else if (twoFactorSecretPopulated && recoveryCode) { + // Check if two factor started less than TWO_FACTOR_TIMEOUT + const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()); + if (totpElapsed) { + await lucia.invalidateSession(session!.id!); + const sessionCookie = lucia.createBlankSessionCookie(); + cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes, + }); + const message = { + type: 'error', + message: 'Two factor authentication has expired', + } as const; + redirect(302, '/login', message, event); + } + + console.log('recoveryCode', recoveryCode); + + console.log('Check for recovery codes'); + const usedRecoveryCode = await checkRecoveryCode(recoveryCode, dbUser.id); + if (!usedRecoveryCode) { + console.log('invalid recovery code'); + recoveryCodeForm.data.recoveryCode = ''; + return setError(recoveryCodeForm, 'recoveryCode', 'Invalid code.'); + } + } + console.log('ip', locals.ip); + console.log('country', locals.country); + await lucia.invalidateSession(session.id); + const newSession = await lucia.createSession(dbUser.id, { + ip_country: locals.country, + ip_address: locals.ip, + twoFactorAuthEnabled: true, + isTwoFactorAuthenticated: true, + }); + console.log('logging in session', newSession); + sessionCookie = lucia.createSessionCookie(newSession.id); + console.log('logging in session cookie', sessionCookie); + + console.log('setting session cookie', sessionCookie); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes, + }); + + recoveryCodeForm.data.recoveryCode = ''; + const message = { type: 'success', message: 'Signed In!' } as const; + redirect(302, '/', message, event); + } }; +async function validateUserData(event: RequestEvent, locals: App.Locals) { + const { user, session } = locals; + + if (!user || !session) { + throw fail(401); + } + + const dbUser = await db.query.users.findFirst({ + where: eq(users.username, user.username), + }); + + if (!dbUser) { + throw fail(401); + } + + const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated; + const twoFactorDetails = await db.query.twoFactor.findFirst({ + where: eq(twoFactor.userId, dbUser!.id!), + }); + + if (!twoFactorDetails) { + const message = {type: 'error', message: 'Unable to process request'} as const; + throw redirect(302, '/login', message, event); + } + + if (isTwoFactorAuthenticated && twoFactorDetails.enabled && twoFactorDetails.secret !== '') { + const message = {type: 'success', message: 'You are already signed in'} as const; + throw redirect('/', message, event); + } + return { dbUser, twoFactorDetails }; +} + + function totpTimeElapsed(initiatedTime: Date) { if (initiatedTime === null || initiatedTime === undefined) { return true; diff --git a/src/routes/(auth)/totp/+page.svelte b/src/routes/(auth)/totp/+page.svelte index 2d0167d..f87f8df 100644 --- a/src/routes/(auth)/totp/+page.svelte +++ b/src/routes/(auth)/totp/+page.svelte @@ -15,38 +15,13 @@ const { data } = $props(); const superTotpForm = superForm(data.totpForm, { - flashMessage: { - module: flashModule, - onError: ({ result, flashMessage }) => { - // Error handling for the flash message: - // - result is the ActionResult - // - message is the flash store (not the status message store) - const errorMessage = result.error.message - flashMessage.set({ type: 'error', message: errorMessage }); - } - }, - syncFlashMessage: false, - taintedMessage: null, + resetForm: false, validators: zodClient(totpSchema), - validationMethod: 'oninput', - delayMs: 0, }); const superRecoveryCodeForm = superForm(data.recoveryCodeForm, { validators: zodClient(recoveryCodeSchema), resetForm: false, - flashMessage: { - module: flashModule, - onError: ({ result, flashMessage }) => { - // Error handling for the flash message: - // - result is the ActionResult - // - message is the flash store (not the status message store) - const errorMessage = result.error.message - flashMessage.set({ type: 'error', message: errorMessage }); - } - }, - syncFlashMessage: false, - taintedMessage: null, validationMethod: 'oninput', delayMs: 0, }); @@ -78,11 +53,11 @@ {#snippet totpForm()} - - + + - TOTP Code - + TOTP Code + @@ -91,10 +66,10 @@ {/snippet} {#snippet recoveryCodeForm()} - - + + - Recovery Code + Recovery Code