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/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",

View file

@ -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

View file

@ -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,
});
}

View file

@ -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' });
});
}

View file

@ -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' });
},
);

View file

@ -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) {

View file

@ -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<AppBindings> = 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();

View file

@ -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) {

View file

@ -40,6 +40,7 @@ export class SessionsService {
}
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 sessions = await this.sessionsRepository.findBySessionId(token);
if (sessions.length < 1) {