From 326db3863fdae7a7d5deebe70c0f22a97fb6be6e Mon Sep 17 00:00:00 2001 From: Hendrik Belitz Date: Mon, 8 Sep 2025 11:31:36 +0200 Subject: [PATCH] Use old session id from old token for refresh. Refresh offset set to ten minutes. --- src/lib/server/auth/jwt-utils.ts | 24 +++++++++++++++++++++++- src/lib/server/auth/session-service.ts | 6 +++--- src/routes/api/auth/refresh/+server.ts | 10 +++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/lib/server/auth/jwt-utils.ts b/src/lib/server/auth/jwt-utils.ts index 156984c..dd34742 100644 --- a/src/lib/server/auth/jwt-utils.ts +++ b/src/lib/server/auth/jwt-utils.ts @@ -1,4 +1,4 @@ -import { SignJWT, jwtVerify, type JWTPayload } from "jose"; +import { SignJWT, decodeJwt, jwtVerify, type JWTPayload } from "jose"; import { env } from "$env/dynamic/private"; import type { SelectUser } from "$lib/server/db/central-schema"; import { UniversalLogger } from "$lib/logger"; @@ -57,6 +57,28 @@ export async function generateRefreshToken(userId: string, sessionId: string): P return jwt; } +export async function decodeAccessToken( + token: string, +): Promise<(JWTPayload & { userId: string; sessionId: string }) | null> { + try { + const payload = await decodeJwt(token); + + return { + userId: payload.userId as string, + email: payload.email, + name: payload.name, + role: payload.role as "GLOBAL_ADMIN" | "TENANT_ADMIN" | "STAFF", + tenantId: payload.tenantId as string | undefined, + sessionId: payload.sessionId as string, + iat: payload.iat, + exp: payload.exp, + }; + } catch (error) { + logger.warn("JWT verification failed:", { error: String(error) }); + return null; + } +} + export async function verifyAccessToken( token: string, ): Promise<(JWTPayload & { userId: string; sessionId: string }) | null> { diff --git a/src/lib/server/auth/session-service.ts b/src/lib/server/auth/session-service.ts index 36576d3..4a0c02b 100644 --- a/src/lib/server/auth/session-service.ts +++ b/src/lib/server/auth/session-service.ts @@ -285,13 +285,13 @@ export class SessionService { logger.info("Expired sessions cleaned up"); } - static async getUserSession(accessToken: string): Promise { - logger.debug(`Getting active sessions for a given access token`); + static async getUserSession(sessionId: string): Promise { + logger.debug(`Getting active sessions for session id: ${sessionId}`); const sessions = await db .select() .from(userSession) - .where(eq(userSession.accessToken, accessToken)) + .where(eq(userSession.id, sessionId)) .orderBy(userSession.lastUsedAt); return sessions[0] ?? null; diff --git a/src/routes/api/auth/refresh/+server.ts b/src/routes/api/auth/refresh/+server.ts index 02f52f5..1dddc88 100644 --- a/src/routes/api/auth/refresh/+server.ts +++ b/src/routes/api/auth/refresh/+server.ts @@ -3,9 +3,11 @@ import { SessionService } from "$lib/server/auth/session-service"; import type { RequestHandler } from "./$types"; import { registerOpenAPIRoute } from "$lib/server/openapi"; import { UniversalLogger } from "$lib/logger"; -import { isBefore } from "date-fns"; +import { isBefore, subMinutes } from "date-fns"; +import { decodeAccessToken } from "$lib/server/auth/jwt-utils"; const logger = new UniversalLogger().setContext("AuthRefreshAPI"); +const REFRESH_OFFSET = 10; registerOpenAPIRoute("/auth/refresh", "POST", { summary: "Refresh access token", @@ -68,13 +70,15 @@ export const POST: RequestHandler = async ({ cookies }) => { } // Get the (possibly invalid) session from the database - const oldSession = await SessionService.getUserSession(accessToken); + const decodedToken = await decodeAccessToken(accessToken); + const oldSessionId = decodedToken?.sessionId ?? null; + const oldSession = oldSessionId ? await SessionService.getUserSession(oldSessionId) : null; if (!oldSession) { return json({ error: "No valid session found for user" }, { status: 401 }); } - if (isBefore(oldSession.expiresAt, new Date())) { + if (isBefore(oldSession.expiresAt, subMinutes(new Date(), REFRESH_OFFSET))) { return json( { message: "Session still valid, no refresh needed",