From d758f5317611b7fe585cf2c784e37e8d4217528c Mon Sep 17 00:00:00 2001 From: Pascal Martineau Date: Thu, 26 Mar 2026 15:46:53 -0400 Subject: [PATCH] feat: Authentication token refresh logic --- .../themes/headless/server/graphql/context.ts | 4 +- .../headless/server/graphql/wp-hooks.ts | 6 +-- .../server/{api/login.gql => utils/auth.gql} | 6 +++ .../themes/headless/server/utils/auth.ts | 48 ++++++++++++++++++- 4 files changed, 58 insertions(+), 6 deletions(-) rename wp-content/themes/headless/server/{api/login.gql => utils/auth.gql} (75%) diff --git a/wp-content/themes/headless/server/graphql/context.ts b/wp-content/themes/headless/server/graphql/context.ts index fcee337..d022941 100644 --- a/wp-content/themes/headless/server/graphql/context.ts +++ b/wp-content/themes/headless/server/graphql/context.ts @@ -1,4 +1,4 @@ export default defineGraphQLContext(async (event) => { - const wpAuthToken = await getAuthToken(event); - return { wpAuthToken }; + const authToken = await getAuthToken(event); + return { authToken }; }); diff --git a/wp-content/themes/headless/server/graphql/wp-hooks.ts b/wp-content/themes/headless/server/graphql/wp-hooks.ts index 66358c7..9def651 100644 --- a/wp-content/themes/headless/server/graphql/wp-hooks.ts +++ b/wp-content/themes/headless/server/graphql/wp-hooks.ts @@ -2,10 +2,10 @@ import { defu } from "defu"; export default defineRemoteExecutorHooks({ onRequest(request, context) { - // Attach the Authorization header if a wpAuthToken is present in the context - if (context?.wpAuthToken) { + // Attach the Authorization header if an authToken is present in the context + if (context?.authToken) { request.extensions = defu(request.extensions, { - headers: { Authorization: `Bearer ${context.wpAuthToken}` }, + headers: { Authorization: `Bearer ${context.authToken}` }, }); } }, diff --git a/wp-content/themes/headless/server/api/login.gql b/wp-content/themes/headless/server/utils/auth.gql similarity index 75% rename from wp-content/themes/headless/server/api/login.gql rename to wp-content/themes/headless/server/utils/auth.gql index 54ebe76..cb84ff9 100644 --- a/wp-content/themes/headless/server/api/login.gql +++ b/wp-content/themes/headless/server/utils/auth.gql @@ -21,3 +21,9 @@ mutation AuthLogin($username: String!, $password: String!) { ...AuthPayload } } + +mutation AuthRefreshToken($refreshToken: String!) { + refreshToken(input: { refreshToken: $refreshToken }) { + authToken + } +} diff --git a/wp-content/themes/headless/server/utils/auth.ts b/wp-content/themes/headless/server/utils/auth.ts index f2fc56e..3275ce2 100644 --- a/wp-content/themes/headless/server/utils/auth.ts +++ b/wp-content/themes/headless/server/utils/auth.ts @@ -67,8 +67,54 @@ export async function getAuthToken(event: H3Event) { const decoded = jwtDecode<{ exp: number }>(session.secure.authToken); const isExpired = decoded.exp * 1000 < Date.now(); if (isExpired) { - // TOOD: Refresh token logic + try { + const newAuthToken = await refreshAuthToken(session.secure.refreshToken); + if (!newAuthToken) { + throw new Error("Impossible de rafraƮchir le jeton d'authentification."); + } + session.secure.authToken = newAuthToken; + await setUserSession(event, session); + } catch { + await clearUserSession(event); + return; + } } return session.secure.authToken; } + +// Track in-flight refreshAuthToken calls to prevent duplicate requests +const refreshTokenPromises = new Map>(); + +// Refresh auth token by calling remote GraphQL endpoint directly +export async function refreshAuthToken(refreshToken: string): Promise { + // Return existing in-flight promise if available + const inFlight = refreshTokenPromises.get(refreshToken); + if (inFlight) { + return inFlight; + } + + const refreshPromise = (async () => { + const { wpUrl } = useRuntimeConfig(); + const endpoint = `${wpUrl}/graphql`; + + const { data } = await executeHttpOperation( + { + operationName: "AuthRefreshToken", + variables: { refreshToken }, + }, + { endpoint, headers: { Authorization: null } }, + ); + + return data?.refreshToken?.authToken || undefined; + })(); + + refreshTokenPromises.set(refreshToken, refreshPromise); + + return refreshPromise.finally(() => { + const current = refreshTokenPromises.get(refreshToken); + if (current === refreshPromise) { + refreshTokenPromises.delete(refreshToken); + } + }); +}