From 181d7b9dec8fb9bd34e764f1a54a94c97247e28c Mon Sep 17 00:00:00 2001 From: Robby Uitbeijerse Date: Tue, 14 Oct 2025 17:02:53 +0200 Subject: [PATCH 1/2] feat: server side token validation/refreshing --- src/lib/data/auth.ts | 90 +++++++++++++++++++++++++++++++++++++++++ src/lib/data/cookies.ts | 67 +++++++++++++++++++++++++----- 2 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 src/lib/data/auth.ts diff --git a/src/lib/data/auth.ts b/src/lib/data/auth.ts new file mode 100644 index 000000000..84896c663 --- /dev/null +++ b/src/lib/data/auth.ts @@ -0,0 +1,90 @@ +'use server'; + +import jsonwebtoken from 'jsonwebtoken'; +import { sdk } from "@lib/config" +import { setAuthToken } from './cookies'; + +interface DecodedJWT { + exp?: number; + sub?: string; + [key: string]: unknown; +} + +/** + * Check if a JWT token is expired or will expire soon + * @param token - The JWT token to validate + * @param bufferSeconds - Number of seconds before expiry to consider token as expired (default: 60) + * @returns true if token is expired or will expire within buffer time + */ +export async function isTokenExpired(token: string, bufferSeconds = 60): Promise { + try { + const decoded = jsonwebtoken.decode(token) as DecodedJWT | null; + + if (!decoded || !decoded.exp) { + console.info('No expiration claim means we should treat as expired'); + return true; // No expiration claim means we should treat as expired + } + + const currentTime = Math.floor(Date.now() / 1000); + const expirationTime = decoded.exp - bufferSeconds; + + return currentTime >= expirationTime; + } catch { + return true; // If we can't decode, treat as expired + } +} + +/** + * Refresh the Medusa authentication token using the SDK + * This function attempts to refresh the token by calling the Medusa auth refresh endpoint + * @param currentToken - The current JWT token + * @returns The new token if successful, null otherwise + */ +export async function refreshMedusaToken(currentToken: string): Promise { + try { + // Attempt to refresh the token using the Medusa SDK + // The SDK should handle the refresh token logic internally + const result = await sdk.client.fetch<{ token?: string }>('/auth/token/refresh', { + method: 'POST', + headers: { + Authorization: `Bearer ${currentToken}`, + }, + }); + + if (result.token) { + // Update the token in the cookie + await setAuthToken(result.token); + // Update the SDK's internal token + await sdk.client.setToken(result.token); + return result.token; + } + + return null; + } catch (error) { + console.error('Token refresh failed:', error); + return null; + } +} + +/** + * Validate and refresh token if needed + * @param token - The current JWT token + * @returns Valid token (either the original or refreshed), null if refresh fails + */ +export async function validateAndRefreshToken(token: string): Promise { + // Check if token is expired or will expire soon + const isExpired = await isTokenExpired(token); + if (!isExpired) { + return token; // Token is still valid + } + + // Attempt to refresh the token + const newToken = await refreshMedusaToken(token); + + if (!newToken) { + console.warn('Failed to refresh expired token, user may need to re-authenticate'); + return null; + } + + return newToken; +} diff --git a/src/lib/data/cookies.ts b/src/lib/data/cookies.ts index 1e21f786a..47d5b6b6d 100644 --- a/src/lib/data/cookies.ts +++ b/src/lib/data/cookies.ts @@ -1,22 +1,69 @@ import "server-only" -import { cookies as nextCookies } from "next/headers" -export const getAuthHeaders = async (): Promise< - { authorization: string } | {} -> => { +import { headers, cookies as nextCookies } from 'next/headers'; +import { redirect } from 'next/navigation'; +import { validateAndRefreshToken } from './auth'; + + +/** + * Get auth headers from cookie for server-side requests + * Automatically validates and refreshes expired tokens + */ +export const getAuthHeaders = async (): Promise<{authorization: string; } | undefined> => { try { - const cookies = await nextCookies() - const token = cookies.get("_medusa_jwt")?.value + const cookies = await nextCookies(); + const token = cookies.get('_medusa_jwt')?.value; + // If there's no valid token available, we want to destroy the supabase cookies and force reauthentication if (!token) { - return {} + const redirectUrl = await getLoginRedirectUrl(); + redirect(redirectUrl); + } + + // Validate and refresh token if needed + const validToken = await validateAndRefreshToken(token); + + // If token validation/refreshing failed, clear + if (!validToken) { + await removeAuthToken(); + const redirectUrl = await getLoginRedirectUrl(); + redirect(redirectUrl); } - return { authorization: `Bearer ${token}` } + return { authorization: `Bearer ${validToken}` }; } catch { - return {} + await removeAuthToken(); + const redirectUrl = await getLoginRedirectUrl(); + redirect(redirectUrl); } -} +}; + +/** + * Helper to build login redirect URL with current page as ext parameter + */ +const getLoginRedirectUrl = async (): Promise => { + try { + const headersList = await headers(); + // Try to get the current page from various headers + const referer = headersList.get('referer'); + const pathname = headersList.get('x-pathname') || headersList.get('x-invoke-path'); + + let currentPage = '/'; + + if (referer) { + // Extract pathname and search params from referer URL + const url = new URL(referer); + currentPage = url.pathname + url.search; + } else if (pathname) { + const searchParams = headersList.get('x-search-params') || ''; + currentPage = searchParams ? `${pathname}?${searchParams}` : pathname; + } + + return `/auth/login?ext=${encodeURIComponent(currentPage)}`; + } catch { + return '/auth/login'; + } +}; export const getCacheTag = async (tag: string): Promise => { try { From ac0ed37a6b77c35660342770ae6349909e8210eb Mon Sep 17 00:00:00 2001 From: Robby Uitbeijerse Date: Tue, 14 Oct 2025 17:05:19 +0200 Subject: [PATCH 2/2] chore: adjust route, fix next param --- src/lib/data/cookies.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/data/cookies.ts b/src/lib/data/cookies.ts index 47d5b6b6d..c085cb7ad 100644 --- a/src/lib/data/cookies.ts +++ b/src/lib/data/cookies.ts @@ -59,9 +59,9 @@ const getLoginRedirectUrl = async (): Promise => { currentPage = searchParams ? `${pathname}?${searchParams}` : pathname; } - return `/auth/login?ext=${encodeURIComponent(currentPage)}`; + return `/login?next=${encodeURIComponent(currentPage)}`; } catch { - return '/auth/login'; + return '/login'; } };