From eeca4e41032df557f4fbf91b90c026fea2b1e2d5 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Wed, 14 Aug 2024 18:07:50 -0700 Subject: [PATCH] Fixing login cookie max-age. --- ...in-email.dto.ts => signin-username.dto.ts} | 4 +- .../api/controllers/login.controller.ts | 26 +++++++-- .../server/api/infrastructure/auth/lucia.ts | 2 +- .../api/services/loginrequest.service.ts | 12 ++-- src/routes/(auth)/login/+page.server.ts | 57 +++++++++++-------- src/routes/(auth)/login/+page.svelte | 2 +- src/routes/(auth)/sign-up/+page.server.ts | 4 +- 7 files changed, 66 insertions(+), 41 deletions(-) rename src/lib/dtos/{signin-email.dto.ts => signin-username.dto.ts} (69%) diff --git a/src/lib/dtos/signin-email.dto.ts b/src/lib/dtos/signin-username.dto.ts similarity index 69% rename from src/lib/dtos/signin-email.dto.ts rename to src/lib/dtos/signin-username.dto.ts index 1ade43a..0ab4a3a 100644 --- a/src/lib/dtos/signin-email.dto.ts +++ b/src/lib/dtos/signin-username.dto.ts @@ -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; +export type SigninUsernameDto = z.infer; \ No newline at end of file diff --git a/src/lib/server/api/controllers/login.controller.ts b/src/lib/server/api/controllers/login.controller.ts index 996e98e..c86cefc 100644 --- a/src/lib/server/api/controllers/login.controller.ts +++ b/src/lib/server/api/controllers/login.controller.ts @@ -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(); 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' }); }) } } diff --git a/src/lib/server/api/infrastructure/auth/lucia.ts b/src/lib/server/api/infrastructure/auth/lucia.ts index a9873c1..712338d 100644 --- a/src/lib/server/api/infrastructure/auth/lucia.ts +++ b/src/lib/server/api/infrastructure/auth/lucia.ts @@ -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) diff --git a/src/lib/server/api/services/loginrequest.service.ts b/src/lib/server/api/services/loginrequest.service.ts index f2138b3..eafaf28 100644 --- a/src/lib/server/api/services/loginrequest.service.ts +++ b/src/lib/server/api/services/loginrequest.service.ts @@ -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 diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 5da474b..9c254cc 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -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 = ''; diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte index 188b4c4..0239c9f 100644 --- a/src/routes/(auth)/login/+page.svelte +++ b/src/routes/(auth)/login/+page.svelte @@ -67,7 +67,7 @@
- Username/Email + Username diff --git a/src/routes/(auth)/sign-up/+page.server.ts b/src/routes/(auth)/sign-up/+page.server.ts index b9db366..aa64cdf 100644 --- a/src/routes/(auth)/sign-up/+page.server.ts +++ b/src/routes/(auth)/sign-up/+page.server.ts @@ -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();