From 7e15100b57255904c2247036753598ee602a8be1 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 18 Apr 2025 14:13:54 +0200 Subject: [PATCH] session correction logout 3 rest 3 --- app/api/auth/[...nextauth]/route.ts | 237 ++++++++++++++-------------- next.config.mjs | 13 ++ 2 files changed, 128 insertions(+), 122 deletions(-) diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 62ec21cb..8b9a19df 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,12 +1,42 @@ import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; -import { prisma } from '@/lib/prisma'; -import { ExtendedJWT, ExtendedSession, ServiceToken, invalidateServiceTokens } from '@/lib/session'; -import { Session } from "next-auth"; + +interface KeycloakProfile { + sub: string; + email?: string; + name?: string; + roles?: string[]; + preferred_username?: string; + given_name?: string; + family_name?: string; +} declare module "next-auth" { - interface Session extends ExtendedSession {} - interface JWT extends ExtendedJWT {} + interface Session { + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + username: string; + first_name: string; + last_name: string; + role: string[]; + }; + accessToken: string; + } + + interface JWT { + sub?: string; + accessToken: string; + refreshToken: string; + accessTokenExpires: number; + role: string[]; + username: string; + first_name: string; + last_name: string; + error?: string; + } } function getRequiredEnvVar(name: string): string { @@ -17,12 +47,46 @@ function getRequiredEnvVar(name: string): string { return value; } +async function refreshAccessToken(token: JWT) { + try { + const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: process.env.KEYCLOAK_CLIENT_ID!, + client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, + grant_type: "refresh_token", + refresh_token: token.refreshToken, + }), + method: "POST", + }); + + const refreshedTokens = await response.json(); + + if (!response.ok) { + throw refreshedTokens; + } + + return { + ...token, + accessToken: refreshedTokens.access_token, + refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, + accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, + }; + } catch (error) { + console.error("Error refreshing access token:", error); + return { + ...token, + error: "RefreshAccessTokenError", + }; + } +} + export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ - clientId: process.env.KEYCLOAK_CLIENT_ID!, - clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, - issuer: process.env.KEYCLOAK_ISSUER!, + clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), + clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), + issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), profile(profile) { return { id: profile.sub, @@ -38,130 +102,46 @@ export const authOptions: NextAuthOptions = { ], session: { strategy: "jwt", - maxAge: 24 * 60 * 60, // 1 day - }, - cookies: { - sessionToken: { - name: process.env.NODE_ENV === 'production' - ? `__Secure-next-auth.session-token` - : `next-auth.session-token`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NODE_ENV === 'production', - maxAge: 24 * 60 * 60 // 1 day - } - }, - callbackUrl: { - name: process.env.NODE_ENV === 'production' - ? `__Secure-next-auth.callback-url` - : `next-auth.callback-url`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NODE_ENV === 'production', - maxAge: 24 * 60 * 60 // 1 day - } - }, - csrfToken: { - name: process.env.NODE_ENV === 'production' - ? `__Host-next-auth.csrf-token` - : `next-auth.csrf-token`, - options: { - httpOnly: true, - sameSite: 'lax', - path: '/', - secure: process.env.NODE_ENV === 'production', - maxAge: 24 * 60 * 60 // 1 day - } - } + maxAge: 30 * 24 * 60 * 60, // 30 days }, callbacks: { - async signIn({ user, account, profile }) { - if (!user.email) { - console.error('No email provided in user profile'); - return false; + async jwt({ token, account, profile }) { + if (account && profile) { + const keycloakProfile = profile as KeycloakProfile; + token.accessToken = account.access_token ?? ''; + token.refreshToken = account.refresh_token ?? ''; + token.accessTokenExpires = account.expires_at ?? 0; + token.sub = keycloakProfile.sub; + token.role = keycloakProfile.roles ?? []; + token.username = keycloakProfile.preferred_username ?? ''; + token.first_name = keycloakProfile.given_name ?? ''; + token.last_name = keycloakProfile.family_name ?? ''; } - try { - await prisma.user.upsert({ - where: { id: user.id }, - update: { - email: user.email, - password: '', // We don't store password for Keycloak users - }, - create: { - id: user.id, - email: user.email, - password: '', // We don't store password for Keycloak users - }, - }); - - return true; - } catch (error) { - console.error('Error in signIn callback:', error); - return false; + if (Date.now() < (token.accessTokenExpires as number) * 1000) { + return token; } + + return refreshAccessToken(token); }, async session({ session, token }) { - const extendedSession = session as ExtendedSession; - const extendedToken = token as ExtendedJWT; - - if (extendedSession?.user && extendedToken.sub) { - extendedSession.user.id = extendedToken.sub; - extendedSession.user.username = extendedToken.username ?? ''; - extendedSession.user.first_name = extendedToken.first_name ?? ''; - extendedSession.user.last_name = extendedToken.last_name ?? ''; - extendedSession.user.role = extendedToken.role ?? []; - extendedSession.accessToken = extendedToken.accessToken ?? ''; - extendedSession.refreshToken = extendedToken.refreshToken; - extendedSession.serviceTokens = extendedToken.serviceTokens ?? {}; + if (token.error) { + throw new Error(token.error); } - return extendedSession; - }, - async jwt({ token, user, account }) { - const extendedToken = token as ExtendedJWT; + session.user = { + id: token.sub ?? '', + email: token.email ?? null, + name: token.name ?? null, + image: null, + username: token.username ?? '', + first_name: token.first_name ?? '', + last_name: token.last_name ?? '', + role: token.role ?? [], + }; + session.accessToken = token.accessToken; - if (user) { - extendedToken.role = user.role; - extendedToken.username = user.username; - extendedToken.first_name = user.first_name; - extendedToken.last_name = user.last_name; - } - - if (account) { - extendedToken.accessToken = account.access_token; - extendedToken.refreshToken = account.refresh_token; - extendedToken.accessTokenExpires = account.expires_at; - extendedToken.serviceTokens = {}; - } - - return extendedToken; - } - }, - events: { - async signOut({ token }) { - const extendedToken = token as ExtendedJWT; - if (extendedToken.sub) { - await invalidateServiceTokens({ - user: { - id: extendedToken.sub, - name: extendedToken.name ?? null, - email: extendedToken.email ?? null, - username: extendedToken.username ?? '', - first_name: extendedToken.first_name ?? '', - last_name: extendedToken.last_name ?? '', - role: extendedToken.role ?? [], - }, - accessToken: extendedToken.accessToken ?? '', - refreshToken: extendedToken.refreshToken, - serviceTokens: extendedToken.serviceTokens ?? {}, - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - } as ExtendedSession); - } + return session; } }, pages: { @@ -174,3 +154,16 @@ export const authOptions: NextAuthOptions = { const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; +interface JWT { + accessToken: string; + refreshToken: string; + accessTokenExpires: number; +} + +interface Profile { + sub?: string; + email?: string; + name?: string; + roles?: string[]; +} + diff --git a/next.config.mjs b/next.config.mjs index 45f341e1..2b4b870d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -20,6 +20,19 @@ const nextConfig = { webpackBuildWorker: true, parallelServerBuildTraces: true, parallelServerCompiles: true, + }, + async headers() { + return [ + { + source: '/:path*', + headers: [ + { + key: 'Content-Security-Policy', + value: "frame-ancestors 'self' https://espace.slm-lab.net https://connect.slm-lab.net" + } + ] + } + ] } };