From a90a9d4fd68f4c90023f5096eacbdab802cd2c90 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Fri, 12 Jul 2024 15:37:05 -0700 Subject: [PATCH 1/2] Fixing user roles building issue. --- .../admin/users/[id]/+page.server.ts | 24 +++++++++---------- src/routes/(auth)/totp/+page.server.ts | 5 ++-- src/server/roles.ts | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts b/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts index 53e875f..d641278 100644 --- a/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts +++ b/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts @@ -3,7 +3,7 @@ import { redirect } from 'sveltekit-flash-message/server'; import type { PageServerLoad } from './$types'; import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages'; import db from '../../../../../../db'; -import { roles, user_roles, users } from '$db/schema'; +import { roles, userRoles, users } from '$db/schema'; export const load: PageServerLoad = async (event) => { const { params } = event; @@ -17,7 +17,7 @@ export const load: PageServerLoad = async (event) => { const foundUser = await db.query.users.findFirst({ where: eq(users.cuid, id), with: { - user_roles: { + userRoles: { with: { role: { columns: { @@ -30,7 +30,7 @@ export const load: PageServerLoad = async (event) => { }, }); - const containsAdminRole = foundUser?.user_roles?.some( + const containsAdminRole = foundUser?.userRoles?.some( (user_role) => user_role?.role?.name === 'admin', ); if (!containsAdminRole) { @@ -38,7 +38,7 @@ export const load: PageServerLoad = async (event) => { redirect(302, '/login', notSignedInMessage, event); } - const currentRoleIds = foundUser?.user_roles?.map((user_role) => user_role?.role.cuid) || []; + const currentRoleIds = foundUser?.userRoles?.map((user_role) => user_role?.role.cuid) || []; let availableRoles: { name: string; cuid: string }[] = []; if (currentRoleIds?.length > 0) { availableRoles = await db.query.roles.findMany({ @@ -65,8 +65,8 @@ export const actions = { redirect(302, '/login', notSignedInMessage, event); } - const userRoles = await db.query.user_roles.findMany({ - where: eq(user_roles.user_id, user.id), + const userRoles = await db.query.userRoles.findMany({ + where: eq(userRoles.user_id, user.id), with: { role: { columns: { @@ -92,7 +92,7 @@ export const actions = { }); console.log('dbRole', dbRole); if (dbRole) { - await db.insert(user_roles).values({ + await db.insert(userRoles).values({ user_id: user.id, role_id: dbRole.id, }); @@ -108,8 +108,8 @@ export const actions = { redirect(302, '/login', notSignedInMessage, event); } - const userRoles = await db.query.user_roles.findMany({ - where: eq(user_roles.user_id, user.id), + const userRoles = await db.query.userRoles.findMany({ + where: eq(userRoles.user_id, user.id), with: { role: { columns: { @@ -133,11 +133,11 @@ export const actions = { console.log('dbRole', dbRole); if (dbRole) { await db - .delete(user_roles) - .where(and(eq(user_roles.user_id, user.id), eq(user_roles.role_id, dbRole.id))); + .delete(userRoles) + .where(and(eq(userRoles.user_id, user.id), eq(userRoles.role_id, dbRole.id))); redirect({ type: 'success', message: `Successfully removed role ${dbRole.name}!` }, event); } else { - redirect({ type: 'error', message: `Failed to remove role ${dbRole.name} !` }, event); + redirect({ type: 'error', message: `Failed to remove role ${role?.toString()} !` }, event); } }, }; diff --git a/src/routes/(auth)/totp/+page.server.ts b/src/routes/(auth)/totp/+page.server.ts index b141372..90b812f 100644 --- a/src/routes/(auth)/totp/+page.server.ts +++ b/src/routes/(auth)/totp/+page.server.ts @@ -13,7 +13,7 @@ import { totpSchema } from '$lib/validations/auth'; import { users, twoFactor, recoveryCodes } from '$db/schema'; import type { PageServerLoad } from './$types'; import { notSignedInMessage } from '$lib/flashMessages'; -import { TWO_FACTOR_TIMEOUT } from '../../../env'; +import env from '../../../env'; export const load: PageServerLoad = async (event) => { const { user, session } = event.locals; @@ -32,7 +32,8 @@ export const load: PageServerLoad = async (event) => { }); // Check if two factor started less than TWO_FACTOR_TIMEOUT - if (Date.now() - twoFactorDetails?.initiatedTime > TWO_FACTOR_TIMEOUT) { + const + if ((Date.now() - twoFactorDetails?.initiatedTime) > env.TWO_FACTOR_TIMEOUT) {s const message = { type: 'error', message: 'Two factor authentication has expired' } as const; redirect(302, '/login', message, event); } diff --git a/src/server/roles.ts b/src/server/roles.ts index e6cf16d..62af8ea 100644 --- a/src/server/roles.ts +++ b/src/server/roles.ts @@ -1,6 +1,6 @@ import { eq } from 'drizzle-orm'; import db from '../db'; -import { roles, user_roles } from '$db/schema'; +import { roles, userRoles } from '$db/schema'; export async function add_user_to_role(user_id: string, role_name: string, primary = false) { // Find the role by its name @@ -13,7 +13,7 @@ export async function add_user_to_role(user_id: string, role_name: string, prima } // Create a UserRole entry linking the user and the role - return db.insert(user_roles).values({ + return db.insert(userRoles).values({ user_id, role_id: role.id, primary, From 2098a5cdfdad068d13fa6e0edc91b170fdf462d3 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Fri, 12 Jul 2024 17:44:45 -0700 Subject: [PATCH 2/2] Checking TOTP expiry time. --- src/lib/flashMessages.ts | 4 ++ src/routes/(auth)/login/+page.server.ts | 2 +- src/routes/(auth)/logout/+page.server.ts | 4 +- src/routes/(auth)/totp/+page.server.ts | 62 +++++++++++++++++++++--- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/lib/flashMessages.ts b/src/lib/flashMessages.ts index 141ca4b..b96f045 100644 --- a/src/lib/flashMessages.ts +++ b/src/lib/flashMessages.ts @@ -6,3 +6,7 @@ export const forbiddenMessage = { type: 'error', message: 'You are not allowed to access this', } as const; +export const signedOutMessage = { + type: 'success', + message: 'Successfully signed out', +} \ No newline at end of file diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 9173289..1bf98a6 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -94,7 +94,7 @@ export const actions: Actions = { if (twoFactorDetails?.secret && twoFactorDetails?.enabled) { await db.update(twoFactor).set({ - initiated_time: new Date(), + initiatedTime: new Date(), }); session = await lucia.createSession(user.id, { diff --git a/src/routes/(auth)/logout/+page.server.ts b/src/routes/(auth)/logout/+page.server.ts index 49655a5..78f9d98 100644 --- a/src/routes/(auth)/logout/+page.server.ts +++ b/src/routes/(auth)/logout/+page.server.ts @@ -1,7 +1,7 @@ import { fail } from '@sveltejs/kit'; import { redirect } from 'sveltekit-flash-message/server'; import { lucia } from '$lib/server/auth'; -import { notSignedInMessage } from '$lib/flashMessages'; +import { signedOutMessage } from '$lib/flashMessages'; import type { Actions } from "./$types"; export const actions: Actions = { @@ -17,6 +17,6 @@ export const actions: Actions = { path: '.', ...sessionCookie.attributes }); - return redirect(302, '/login', notSignedInMessage, event); + return redirect(302, '/login', signedOutMessage, event); } }; diff --git a/src/routes/(auth)/totp/+page.server.ts b/src/routes/(auth)/totp/+page.server.ts index 90b812f..576500a 100644 --- a/src/routes/(auth)/totp/+page.server.ts +++ b/src/routes/(auth)/totp/+page.server.ts @@ -1,4 +1,4 @@ -import { fail, error, type Actions } from '@sveltejs/kit'; +import { fail, error, type Actions, type Cookies, type RequestEvent } from '@sveltejs/kit'; import { and, eq } from 'drizzle-orm'; import { Argon2id } from 'oslo/password'; import { decodeHex } from 'oslo/encoding'; @@ -16,7 +16,8 @@ import { notSignedInMessage } from '$lib/flashMessages'; import env from '../../../env'; export const load: PageServerLoad = async (event) => { - const { user, session } = event.locals; + const { cookies, locals } = event; + const { user, session } = locals; if (!user || !session) { redirect(302, '/login', notSignedInMessage, event); @@ -31,9 +32,33 @@ export const load: PageServerLoad = async (event) => { where: eq(twoFactor.userId, dbUser!.id!), }); + if (!twoFactorDetails || !twoFactorDetails.enabled) { + const message = { type: 'error', message: 'Two factor authentication is not enabled' } as const; + redirect(302, '/login', message, event); + } + + let twoFactorInitiatedTime = twoFactorDetails.initiatedTime; + if (twoFactorInitiatedTime === null) { + console.log('twoFactorInitiatedTime is null'); + twoFactorInitiatedTime = new Date(); + console.log('twoFactorInitiatedTime', twoFactorInitiatedTime); + await db + .update(twoFactor) + .set({ initiatedTime: twoFactorInitiatedTime }) + .where(eq(twoFactor.userId, dbUser!.id!)); + } + // Check if two factor started less than TWO_FACTOR_TIMEOUT - const - if ((Date.now() - twoFactorDetails?.initiatedTime) > env.TWO_FACTOR_TIMEOUT) {s + const timeElapsed = Date.now() - twoFactorInitiatedTime.getTime(); + console.log('Time elapsed', timeElapsed); + if (timeElapsed > env.TWO_FACTOR_TIMEOUT) { + console.log('Time elapsed was more than TWO_FACTOR_TIMEOUT', timeElapsed, env.TWO_FACTOR_TIMEOUT); + 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); } @@ -71,7 +96,7 @@ export const actions: Actions = { throw error(429); } - const { locals } = event; + const { cookies, locals } = event; const session = locals.session; const user = locals.user; @@ -116,15 +141,18 @@ export const actions: Actions = { const twoFactorSecretPopulated = twoFactorDetails?.secret !== '' && twoFactorDetails?.secret !== null; - if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !totpToken) { + if (twoFactorDetails?.enabled && !twoFactorSecretPopulated && !totpToken) { return fail(400, { form, }); } else if (twoFactorSecretPopulated && totpToken) { + // Check if two factor started less than TWO_FACTOR_TIMEOUT + await checkTOTPExpiry(twoFactorDetails, session, cookies, event); + console.log('totpToken', totpToken); const validOTP = await new TOTPController().verify( totpToken, - decodeHex(twoFactorDetails.secret ?? ''), + decodeHex(twoFactorDetails?.secret ?? ''), ); console.log('validOTP', validOTP); @@ -167,6 +195,26 @@ export const actions: Actions = { }, }; +async function checkTOTPExpiry(twoFactorDetails: { id: string; cuid: string | null; secret: string; enabled: boolean; initiatedTime: Date | null; createdAt: Date; updatedAt: Date; userId: string; } | undefined, session, cookies: Cookies, event: RequestEvent>, string | null>) { + const twoFactorInitiatedTime = twoFactorDetails?.initiatedTime; + if (twoFactorInitiatedTime === null || twoFactorInitiatedTime === undefined) { + redirect(302, '/login'); + } + const timeElapsed = Date.now() - twoFactorInitiatedTime.getTime(); + console.log('Time elapsed', timeElapsed); + if (timeElapsed > env.TWO_FACTOR_TIMEOUT) { + console.log('Time elapsed was more than TWO_FACTOR_TIMEOUT', timeElapsed, env.TWO_FACTOR_TIMEOUT); + 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); + } +} + async function checkRecoveryCode(recoveryCode: string, userId: string) { const userRecoveryCodes = await db.query.recoveryCodes.findMany({ where: and(eq(recoveryCodes.used, false), eq(recoveryCodes.userId, userId)),