Creating session cookie util, typing cookies, and fixing session creation and cookie creation for flows.

This commit is contained in:
Bradley Shellnut 2024-11-08 10:41:32 -08:00
parent 50cd97993a
commit 7d4b1a8eb9
9 changed files with 72 additions and 100 deletions

View file

@ -93,13 +93,13 @@
"@oslojs/otp": "^1.0.0", "@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0", "@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2", "@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-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.7", "@sveltejs/adapter-vercel": "^5.4.7",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"bits-ui": "^0.21.16", "bits-ui": "^0.21.16",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.25.1", "bullmq": "^5.25.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^1.0.1", "cookie": "^1.0.1",

View file

@ -66,8 +66,8 @@ importers:
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.2.2 version: 2.2.2
'@scalar/hono-api-reference': '@scalar/hono-api-reference':
specifier: ^0.5.158 specifier: ^0.5.159
version: 0.5.158(hono@4.6.9) version: 0.5.159(hono@4.6.9)
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.2.9 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))) 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 specifier: ^1.9.1
version: 1.9.1 version: 1.9.1
bullmq: bullmq:
specifier: ^5.25.1 specifier: ^5.25.3
version: 5.25.1 version: 5.25.3
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
@ -2156,18 +2156,18 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@scalar/hono-api-reference@0.5.158': '@scalar/hono-api-reference@0.5.159':
resolution: {integrity: sha512-2P7l/ivuC/RWpAKddLtkqIZ89TA5/QlpfEBrdpH9/yjn4NpR5XkbtT+/8uZVELCyjfUMpGcaNXrxuJERFJ3sxA==} resolution: {integrity: sha512-nUKaN0CKvytbXPj9b6taF/efKKRqEUwhVxlfLVjrJXN0eHNHDWxG9e/5Tyw1o2MXJo1cQpGZ4qTh48k/8u6ZjA==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
hono: ^4.0.0 hono: ^4.0.0
'@scalar/openapi-types@0.1.4': '@scalar/openapi-types@0.1.5':
resolution: {integrity: sha512-+wRXgmqzgDnj8Dxqf4OOPMPo4or/LRd1Bsy4pnrIW0yBt8rKSdtBb+jH/aRnhgDDmKVjWxJ+KFk7WlSKvZwNTw==} resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@scalar/types@0.0.18': '@scalar/types@0.0.19':
resolution: {integrity: sha512-gfJB/e9Rq/vjsiWlNwBkaIAZVb9v5guHQB5uVoVFcU0gdAuXni0KVxFxl3gGTu2zhBdB+DkixjyPcNzpqwksmA==} resolution: {integrity: sha512-wOxtXd35BS0DaVhBopQUB8c8hfLQ+/PKEr99GbOZW+4DWCrEB8JfWJgvpJyxHU6by7LHNVY4fvpFQR7Ezh1IIw==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@sideway/address@4.1.5': '@sideway/address@4.1.5':
@ -2567,8 +2567,8 @@ packages:
buffer@6.0.3: buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bullmq@5.25.1: bullmq@5.25.3:
resolution: {integrity: sha512-3q87TXIN31OgNeHT9n4nQWVV5wMog6dXUAoz1rAMXjb1QT2Fn8pElg5m/IHomqe7Q7FA/3b/8q4W8RCAFFUmRA==} resolution: {integrity: sha512-nUFTszxV/V3qJMZQxSMNOBF1HiGKh895WyJmE5keUonkutpTsxdYIr0dzVUTPbhXvBvW9LWlY7BetWY3afy/MQ==}
bytes@3.1.2: bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
@ -6560,16 +6560,16 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.24.0': '@rollup/rollup-win32-x64-msvc@4.24.0':
optional: true 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: dependencies:
'@scalar/types': 0.0.18 '@scalar/types': 0.0.19
hono: 4.6.9 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: dependencies:
'@scalar/openapi-types': 0.1.4 '@scalar/openapi-types': 0.1.5
'@unhead/schema': 1.11.11 '@unhead/schema': 1.11.11
'@sideway/address@4.1.5': '@sideway/address@4.1.5':
@ -7061,7 +7061,7 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
bullmq@5.25.1: bullmq@5.25.3:
dependencies: dependencies:
cron-parser: 4.9.0 cron-parser: 4.9.0
ioredis: 5.4.1 ioredis: 5.4.1

View file

@ -1,5 +1,8 @@
import { config } from '$lib/server/api/common/config'; import { config } from '$lib/server/api/common/config';
import env from '$lib/server/api/common/env'; 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'; import { TimeSpan } from 'oslo';
export const cookieMaxAge = 60 * 60 * 24 * 30; 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 halfCookieExpiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
export const cookieName = 'session'; 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 { return {
name: cookieName, name: cookieName,
value: token, value: token,
@ -25,7 +34,7 @@ export function createSessionTokenCookie(token: string, expiresAt: Date) {
}; };
} }
export function createBlankSessionTokenCookie() { export function createBlankSessionTokenCookie(): SessionCookie {
return { return {
name: cookieName, name: cookieName,
value: '', 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,
});
}

View file

@ -1,6 +1,6 @@
import { StatusCodes } from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller'; 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 { changePasswordDto } from '$lib/server/api/dtos/change-password.dto';
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'; import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.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.iamService.updatePassword(user.id, { password, confirm_password });
await this.sessionsService.invalidateSession(user.id); await this.sessionsService.invalidateSession(user.id);
await this.loginRequestService.createUserSession(user.id, c.req, undefined); await this.loginRequestService.createUserSession(user.id, c.req, undefined);
deleteSessionTokenCookie(c); const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' }); return c.json({ status: 'success' });
} catch (error) { } catch (error) {
console.error('Error updating password', error); console.error('Error updating password', error);
@ -108,7 +109,8 @@ export class IamController extends Controller {
.post('/logout', requireAuth, openApi(logout), async (c) => { .post('/logout', requireAuth, openApi(logout), async (c) => {
const sessionId = c.var.session.id; const sessionId = c.var.session.id;
await this.iamService.logout(sessionId); await this.iamService.logout(sessionId);
deleteSessionTokenCookie(c); const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' }); return c.json({ status: 'success' });
}); });
} }

View file

@ -1,12 +1,10 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller'; 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 { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import { SessionsService } from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/services/sessions.service';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { setCookie } from 'hono/cookie';
import { TimeSpan } from 'oslo';
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { limiter } from '../middleware/rate-limiter.middleware'; import { limiter } from '../middleware/rate-limiter.middleware';
import { LoginRequestsService } from '../services/loginrequest.service'; 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 session = await this.loginRequestsService.verify({ username, password }, c.req);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
console.log('set cookie', sessionCookie); console.log('set cookie', sessionCookie);
setCookie(c, sessionCookie.name, sessionCookie.value, { setSessionCookie(c, sessionCookie);
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' }); return c.json({ message: 'ok' });
}, },
); );

View file

@ -1,7 +1,7 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller'; import { Controller } from '$lib/server/api/common/types/controller';
import type { OAuthUser } from '$lib/server/api/common/types/oauth'; 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 { OAuthService } from '$lib/server/api/services/oauth.service';
import { SessionsService } from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/services/sessions.service';
import { github, google } from '$lib/server/auth'; import { github, google } from '$lib/server/auth';
@ -50,21 +50,8 @@ export class OAuthController extends Controller {
const sessionToken = this.sessionsService.generateSessionToken(); const sessionToken = this.sessionsService.generateSessionToken();
const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false); const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false);
const sessionCookie = createSessionTokenCookie(session.id, new Date(new TimeSpan(2, 'w').milliseconds())); const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
setSessionCookie(c, 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' }); return c.json({ message: 'ok' });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -106,22 +93,10 @@ export class OAuthController extends Controller {
}; };
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google'); const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google');
const sessionToken = this.sessionsService.generateSessionToken();
const session = await this.sessionsService.createSession(); const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false);
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id); const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
setSessionCookie(c, 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' }); return c.json({ message: 'ok' });
} catch (error) { } catch (error) {

View file

@ -1,10 +1,15 @@
import 'reflect-metadata'; 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 { SessionsService } from '$lib/server/api/services/sessions.service';
import type { MiddlewareHandler } from 'hono'; import type { MiddlewareHandler } from 'hono';
import { setCookie } from 'hono/cookie';
import { createMiddleware } from 'hono/factory'; import { createMiddleware } from 'hono/factory';
import { TimeSpan } from 'oslo';
import { parseCookies } from 'oslo/cookie'; import { parseCookies } from 'oslo/cookie';
import { verifyRequestOrigin } from 'oslo/request'; import { verifyRequestOrigin } from 'oslo/request';
import { container } from 'tsyringe'; import { container } from 'tsyringe';
@ -35,32 +40,13 @@ export const validateAuthSession: MiddlewareHandler<AppBindings> = createMiddlew
} }
const { session, user } = await sessionService.validateSessionToken(sessionId); const { session, user } = await sessionService.validateSessionToken(sessionId);
let sessionCookie: SessionCookie;
if (session !== null) { if (session !== null) {
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); 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 { } else {
const sessionCookie = createBlankSessionTokenCookie(); 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,
});
} }
setSessionCookie(c, sessionCookie);
c.set('session', session); c.set('session', session);
c.set('user', user); c.set('user', user);
return next(); return next();

View file

@ -27,12 +27,12 @@ simple as possible. This makes the service easier to read, test and understand.
@injectable() @injectable()
export class IamService { export class IamService {
constructor( constructor(
@inject(SessionsService) private luciaService: SessionsService, @inject(SessionsService) private sessionsService: SessionsService,
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
) {} ) {}
async logout(sessionId: string) { async logout(sessionId: string) {
return this.luciaService.lucia.invalidateSession(sessionId); return this.sessionsService.invalidateSession(sessionId);
} }
async updateProfile(userId: string, data: UpdateProfileDto) { async updateProfile(userId: string, data: UpdateProfileDto) {

View file

@ -40,6 +40,7 @@ export class SessionsService {
} }
async validateSessionToken(token: string): Promise<SessionValidationResult> { async validateSessionToken(token: string): Promise<SessionValidationResult> {
// 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 sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const sessions = await this.sessionsRepository.findBySessionId(token); const sessions = await this.sessionsRepository.findBySessionId(token);
if (sessions.length < 1) { if (sessions.length < 1) {