Merge pull request #18 from BradNut/development

Add two factor auth timeout
This commit is contained in:
Bradley Shellnut 2024-07-13 07:10:58 +00:00 committed by GitHub
commit ce0e44bf85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 77 additions and 24 deletions

View file

@ -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',
}

View file

@ -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);
}
},
};

View file

@ -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, {

View file

@ -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);
}
};

View file

@ -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';
@ -13,10 +13,11 @@ 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;
const { cookies, locals } = event;
const { user, session } = locals;
if (!user || !session) {
redirect(302, '/login', notSignedInMessage, event);
@ -31,8 +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
if (Date.now() - twoFactorDetails?.initiatedTime > TWO_FACTOR_TIMEOUT) {
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);
}
@ -70,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;
@ -115,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);
@ -166,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<Partial<Record<string, string>>, 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)),

View file

@ -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,