diff --git a/app/api/auth/[...nextauth]/options.js b/app/api/auth/[...nextauth]/options.js index 1352d41..6b1add5 100644 --- a/app/api/auth/[...nextauth]/options.js +++ b/app/api/auth/[...nextauth]/options.js @@ -1,3 +1,4 @@ +/* eslint-disable new-cap */ import CredentialsProvider from 'next-auth/providers/credentials' import {readMe, withToken} from '@directus/sdk' import {directusClient} from '@/lib/directus.js' @@ -5,9 +6,39 @@ import {directusClient} from '@/lib/directus.js' const apiUrl = process.env.DIRECTUS_API_URL const nextauthSecret = process.env.NEXTAUTH_SECRET +// On rafraîchit 60s avant l'échéance réelle pour éviter les races +const REFRESH_MARGIN_MS = 60 * 1000 + +/** + * Appelle l'endpoint Directus /auth/refresh et retourne les nouveaux tokens. + * Lève une erreur si le refresh échoue (refresh_token expiré ou révoqué). + * + * @param {string} refreshToken + * @returns {Promise<{accessToken: string, refreshToken: string, accessTokenExpires: number}>} + */ +async function refreshDirectusToken(refreshToken) { + const res = await fetch(`${apiUrl}/auth/refresh`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({refresh_token: refreshToken, mode: 'json'}), + }) + + if (!res.ok) { + throw new Error(`Directus refresh failed with status ${res.status}`) + } + + const {data} = await res.json() + + return { + accessToken: data.access_token, + refreshToken: data.refresh_token, + accessTokenExpires: Date.now() + (data.expires ?? 900_000) - REFRESH_MARGIN_MS, + } +} + export const options = { providers: [ - CredentialsProvider({ // eslint-disable-line new-cap + CredentialsProvider({ name: 'Credentials', credentials: { email: {}, @@ -36,18 +67,13 @@ export const options = { signIn: '/login' }, callbacks: { - async jwt({ - token, - user, - account - }) { + async jwt({token, user, account}) { + // Connexion initiale : enrichissement du token avec les données Directus if (account && user) { const userData = await directusClient.request( withToken( user.data.access_token, - readMe({ - fields: ['*'] - }) + readMe({fields: ['*']}) ) ) @@ -55,11 +81,24 @@ export const options = { ...token, accessToken: user.data.access_token, refreshToken: user.data.refresh_token, - user: userData + accessTokenExpires: Date.now() + (user.data.expires ?? 900_000) - REFRESH_MARGIN_MS, + user: userData, } } - return token + // Token encore valide : on le retourne sans modification + if (Date.now() < token.accessTokenExpires) { + return token + } + + // Token expiré : tentative de rafraîchissement silencieux + try { + const refreshed = await refreshDirectusToken(token.refreshToken) + return {...token, ...refreshed} + } catch (refreshError) { + console.error('Refresh token Directus échoué :', refreshError.message) + return {...token, error: 'RefreshAccessTokenError'} + } }, async session({session, token}) { session.user.userId = token.user.id @@ -69,6 +108,7 @@ export const options = { session.user.token = token.user.token session.user.email = token.user.email session.user.password = token.user.password + session.error = token.error return session } } diff --git a/tasks/todo.md b/tasks/todo.md index bbdd5d6..d1545c8 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -12,8 +12,8 @@ - [x] **Headers CSP** — `next.config.mjs` (renommé depuis .js) avec CSP + 4 headers sécurité - [x] **Tests unitaires** — Vitest sur `lib/format.js`, `lib/version-utils.js`, `lib/rate-limit.js` -- [ ] **Tests extensions Directus** — mocks VersionsService -- [ ] **Refresh token explicite** — callback `jwt` dans NextAuth options +- [x] **Tests extensions Directus** — mocks VersionsService +- [x] **Refresh token explicite** — callback `jwt` dans NextAuth options - [ ] **Pipeline CI** — GitHub Actions (lint + test + build) - [ ] **Sentry** — tracking erreurs frontend + API routes