Added token refresh for session tokens

This commit is contained in:
Patrick 2025-09-11 16:01:10 +02:00
parent 1476289722
commit 8f76173bc5
2 changed files with 104 additions and 43 deletions

View File

@ -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; event.locals.user = user;
return await resolve(event); return await resolve(event);

View File

@ -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" import Logs from "$lib/server/log"
interface UserSession { interface UserSession {
user: User; user: User
last_active: Date; last_active: Date
} }
export interface TokenInfo { export interface TokenInfo {
user_id: number; user_id: number
creation_time: Date; creation_time: Date
expiry_time: Date; expiry_time: Date
} }
class LoginError extends Error { class LoginError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message)
} }
} }
class UserLoginError extends LoginError { class UserLoginError extends LoginError {
user_id: number; user_id: number
constructor(user_id: number, message: string) { constructor(user_id: number, message: string) {
super(message); super(message)
this.user_id = user_id; this.user_id = user_id
} }
} }
const TOKEN_LENGTH = 256; const TOKEN_LENGTH = 256
const active_users = new Map<number, UserSession>(); const active_users = new Map<number, UserSession>()
const active_session_tokens = new Map<string, TokenInfo>(); const active_session_tokens = new Map<string, TokenInfo>()
function issue_access_token_for_user(user: User, expiry_date: Date): string { 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()}`) 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) { if (user_session === undefined) {
user_session = { user_session = {
user: user, user: user,
last_active: new Date() 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 | */ /* | 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( const session_token = Buffer.concat(
[ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3), [ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3),
Buffer.from(token_info.creation_time.getTime().toFixed(0)) ] Buffer.from(token_info.creation_time.getTime().toFixed(0)) ]
).toString("base64"); ).toString("base64")
if (active_session_tokens.has(session_token)) { 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 { 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()) { 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) { 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 { 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) { if (!token_info) {
Logs.user.warn(`Failed to logout user by token, because token does not exist`); Logs.user.warn(`Failed to logout user by token, because token does not exist`)
return false; return false
} }
Logs.user.info(`Logging out user ${token_info?.user_id}`) 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) { function reload_user_data(user: User) {
@ -116,33 +159,35 @@ async function __clean_session_store() {
let cleaned_user_sessions = 0 let cleaned_user_sessions = 0
let cleaned_active_users = 0 let cleaned_active_users = 0
const active_users_session = new Set<number>(); const active_users_session = new Set<number>()
active_session_tokens.forEach((token_info, token, token_map) => { active_session_tokens.forEach((token_info, token, token_map) => {
if (token_info.expiry_time.getTime() < Date.now()) { if (token_info.expiry_time.getTime() < Date.now()) {
token_map.delete(token); token_map.delete(token)
cleaned_user_sessions += 1 cleaned_user_sessions += 1
} else { } 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) => { 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)) { if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) {
user_map.delete(user_id) user_map.delete(user_id)
cleaned_active_users += 1 cleaned_active_users += 1
} }
}); })
if (cleaned_active_users > 0 || cleaned_active_users > 0) { 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`) Logs.user.info(`Cleaned ${cleaned_user_sessions} inactive session tokens and ${cleaned_active_users} inactive users`)
} }
} }
export default class SessionStore { export default {
static issue_access_token_for_user = issue_access_token_for_user; issue_access_token_for_user,
static get_user_by_access_token = get_user_by_access_token; reissue_access_token,
static logout_user_session = logout_user_session; get_user_by_access_token,
static reload_user_data = reload_user_data; get_remaining_lifetime,
logout_user_session,
reload_user_data
} }
setInterval(__clean_session_store, 15*60*1000); setInterval(__clean_session_store, 15*60*1000)