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 7233b50..4a0c02b 100644 --- a/src/lib/server/auth/session-service.ts +++ b/src/lib/server/auth/session-service.ts @@ -285,6 +285,18 @@ export class SessionService { logger.info("Expired sessions cleaned up"); } + 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.id, sessionId)) + .orderBy(userSession.lastUsedAt); + + return sessions[0] ?? null; + } + static async getActiveSessions(userId: string): Promise { logger.debug(`Getting active sessions for user: ${userId}`); diff --git a/src/routes/api/auth/refresh/+server.ts b/src/routes/api/auth/refresh/+server.ts index 2276fa8..1dddc88 100644 --- a/src/routes/api/auth/refresh/+server.ts +++ b/src/routes/api/auth/refresh/+server.ts @@ -3,8 +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, 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", @@ -57,7 +60,7 @@ registerOpenAPIRoute("/auth/refresh", "POST", { }, }); -export const POST: RequestHandler = async ({ locals, cookies }) => { +export const POST: RequestHandler = async ({ cookies }) => { try { // Get current access token from cookie to extract session info const accessToken = cookies.get("access_token"); @@ -66,17 +69,26 @@ export const POST: RequestHandler = async ({ locals, cookies }) => { return json({ error: "Access token is required" }, { status: 400 }); } - // The user object is already available from authHandle, - // so we can create a new session based on that - if (!locals.user) { - return json({ error: "Invalid user context" }, { status: 400 }); + // Get the (possibly invalid) session from the database + 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 }); } - const result = await SessionService.createSession( - locals.user.userId as string, - "", // IP address - could be extracted from request if needed - undefined, // user agent - ); + if (isBefore(oldSession.expiresAt, subMinutes(new Date(), REFRESH_OFFSET))) { + return json( + { + message: "Session still valid, no refresh needed", + expiresAt: oldSession.expiresAt.toISOString(), + }, + { status: 200 }, + ); + } + + const result = await SessionService.refreshSession(oldSession.refreshToken); if (!result) { return json({ error: "Invalid or expired refresh token" }, { status: 401 }); diff --git a/src/server-hooks/apiAuthHandle.ts b/src/server-hooks/apiAuthHandle.ts index b00eed1..5bc34ab 100644 --- a/src/server-hooks/apiAuthHandle.ts +++ b/src/server-hooks/apiAuthHandle.ts @@ -21,13 +21,13 @@ const PUBLIC_PATHS = [ "/api/log", "/api/admin/init", "/api/admin/exists", + "/api/auth/refresh", // Must be public since the access token might already be invalid when this gets called ]; const GLOBAL_ADMIN_PATHS = ["/api/admin", "/api/tenants"]; const ADMIN_PATHS = ["/api/tenant-admin"]; const PROTECTED_AUTH_PATHS = [ "/api/auth/logout", - "/api/auth/refresh", "/api/auth/session", "/api/auth/sessions", "/api/auth/passkeys",