import {getServerSession} from "next-auth"; import {authOptions} from "@/lib/api/auth/authOptions"; interface JwtPayload { exp: number; iat?: number; sub?: string; [key: string]: unknown; // for any extra claims } // Function to decode JWT payload without verification (for local expiration check) function decodeJwtPayload(token: string): JwtPayload | null { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( atob(base64) .split('') .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('') ); return JSON.parse(jsonPayload); } catch (error) { console.error("[auth] Failed to decode JWT payload:", error); return null; } } // Function to check if token is expired or about to expire (within 5 minutes) function isTokenExpiredOrExpiring(token: string): boolean { const payload = decodeJwtPayload(token); if (!payload || !payload.exp) { return true; // Consider invalid tokens as expired } const currentTime = Math.floor(Date.now() / 1000); const expirationTime = payload.exp; const bufferTime = 5 * 60; // 5 minutes buffer return (expirationTime - currentTime) <= bufferTime; } // Function to refresh token using refresh token async function refreshAccessToken(refreshToken: string): Promise { try { const { KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_ISSUER, } = process.env; if (!KEYCLOAK_CLIENT_ID || !KEYCLOAK_CLIENT_SECRET || !KEYCLOAK_ISSUER) { console.error("[auth] Missing Keycloak configuration for token refresh"); return null; } const tokenEndpoint = `${KEYCLOAK_ISSUER}/protocol/openid-connect/token`; const response = await fetch(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: KEYCLOAK_CLIENT_ID, client_secret: KEYCLOAK_CLIENT_SECRET, refresh_token: refreshToken, }), }); if (!response.ok) { console.error("[auth] Failed to refresh token:", response.status, response.statusText); return null; } const data = await response.json(); console.log("[auth] Token refreshed successfully"); return data.access_token; } catch (error) { console.error("[auth] Error refreshing token:", error); return null; } } export async function serverCall( path: string, method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", body?: unknown ): Promise { const url = `${process.env.INTERNAL_BACKEND_URL ?? "http://localhost:8080"}${path}`; const session = await getServerSession(authOptions); const headers: Record = { "Content-Type": "application/json", }; if (session?.accessToken) { // Check if token is expired or about to expire if (isTokenExpiredOrExpiring(session.accessToken)) { console.log("[auth] Access token is expired or about to expire, attempting refresh"); if (session.refreshToken) { const newAccessToken = await refreshAccessToken(session.refreshToken); if (newAccessToken) { // Update the session with new token (note: this won't persist across requests) session.accessToken = newAccessToken; console.log("[auth] Using refreshed access token"); } else { console.warn("[auth] Failed to refresh token, proceeding with expired token"); } } else { console.warn("[auth] No refresh token available, proceeding with expired token"); } } headers["Authorization"] = `Bearer ${session.accessToken}`; } console.log("[api] Calling backend API: ", method, url, body); return fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }); }