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