From 16b5aba457a76dd0cfdae9f02d92364d4bbb8ee6 Mon Sep 17 00:00:00 2001 From: Patrick Date: Sat, 4 Oct 2025 13:05:03 +0200 Subject: [PATCH] implemented automated user session login and session refresh --- src/hooks.server.ts | 77 +++++++++++++++++++++++++++++++ src/lib/errors.ts | 11 +++++ src/lib/server/config.ts | 2 + src/lib/server/usermgmt.ts | 35 ++++++++++++-- src/routes/api/refresh/+server.ts | 27 +++++++++++ 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/hooks.server.ts create mode 100644 src/routes/api/refresh/+server.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..cc5fc0a --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,77 @@ +import type { Handle } from "@sveltejs/kit" + +import { error, redirect, json, fail } from "@sveltejs/kit" + +import { Error401Cause } from "$lib/errors" + +import Config from "$lib/server/config" +import UserMgmt from "$lib/server/usermgmt" + +function action_fail(status: number, data: T) { + return json(fail(status, data)) +} + +export const handle: Handle = async ({ event, resolve }) => { + + const token = event.cookies.get("session") + let session = await UserMgmt.session_login(token ?? "") + + if (session && event.route.id !== "/api/refresh") { + if (session.expires.getTime() < Date.now() + Config.session_refresh_grace) { + + const response = await event.fetch("/api/refresh", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + token: token + }) + }) + + if (response.ok) { + const new_session = await response.json() + + if (new_session && new_session.token !== null && new_session.expires !== null) { + event.cookies.set("session", new_session.token, { + expires: new Date(new_session.expires), + httpOnly: true, + secure: true, + sameSite: "strict", + path: "/" + }) + // For mobile + /*event.setHeaders({ + "X-Authorization-Refresh": new_session.token + })*/ + + session = await UserMgmt.session_login(new_session.token) + } + } + } + } + + event.locals.user = session?.user + + if (!event.route.id) { + return error(404, { message: "Diese Seite existiert nicht" }) + } + + if (event.route.id.startsWith("/api")) { + if (event.route.id.startsWith("/api/users") && !event.locals.user) { + return error(401, { cause: Error401Cause.NotLoggedIn, message: "Please log in" }) + } + } else { + if (!event.locals.user && !event.route.id.startsWith("/login")) { + if (event.request.method === "POST") { + if (event.request.headers.get("x-sveltekit-action")) { + return action_fail(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." }) + } + return error(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." }) + } + return redirect(307, "/login") + } + } + + return await resolve(event) +} diff --git a/src/lib/errors.ts b/src/lib/errors.ts index fe71e74..c5b3624 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -14,6 +14,10 @@ export class DuplicateError extends Error { } } +export const enum Error401Cause { + NotLoggedIn +} + export const enum RegisterResponseCause { Server = 1, MalformedRequest, @@ -32,3 +36,10 @@ export const enum LoginResponseCause { NotFound, Timeout } + +export const enum RefreshResponseCause { + Server = 1, + MalformedRequest, + InvalidToken, + InvalidSession +} diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts index 0cf11f9..068d35d 100644 --- a/src/lib/server/config.ts +++ b/src/lib/server/config.ts @@ -38,6 +38,7 @@ class Config { readonly is_production: boolean = process.env.NODE_ENV == "production" private _session_timeout: number = 15 * 60 * 1000 + private _session_refresh_grace: number = 5 * 60 * 1000 // time until expiration get log_dir(): string { return this._log_dir @@ -47,6 +48,7 @@ class Config { } get session_timeout(): number { return this._session_timeout } + get session_refresh_grace(): number { return this._session_refresh_grace } constructor() { this._log_dir = resolve_env_to_path(process.env.APP_LOG_DIR, "./data/logs") diff --git a/src/lib/server/usermgmt.ts b/src/lib/server/usermgmt.ts index 44a832e..d8c495e 100644 --- a/src/lib/server/usermgmt.ts +++ b/src/lib/server/usermgmt.ts @@ -52,6 +52,10 @@ class Cache { } } + invalidate_session(session: SessionData) { + this._session_map.delete(session.token) + } + get_session(token: string): SessionData|null { const session_info = this._session_map.get(token) if (!session_info) return null @@ -118,14 +122,39 @@ class UserMgmt { return null } - const token = Crypto.randomBytes(32).toBase64() - const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout)) + const session_info = this._generate_session_for_user(user) return session_info } async session_login(token: string): Promise { - return this._cache.get_session(token) + const session = this._cache.get_session(token) + if (!session) { + return null + } + if (session?.expires.getTime() < Date.now()) { + return null + } + return session + } + + async refresh_session(token: string): Promise { + const session = await this.session_login(token) + if (!session) { + return null + } + + const new_session = this._generate_session_for_user(session.user) + this._cache.invalidate_session(session) + + return new_session + } + + private _generate_session_for_user(user: User): SessionData { + const token = Crypto.randomBytes(32).toBase64() + const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout)) + + return session_info } } diff --git a/src/routes/api/refresh/+server.ts b/src/routes/api/refresh/+server.ts new file mode 100644 index 0000000..8c2d1e1 --- /dev/null +++ b/src/routes/api/refresh/+server.ts @@ -0,0 +1,27 @@ +import type { RequestHandler } from "./$types" + +import { error, json } from "@sveltejs/kit" + +import UserMgmt from "$lib/server/usermgmt" +import { RefreshResponseCause } from "$lib/errors" + +export const POST: RequestHandler = async ({ request }) => { + + const data = await request.json() + + const token = data["token"] + if (!token || typeof token !== "string") { + return error(400, { cause: RefreshResponseCause.MalformedRequest, message: "token must be provided as string." }) + } + + const new_session = await UserMgmt.refresh_session(token) + + if (!new_session) { + return error(401, { cause: RefreshResponseCause.InvalidSession, message: "No session for token" }) + } + + return json({ + token: new_session.token, + expires: new_session.expires + }) +}