From 7d4b1a8eb9f94be79e453216591ac3eb74a5934e Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Fri, 8 Nov 2024 10:41:32 -0800 Subject: [PATCH] Creating session cookie util, typing cookies, and fixing session creation and cookie creation for flows. --- package.json | 4 +- pnpm-lock.yaml | 36 ++++++++--------- src/lib/server/api/common/utils/cookies.ts | 25 +++++++++++- .../server/api/controllers/iam.controller.ts | 8 ++-- .../api/controllers/login.controller.ts | 17 +------- .../api/controllers/oauth.controller.ts | 39 ++++--------------- .../server/api/middleware/auth.middleware.ts | 38 ++++++------------ src/lib/server/api/services/iam.service.ts | 4 +- .../server/api/services/sessions.service.ts | 1 + 9 files changed, 72 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 95fca42..fde1d1b 100644 --- a/package.json +++ b/package.json @@ -93,13 +93,13 @@ "@oslojs/otp": "^1.0.0", "@oslojs/webauthn": "^1.0.0", "@paralleldrive/cuid2": "^2.2.2", - "@scalar/hono-api-reference": "^0.5.158", + "@scalar/hono-api-reference": "^0.5.159", "@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-vercel": "^5.4.7", "@types/feather-icons": "^4.29.4", "bits-ui": "^0.21.16", "boardgamegeekclient": "^1.9.1", - "bullmq": "^5.25.1", + "bullmq": "^5.25.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cookie": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a4ac31..e86c86d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^2.2.2 version: 2.2.2 '@scalar/hono-api-reference': - specifier: ^0.5.158 - version: 0.5.158(hono@4.6.9) + specifier: ^0.5.159 + version: 0.5.159(hono@4.6.9) '@sveltejs/adapter-node': specifier: ^5.2.9 version: 5.2.9(@sveltejs/kit@2.8.0(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.10(@types/node@20.17.6))) @@ -84,8 +84,8 @@ importers: specifier: ^1.9.1 version: 1.9.1 bullmq: - specifier: ^5.25.1 - version: 5.25.1 + specifier: ^5.25.3 + version: 5.25.3 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -2156,18 +2156,18 @@ packages: cpu: [x64] os: [win32] - '@scalar/hono-api-reference@0.5.158': - resolution: {integrity: sha512-2P7l/ivuC/RWpAKddLtkqIZ89TA5/QlpfEBrdpH9/yjn4NpR5XkbtT+/8uZVELCyjfUMpGcaNXrxuJERFJ3sxA==} + '@scalar/hono-api-reference@0.5.159': + resolution: {integrity: sha512-nUKaN0CKvytbXPj9b6taF/efKKRqEUwhVxlfLVjrJXN0eHNHDWxG9e/5Tyw1o2MXJo1cQpGZ4qTh48k/8u6ZjA==} engines: {node: '>=18'} peerDependencies: hono: ^4.0.0 - '@scalar/openapi-types@0.1.4': - resolution: {integrity: sha512-+wRXgmqzgDnj8Dxqf4OOPMPo4or/LRd1Bsy4pnrIW0yBt8rKSdtBb+jH/aRnhgDDmKVjWxJ+KFk7WlSKvZwNTw==} + '@scalar/openapi-types@0.1.5': + resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==} engines: {node: '>=18'} - '@scalar/types@0.0.18': - resolution: {integrity: sha512-gfJB/e9Rq/vjsiWlNwBkaIAZVb9v5guHQB5uVoVFcU0gdAuXni0KVxFxl3gGTu2zhBdB+DkixjyPcNzpqwksmA==} + '@scalar/types@0.0.19': + resolution: {integrity: sha512-wOxtXd35BS0DaVhBopQUB8c8hfLQ+/PKEr99GbOZW+4DWCrEB8JfWJgvpJyxHU6by7LHNVY4fvpFQR7Ezh1IIw==} engines: {node: '>=18'} '@sideway/address@4.1.5': @@ -2567,8 +2567,8 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bullmq@5.25.1: - resolution: {integrity: sha512-3q87TXIN31OgNeHT9n4nQWVV5wMog6dXUAoz1rAMXjb1QT2Fn8pElg5m/IHomqe7Q7FA/3b/8q4W8RCAFFUmRA==} + bullmq@5.25.3: + resolution: {integrity: sha512-nUFTszxV/V3qJMZQxSMNOBF1HiGKh895WyJmE5keUonkutpTsxdYIr0dzVUTPbhXvBvW9LWlY7BetWY3afy/MQ==} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -6560,16 +6560,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.0': optional: true - '@scalar/hono-api-reference@0.5.158(hono@4.6.9)': + '@scalar/hono-api-reference@0.5.159(hono@4.6.9)': dependencies: - '@scalar/types': 0.0.18 + '@scalar/types': 0.0.19 hono: 4.6.9 - '@scalar/openapi-types@0.1.4': {} + '@scalar/openapi-types@0.1.5': {} - '@scalar/types@0.0.18': + '@scalar/types@0.0.19': dependencies: - '@scalar/openapi-types': 0.1.4 + '@scalar/openapi-types': 0.1.5 '@unhead/schema': 1.11.11 '@sideway/address@4.1.5': @@ -7061,7 +7061,7 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bullmq@5.25.1: + bullmq@5.25.3: dependencies: cron-parser: 4.9.0 ioredis: 5.4.1 diff --git a/src/lib/server/api/common/utils/cookies.ts b/src/lib/server/api/common/utils/cookies.ts index 7ba7e72..2aaa77a 100644 --- a/src/lib/server/api/common/utils/cookies.ts +++ b/src/lib/server/api/common/utils/cookies.ts @@ -1,5 +1,8 @@ import { config } from '$lib/server/api/common/config'; import env from '$lib/server/api/common/env'; +import type { Context } from 'hono'; +import { setCookie } from 'hono/cookie'; +import type { CookieOptions } from 'hono/utils/cookie'; import { TimeSpan } from 'oslo'; export const cookieMaxAge = 60 * 60 * 24 * 30; @@ -9,7 +12,13 @@ export const halfCookieExpiresMilliseconds = cookieExpiresMilliseconds / 2; export const halfCookieExpiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); export const cookieName = 'session'; -export function createSessionTokenCookie(token: string, expiresAt: Date) { +export type SessionCookie = { + name: string; + value: string; + attributes: CookieOptions; +}; + +export function createSessionTokenCookie(token: string, expiresAt: Date): SessionCookie { return { name: cookieName, value: token, @@ -25,7 +34,7 @@ export function createSessionTokenCookie(token: string, expiresAt: Date) { }; } -export function createBlankSessionTokenCookie() { +export function createBlankSessionTokenCookie(): SessionCookie { return { name: cookieName, value: '', @@ -40,3 +49,15 @@ export function createBlankSessionTokenCookie() { }, }; } + +export function setSessionCookie(c: Context, sessionCookie: SessionCookie) { + setCookie(c, sessionCookie.name, sessionCookie.value, { + path: sessionCookie.attributes.path, + maxAge: sessionCookie.attributes?.maxAge, + domain: sessionCookie.attributes.domain, + sameSite: sessionCookie.attributes.sameSite as undefined, + secure: sessionCookie.attributes.secure, + httpOnly: sessionCookie.attributes.httpOnly, + expires: sessionCookie.attributes.expires, + }); +} diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts index 9d38c1c..fc2b1c8 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/iam.controller.ts @@ -1,6 +1,6 @@ import { StatusCodes } from '$lib/constants/status-codes'; import { Controller } from '$lib/server/api/common/types/controller'; -import { createBlankSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; +import { createBlankSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies'; import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto'; import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'; import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'; @@ -81,7 +81,8 @@ export class IamController extends Controller { await this.iamService.updatePassword(user.id, { password, confirm_password }); await this.sessionsService.invalidateSession(user.id); await this.loginRequestService.createUserSession(user.id, c.req, undefined); - deleteSessionTokenCookie(c); + const sessionCookie = createBlankSessionTokenCookie(); + setSessionCookie(c, sessionCookie); return c.json({ status: 'success' }); } catch (error) { console.error('Error updating password', error); @@ -108,7 +109,8 @@ export class IamController extends Controller { .post('/logout', requireAuth, openApi(logout), async (c) => { const sessionId = c.var.session.id; await this.iamService.logout(sessionId); - deleteSessionTokenCookie(c); + const sessionCookie = createBlankSessionTokenCookie(); + setSessionCookie(c, sessionCookie); return c.json({ status: 'success' }); }); } diff --git a/src/lib/server/api/controllers/login.controller.ts b/src/lib/server/api/controllers/login.controller.ts index 910051a..90b4a49 100644 --- a/src/lib/server/api/controllers/login.controller.ts +++ b/src/lib/server/api/controllers/login.controller.ts @@ -1,12 +1,10 @@ import 'reflect-metadata'; import { Controller } from '$lib/server/api/common/types/controller'; -import { cookieExpiresAt, createSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; +import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } 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'; import { openApi } from 'hono-zod-openapi'; -import { setCookie } from 'hono/cookie'; -import { TimeSpan } from 'oslo'; import { inject, injectable } from 'tsyringe'; import { limiter } from '../middleware/rate-limiter.middleware'; import { LoginRequestsService } from '../services/loginrequest.service'; @@ -32,18 +30,7 @@ export class LoginController extends Controller { const session = await this.loginRequestsService.verify({ username, password }, c.req); const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); 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, - }); + setSessionCookie(c, sessionCookie); return c.json({ message: 'ok' }); }, ); diff --git a/src/lib/server/api/controllers/oauth.controller.ts b/src/lib/server/api/controllers/oauth.controller.ts index b80c3b5..f783619 100644 --- a/src/lib/server/api/controllers/oauth.controller.ts +++ b/src/lib/server/api/controllers/oauth.controller.ts @@ -1,7 +1,7 @@ import 'reflect-metadata'; import { Controller } from '$lib/server/api/common/types/controller'; import type { OAuthUser } from '$lib/server/api/common/types/oauth'; -import { createSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; +import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies'; import { OAuthService } from '$lib/server/api/services/oauth.service'; import { SessionsService } from '$lib/server/api/services/sessions.service'; import { github, google } from '$lib/server/auth'; @@ -50,21 +50,8 @@ export class OAuthController extends Controller { const sessionToken = this.sessionsService.generateSessionToken(); 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, { - 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, - }); - + const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); + setSessionCookie(c, sessionCookie); return c.json({ message: 'ok' }); } catch (error) { console.error(error); @@ -106,22 +93,10 @@ export class OAuthController extends Controller { }; const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google'); - - const session = await this.sessionsService.createSession(); - const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id); - - 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, - }); + const sessionToken = this.sessionsService.generateSessionToken(); + const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false); + const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); + setSessionCookie(c, sessionCookie); return c.json({ message: 'ok' }); } catch (error) { diff --git a/src/lib/server/api/middleware/auth.middleware.ts b/src/lib/server/api/middleware/auth.middleware.ts index 4ba6b9f..9d9f6be 100644 --- a/src/lib/server/api/middleware/auth.middleware.ts +++ b/src/lib/server/api/middleware/auth.middleware.ts @@ -1,10 +1,15 @@ import 'reflect-metadata'; -import { cookieExpiresAt, cookieName, createBlankSessionTokenCookie, createSessionTokenCookie } from '$lib/server/api/common/utils/cookies'; +import { + type SessionCookie, + cookieExpiresAt, + cookieName, + createBlankSessionTokenCookie, + createSessionTokenCookie, + setSessionCookie, +} 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'; @@ -35,32 +40,13 @@ export const validateAuthSession: MiddlewareHandler = createMiddlew } const { session, user } = await sessionService.validateSessionToken(sessionId); + let sessionCookie: SessionCookie; 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, - }); + sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); } 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, - }); + sessionCookie = createBlankSessionTokenCookie(); } + setSessionCookie(c, sessionCookie); c.set('session', session); c.set('user', user); return next(); diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts index d6f6103..cd3d6d4 100644 --- a/src/lib/server/api/services/iam.service.ts +++ b/src/lib/server/api/services/iam.service.ts @@ -27,12 +27,12 @@ simple as possible. This makes the service easier to read, test and understand. @injectable() export class IamService { constructor( - @inject(SessionsService) private luciaService: SessionsService, + @inject(SessionsService) private sessionsService: SessionsService, @inject(UsersService) private readonly usersService: UsersService, ) {} async logout(sessionId: string) { - return this.luciaService.lucia.invalidateSession(sessionId); + return this.sessionsService.invalidateSession(sessionId); } async updateProfile(userId: string, data: UpdateProfileDto) { diff --git a/src/lib/server/api/services/sessions.service.ts b/src/lib/server/api/services/sessions.service.ts index 39d92c4..3e6db7d 100644 --- a/src/lib/server/api/services/sessions.service.ts +++ b/src/lib/server/api/services/sessions.service.ts @@ -40,6 +40,7 @@ export class SessionsService { } async validateSessionToken(token: string): Promise { + // TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm // const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const sessions = await this.sessionsRepository.findBySessionId(token); if (sessions.length < 1) {