Fixing login cookie max-age.

This commit is contained in:
Bradley Shellnut 2024-08-14 18:07:50 -07:00
parent 8894fbf98b
commit eeca4e4103
7 changed files with 66 additions and 41 deletions

View file

@ -1,6 +1,6 @@
import { z } from "zod";
export const signInEmailDto = z.object({
export const signinUsernameDto = z.object({
username: z
.string()
.trim()
@ -9,4 +9,4 @@ export const signInEmailDto = z.object({
password: z.string({ required_error: 'Password is required' }).trim(),
});
export type SignInEmailDto = z.infer<typeof signInEmailDto>;
export type SigninUsernameDto = z.infer<typeof signinUsernameDto>;

View file

@ -1,27 +1,43 @@
import 'reflect-metadata';
import { Hono } from 'hono';
import { setCookie } from 'hono/cookie';
import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from 'tsyringe';
import { TimeSpan } from 'oslo';
import type { HonoTypes } from '../types';
import { limiter } from '../middleware/rate-limiter.middleware';
import type { Controller } from '../interfaces/controller.interface';
import { signInEmailDto } from '$lib/dtos/signin-email.dto';
import { LoginRequestsService } from '../services/loginrequest.service';
import { signinUsernameDto } from "$lib/dtos/signin-username.dto";
import {LuciaProvider} from "$lib/server/api/providers";
@injectable()
export class LoginController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
@inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService
@inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService,
@inject(LuciaProvider) private lucia: LuciaProvider
) { }
routes() {
return this.controller
.post('/', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
.post('/', zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { username, password } = c.req.valid('json');
await this.loginRequestsService.verify({ username, password }, c.req);
return c.json({ message: 'Verification email sent' });
const session = await this.loginRequestsService.verify({ username, password }, c.req);
const sessionCookie = this.lucia.createSessionCookie(session.id);
console.log("set cookie", sessionCookie);
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
? sessionCookie.attributes.maxAge : new TimeSpan(2, 'w').seconds(),
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires
});
return c.json({ message: 'ok' });
})
}
}

View file

@ -21,7 +21,7 @@ export const lucia = new Lucia(adapter, {
...attributes,
};
},
sessionExpiresIn: new TimeSpan(30, 'd'), // 30 days
sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks
sessionCookie: {
name: 'session',
expires: false, // session cookies have very long lifespan (2 years)

View file

@ -5,9 +5,9 @@ import { MailerService } from './mailer.service';
import { TokensService } from './tokens.service';
import { LuciaProvider } from '../providers/lucia.provider';
import { UsersRepository } from '../repositories/users.repository';
import type { SignInEmailDto } from '../../../dtos/signin-email.dto';
import { CredentialsRepository } from '../repositories/credentials.repository';
import type { HonoRequest } from 'hono';
import type {SigninUsernameDto} from "$lib/dtos/signin-username.dto";
@injectable()
export class LoginRequestsService {
@ -32,7 +32,7 @@ export class LoginRequestsService {
// });
// }
async verify(data: SignInEmailDto, req: HonoRequest) {
async verify(data: SigninUsernameDto, req: HonoRequest) {
const requestIpAddress = req.header('x-real-ip');
const requestIpCountry = req.header('x-vercel-ip-country');
const existingUser = await this.usersRepository.findOneByUsername(data.username);
@ -47,7 +47,7 @@ export class LoginRequestsService {
throw BadRequest('Invalid credentials');
}
if (!await this.tokensService.verifyHashedToken(credential.hashedPassword, data.password)) {
if (!await this.tokensService.verifyHashedToken(credential.secret_data, data.password)) {
throw BadRequest('Invalid credentials');
}
@ -58,15 +58,15 @@ export class LoginRequestsService {
ip_address: requestIpAddress || 'unknown',
twoFactorAuthEnabled:
!!totpCredentials &&
totpCredentials?.secret !== null &&
totpCredentials?.secret !== '',
totpCredentials?.secret_data !== null &&
totpCredentials?.secret_data !== '',
isTwoFactorAuthenticated: false,
});
}
// Create a new user and send a welcome email - or other onboarding process
private async handleNewUserRegistration(email: string) {
const newUser = await this.usersRepository.create({ email, verified: true, avatar: null })
const newUser = await this.usersRepository.create({ email, verified: true })
this.mailerService.sendWelcome({ to: email, props: null });
// TODO: add whatever onboarding process or extra data you need here
return newUser

View file

@ -1,4 +1,4 @@
import { fail, error, type Actions } from '@sveltejs/kit';
import { fail, type Actions } from '@sveltejs/kit';
import { eq, or } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { zod } from 'sveltekit-superforms/adapters';
@ -7,46 +7,57 @@ import { redirect } from 'sveltekit-flash-message/server';
import { RateLimiter } from 'sveltekit-rate-limiter/server';
import db from '../../../db';
import { lucia } from '$lib/server/auth';
import { signInSchema } from '$lib/validations/auth';
import { twoFactor, usersTable, type Users } from '$db/schema';
import type { PageServerLoad } from './$types';
import { userFullyAuthenticated, userNotFullyAuthenticated } from '$lib/server/auth-utils';
import {signinUsernameDto} from "$lib/dtos/signin-username.dto";
export const load: PageServerLoad = async (event) => {
const { locals, cookies } = event;
const { user, session } = event.locals;
const { locals } = event;
if (userFullyAuthenticated(user, session)) {
const authedUser = await locals.getAuthedUser();
if (authedUser) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
} else if (userNotFullyAuthenticated(user, session)) {
await lucia.invalidateSession(locals.session!.id!);
const sessionCookie = lucia.createBlankSessionCookie();
cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes,
});
}
const form = await superValidate(event, zod(signInSchema));
// if (userFullyAuthenticated(user, session)) {
// const message = { type: 'success', message: 'You are already signed in' } as const;
// throw redirect('/', message, event);
// } else if (userNotFullyAuthenticated(user, session)) {
// await lucia.invalidateSession(locals.session!.id!);
// const sessionCookie = lucia.createBlankSessionCookie();
// cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
// }
const form = await superValidate(event, zod(signinUsernameDto));
return {
form,
};
};
const limiter = new RateLimiter({
// A rate is defined by [number, unit]
IPUA: [5, 'm'],
});
export const actions: Actions = {
default: async (event) => {
if (await limiter.isLimited(event)) {
throw error(429);
}
// if (await limiter.isLimited(event)) {
// throw error(429);
// }
const { locals } = event;
const form = await superValidate(event, zod(signInSchema));
const authedUser = await locals.getAuthedUser();
if (authedUser) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const form = await superValidate(event, zod(signinUsernameDto));
const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse);
if (error) return setError(form, 'username', error);
if (!form.valid) {
form.data.password = '';

View file

@ -67,7 +67,7 @@
<form method="POST" use:enhance>
<Form.Field form={superLoginForm} name="username">
<Form.Control let:attrs>
<Form.Label for="username">Username/Email</Form.Label>
<Form.Label for="username">Username</Form.Label>
<Input {...attrs} autocomplete="username" bind:value={$loginForm.username} />
</Form.Control>
<Form.FieldErrors />

View file

@ -12,7 +12,6 @@ import { add_user_to_role } from '$server/roles';
import db from '../../../db';
import { collections, usersTable, wishlists } from '$db/schema';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { userFullyAuthenticated, userNotFullyAuthenticated } from '$lib/server/auth-utils';
const limiter = new RateLimiter({
// A rate is defined by [number, unit]
@ -30,8 +29,7 @@ const signUpDefaults = {
};
export const load: PageServerLoad = async (event) => {
const { locals, cookies } = event;
const { user, session } = event.locals;
const { locals } = event;
const authedUser = await locals.getAuthedUser();