diff --git a/src/lib/server/api/common/types/hono.ts b/src/lib/server/api/common/types/hono.ts index f339c92..ad4cd9d 100644 --- a/src/lib/server/api/common/types/hono.ts +++ b/src/lib/server/api/common/types/hono.ts @@ -1,7 +1,8 @@ +import type { Sessions } from '$lib/server/api/databases/postgres/tables'; import type { Hono } from 'hono'; import type { PinoLogger } from 'hono-pino'; import type { Promisify, RateLimitInfo } from 'hono-rate-limiter'; -import type { Session, User } from 'lucia'; +import type { User } from 'lucia'; // export type AppOpenAPI = OpenAPIHono; export type AppOpenAPI = Hono; @@ -9,7 +10,7 @@ export type AppOpenAPI = Hono; export type AppBindings = { Variables: { logger: PinoLogger; - session: Session | null; + session: Sessions | null; user: User | null; rateLimit: RateLimitInfo; rateLimitStore: { @@ -22,7 +23,7 @@ export type AppBindings = { export type HonoTypes = { Variables: { logger: PinoLogger; - session: Session | null; + session: Sessions | null; user: User | null; rateLimit: RateLimitInfo; rateLimitStore: { diff --git a/src/lib/server/api/common/utils/cookies.ts b/src/lib/server/api/common/utils/cookies.ts index 6e0f4c1..7ba7e72 100644 --- a/src/lib/server/api/common/utils/cookies.ts +++ b/src/lib/server/api/common/utils/cookies.ts @@ -1,13 +1,21 @@ import { config } from '$lib/server/api/common/config'; import env from '$lib/server/api/common/env'; +import { TimeSpan } from 'oslo'; + +export const cookieMaxAge = 60 * 60 * 24 * 30; +export const cookieExpiresMilliseconds = new TimeSpan(2, 'w').milliseconds(); +export const cookieExpiresAt = new Date(Date.now() + cookieExpiresMilliseconds); +export const halfCookieExpiresMilliseconds = cookieExpiresMilliseconds / 2; +export const halfCookieExpiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); +export const cookieName = 'session'; export function createSessionTokenCookie(token: string, expiresAt: Date) { return { - name: 'session', + name: cookieName, value: token, attributes: { path: '/', - maxAge: 60 * 60 * 24 * 30, + maxAge: cookieMaxAge, domain: env.DOMAIN, sameSite: 'lax', secure: config.isProduction, @@ -19,7 +27,7 @@ export function createSessionTokenCookie(token: string, expiresAt: Date) { export function createBlankSessionTokenCookie() { return { - name: 'session', + name: cookieName, value: '', attributes: { path: '/', diff --git a/src/lib/server/api/controllers/login.controller.ts b/src/lib/server/api/controllers/login.controller.ts index e04e3dd..910051a 100644 --- a/src/lib/server/api/controllers/login.controller.ts +++ b/src/lib/server/api/controllers/login.controller.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; import { Controller } from '$lib/server/api/common/types/controller'; +import { cookieExpiresAt, createSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto'; import { SessionsService } from '$lib/server/api/services/sessions.service'; import { zValidator } from '@hono/zod-validator'; @@ -29,7 +30,7 @@ export class LoginController extends Controller { async (c) => { const { username, password } = c.req.valid('json'); const session = await this.loginRequestsService.verify({ username, password }, c.req); - const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id); + const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); console.log('set cookie', sessionCookie); setCookie(c, sessionCookie.name, sessionCookie.value, { path: sessionCookie.attributes.path, diff --git a/src/lib/server/api/controllers/oauth.controller.ts b/src/lib/server/api/controllers/oauth.controller.ts index 3a4f3a5..b80c3b5 100644 --- a/src/lib/server/api/controllers/oauth.controller.ts +++ b/src/lib/server/api/controllers/oauth.controller.ts @@ -8,6 +8,7 @@ import { github, google } from '$lib/server/auth'; import { OAuth2RequestError } from 'arctic'; import { getCookie, setCookie } from 'hono/cookie'; import { TimeSpan } from 'oslo'; + import { inject, injectable } from 'tsyringe'; @injectable() @@ -48,8 +49,7 @@ export class OAuthController extends Controller { const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'github'); const sessionToken = this.sessionsService.generateSessionToken(); - const session = await this.sessionsService.createSession(sessionToken, userId, - req.); + const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false); const sessionCookie = createSessionTokenCookie(session.id, new Date(new TimeSpan(2, 'w').milliseconds())); setCookie(c, sessionCookie.name, sessionCookie.value, { @@ -107,7 +107,7 @@ export class OAuthController extends Controller { const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google'); - const session = await this.luciaService.lucia.createSession(userId, {}); + const session = await this.sessionsService.createSession(); const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id); setCookie(c, sessionCookie.name, sessionCookie.value, { diff --git a/src/lib/server/api/middleware/auth.middleware.ts b/src/lib/server/api/middleware/auth.middleware.ts index 058df44..4ba6b9f 100644 --- a/src/lib/server/api/middleware/auth.middleware.ts +++ b/src/lib/server/api/middleware/auth.middleware.ts @@ -1,7 +1,11 @@ -import { createSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; +import 'reflect-metadata'; +import { cookieExpiresAt, cookieName, createBlankSessionTokenCookie, createSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; import { SessionsService } from '$lib/server/api/services/sessions.service'; import type { MiddlewareHandler } from 'hono'; +import { setCookie } from 'hono/cookie'; import { createMiddleware } from 'hono/factory'; +import { TimeSpan } from 'oslo'; +import { parseCookies } from 'oslo/cookie'; import { verifyRequestOrigin } from 'oslo/request'; import { container } from 'tsyringe'; import type { AppBindings } from '../common/types/hono'; @@ -22,19 +26,40 @@ export const verifyOrigin: MiddlewareHandler = createMiddleware(asy }); export const validateAuthSession: MiddlewareHandler = createMiddleware(async (c, next) => { - const sessionId = lucia.readSessionCookie(c.req.header('Cookie') ?? ''); + const cookies = parseCookies(c.req.header('Cookie') ?? ''); + const sessionId = cookies.get(cookieName) ?? null; if (!sessionId) { c.set('user', null); c.set('session', null); return next(); } - const { session, user } = await lucia.validateSession(sessionId); - if (session?.fresh) { - c.header('Set-Cookie', createSessionTokenCookie(session.id).serialize(), { append: true }); - } - if (!session) { - c.header('Set-Cookie', lucia.createBlankSessionCookie().serialize(), { append: true }); + const { session, user } = await sessionService.validateSessionToken(sessionId); + if (session !== null) { + const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); + 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, + }); + } else { + const sessionCookie = createBlankSessionTokenCookie(); + setCookie(c, sessionCookie.name, sessionCookie.value, { + path: sessionCookie.attributes.path, + maxAge: sessionCookie.attributes?.maxAge, + domain: sessionCookie.attributes.domain, + sameSite: sessionCookie.attributes.sameSite as any, + secure: sessionCookie.attributes.secure, + httpOnly: sessionCookie.attributes.httpOnly, + expires: sessionCookie.attributes.expires, + }); } c.set('session', session); c.set('user', user); diff --git a/src/lib/server/api/middleware/require-auth.middleware.ts b/src/lib/server/api/middleware/require-auth.middleware.ts index 5ca6e09..3c3e0a6 100644 --- a/src/lib/server/api/middleware/require-auth.middleware.ts +++ b/src/lib/server/api/middleware/require-auth.middleware.ts @@ -1,15 +1,16 @@ -import { Unauthorized } from '$lib/server/api/common/exceptions' -import type { MiddlewareHandler } from 'hono' -import { createMiddleware } from 'hono/factory' -import type { Session, User } from 'lucia' +import { Unauthorized } from '$lib/server/api/common/exceptions'; +import type { Sessions } from '$lib/server/api/databases/postgres/tables'; +import type { MiddlewareHandler } from 'hono'; +import { createMiddleware } from 'hono/factory'; +import type { User } from 'lucia'; export const requireAuth: MiddlewareHandler<{ Variables: { - session: Session - user: User - } + session: Sessions; + user: User; + }; }> = createMiddleware(async (c, next) => { - const user = c.var.user - if (!user) throw Unauthorized('You must be logged in to access this resource') - return next() -}) + const user = c.var.user; + if (!user) throw Unauthorized('You must be logged in to access this resource'); + return next(); +}); diff --git a/src/lib/server/api/packages/drizzle.ts b/src/lib/server/api/packages/drizzle.ts index e5e5da2..75a7a16 100644 --- a/src/lib/server/api/packages/drizzle.ts +++ b/src/lib/server/api/packages/drizzle.ts @@ -1,10 +1,10 @@ import { drizzle } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; +import pg from 'pg'; import { config } from '../common/config'; import * as schema from '../databases/postgres/tables'; // create the connection -export const pool = new Pool({ +export const pool = new pg.Pool({ user: config.postgres.user, password: config.postgres.password, host: config.postgres.host, diff --git a/src/lib/server/api/services/sessions.service.ts b/src/lib/server/api/services/sessions.service.ts index 73356c7..39d92c4 100644 --- a/src/lib/server/api/services/sessions.service.ts +++ b/src/lib/server/api/services/sessions.service.ts @@ -1,7 +1,7 @@ +import { cookieExpiresAt, cookieExpiresMilliseconds, halfCookieExpiresMilliseconds } from '$lib/server/api/common/utils/cookies'; import { SessionsRepository } from '$lib/server/api/repositories/sessions.repository'; import { sha256 } from '@oslojs/crypto/sha2'; import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding'; -import { TimeSpan } from 'lucia'; import { inject, injectable } from 'tsyringe'; import type { Sessions, Users } from '../databases/postgres/tables'; @@ -29,7 +29,7 @@ export class SessionsService { const session: Sessions = { id: sessionId, userId, - expiresAt: new Date(Date.now() + new TimeSpan(2, 'w').seconds()), + expiresAt: cookieExpiresAt, ipCountry, ipAddress, twoFactorAuthEnabled, @@ -40,8 +40,8 @@ export class SessionsService { } async validateSessionToken(token: string): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const sessions = await this.sessionsRepository.findBySessionId(sessionId); + // const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const sessions = await this.sessionsRepository.findBySessionId(token); if (sessions.length < 1) { return { session: null, @@ -50,16 +50,16 @@ export class SessionsService { } const { user, session } = sessions[0]; if (Date.now() >= session.expiresAt.getTime()) { - await this.sessionsRepository.deleteBySessionId(sessionId); + await this.sessionsRepository.deleteBySessionId(token); return { session: null, user: null, }; } - if (Date.now() >= session.expiresAt.getTime() - new TimeSpan(1, 'w').seconds()) { - session.expiresAt = new Date(Date.now() + new TimeSpan(2, 'w').seconds()); - await this.sessionsRepository.updateSessionExpiresAt(sessionId, session.expiresAt); + if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) { + session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); + await this.sessionsRepository.updateSessionExpiresAt(token, session.expiresAt); } return { session, user };