From 8f76173bc57c62a6e810c9f1c9ba6b0dd8f92a80 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 11 Sep 2025 16:01:10 +0200 Subject: [PATCH] Added token refresh for session tokens --- src/hooks.server.ts | 16 ++++ src/lib/server/session_store.ts | 131 +++++++++++++++++++++----------- 2 files changed, 104 insertions(+), 43 deletions(-) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 05be73a..bb8814f 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -105,6 +105,22 @@ export let handle: Handle = async function ({ event, resolve }) { } } + if (SessionStore.get_remaining_lifetime(token) < 5 * 60 * 1000) { + + const new_expiry_date = new Date(Date.now() + 15*60*1000) + const new_token = SessionStore.reissue_access_token(token, new_expiry_date) + + if (new_token) { + event.cookies.set("session_id", new_token, { + expires: new_expiry_date, + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/' + }) + } + } + event.locals.user = user; return await resolve(event); diff --git a/src/lib/server/session_store.ts b/src/lib/server/session_store.ts index cdd5fac..7ece93c 100644 --- a/src/lib/server/session_store.ts +++ b/src/lib/server/session_store.ts @@ -1,53 +1,53 @@ -import Crypto from "node:crypto"; +import Crypto from "node:crypto" -import type { User } from "$lib/server/database"; +import type { User } from "$lib/server/database" import Logs from "$lib/server/log" interface UserSession { - user: User; - last_active: Date; + user: User + last_active: Date } export interface TokenInfo { - user_id: number; - creation_time: Date; - expiry_time: Date; + user_id: number + creation_time: Date + expiry_time: Date } class LoginError extends Error { constructor(message: string) { - super(message); + super(message) } } class UserLoginError extends LoginError { - user_id: number; + user_id: number constructor(user_id: number, message: string) { - super(message); - this.user_id = user_id; + super(message) + this.user_id = user_id } } -const TOKEN_LENGTH = 256; +const TOKEN_LENGTH = 256 -const active_users = new Map(); -const active_session_tokens = new Map(); +const active_users = new Map() +const active_session_tokens = new Map() function issue_access_token_for_user(user: User, expiry_date: Date): string { Logs.user.info(`Issuing access token for user id ${user.id}. Expiry date: ${expiry_date.toISOString()}`) - let user_session = active_users.get(user.id); + let user_session = active_users.get(user.id) if (user_session === undefined) { user_session = { user: user, last_active: new Date() - } as UserSession; + } as UserSession - active_users.set(user.id, user_session); + active_users.set(user.id, user_session) } /* | Token (256) | Expiry date as getTime | */ @@ -59,49 +59,92 @@ function issue_access_token_for_user(user: User, expiry_date: Date): string { const session_token = Buffer.concat( [ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3), Buffer.from(token_info.creation_time.getTime().toFixed(0)) ] - ).toString("base64"); + ).toString("base64") if (active_session_tokens.has(session_token)) { - throw new UserLoginError(user.id, "Tried issuing a duplicate session token."); + throw new UserLoginError(user.id, "Tried issuing a duplicate session token.") } - active_session_tokens.set(session_token, token_info); + active_session_tokens.set(session_token, token_info) - return session_token; + return session_token +} + +function reissue_access_token(token: string, expiry_date: Date): string | null { + + const token_info = active_session_tokens.get(token) + + if (!token_info || token_info.expiry_time.getTime() < Date.now()) { + Logs.user.warn("Tried to reissue an access token for an invalid access token!") + return null + } + + Logs.user.info(`Reissuing access token for user id ${token_info.user_id}. Expiry date: ${expiry_date.toISOString()}`) + + /* | Token (256) | Expiry date as getTime | */ + const new_token_info: TokenInfo = { + user_id: token_info.user_id, + creation_time: new Date(), + expiry_time: expiry_date + } + const new_session_token = Buffer.concat( + [ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3), + Buffer.from(token_info.creation_time.getTime().toFixed(0)) ] + ).toString("base64") + + if (active_session_tokens.has(new_session_token)) { + throw new UserLoginError(token_info.user_id, "Tried issuing a duplicate session token.") + } + + active_session_tokens.set(new_session_token, new_token_info) + active_session_tokens.delete(token) + + return new_session_token } function get_user_by_access_token(token: string): User | null { - const token_info = active_session_tokens.get(token); + const token_info = active_session_tokens.get(token) if (token_info === undefined || token_info.expiry_time.getTime() < Date.now()) { - return null; + return null } - const user = active_users.get(token_info.user_id); + const user = active_users.get(token_info.user_id) if (user === undefined) { - return null; + Logs.user.error(`User ${token_info.user_id} has an active session token but is not loaded when retrieving user`) + return null } - user.last_active = new Date(); + user.last_active = new Date() - return user.user; + return user.user } function logout_user_session(token: string): boolean { - const token_info = active_session_tokens.get(token); + const token_info = active_session_tokens.get(token) if (!token_info) { - Logs.user.warn(`Failed to logout user by token, because token does not exist`); - return false; + Logs.user.warn(`Failed to logout user by token, because token does not exist`) + return false } Logs.user.info(`Logging out user ${token_info?.user_id}`) - token_info.expiry_time = new Date(0); + token_info.expiry_time = new Date(0) - return true; + return true +} + +function get_remaining_lifetime(token: string): number { + const token_info = active_session_tokens.get(token) + + if (!token_info) { + return NaN + } + + return token_info.expiry_time.getTime() - Date.now() } function reload_user_data(user: User) { @@ -116,33 +159,35 @@ async function __clean_session_store() { let cleaned_user_sessions = 0 let cleaned_active_users = 0 - const active_users_session = new Set(); + const active_users_session = new Set() active_session_tokens.forEach((token_info, token, token_map) => { if (token_info.expiry_time.getTime() < Date.now()) { - token_map.delete(token); + token_map.delete(token) cleaned_user_sessions += 1 } else { - active_users_session.add(token_info.user_id); + active_users_session.add(token_info.user_id) } - }); + }) active_users.forEach((user_info, user_id, user_map) => { if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) { user_map.delete(user_id) cleaned_active_users += 1 } - }); + }) if (cleaned_active_users > 0 || cleaned_active_users > 0) { Logs.user.info(`Cleaned ${cleaned_user_sessions} inactive session tokens and ${cleaned_active_users} inactive users`) } } -export default class SessionStore { - static issue_access_token_for_user = issue_access_token_for_user; - static get_user_by_access_token = get_user_by_access_token; - static logout_user_session = logout_user_session; - static reload_user_data = reload_user_data; +export default { + issue_access_token_for_user, + reissue_access_token, + get_user_by_access_token, + get_remaining_lifetime, + logout_user_session, + reload_user_data } -setInterval(__clean_session_store, 15*60*1000); +setInterval(__clean_session_store, 15*60*1000)