From b62ee3e9add798913aa85fe1963d294ef7e989fa Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Fri, 11 Jul 2025 20:30:12 +0200 Subject: [PATCH] Add token expiration check and refresh mechanism in `serverCall` - Decode and validate JWT payload to detect expired or near-expiring tokens. - Implement `refreshAccessToken` using Keycloak endpoints for seamless token refresh. - Modify `serverCall` to refresh and update token dynamically before API requests. - Improve error logging for token decoding and refresh operations. --- internal_frontend/lib/api/serverCall.ts | 106 +++++++++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/internal_frontend/lib/api/serverCall.ts b/internal_frontend/lib/api/serverCall.ts index 2d329ed..d810b55 100644 --- a/internal_frontend/lib/api/serverCall.ts +++ b/internal_frontend/lib/api/serverCall.ts @@ -1,7 +1,89 @@ -// lib/callBackendApi.ts 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", @@ -14,7 +96,25 @@ export async function serverCall( "Content-Type": "application/json", }; - if (session != null) { + 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}`; } @@ -25,4 +125,4 @@ export async function serverCall( headers, body: body ? JSON.stringify(body) : undefined, }); -} \ No newline at end of file +}