import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; declare module "next-auth" { 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 { accessToken: string; refreshToken: string; accessTokenExpires: number; role: string[]; username: string; first_name: string; last_name: string; } } function getRequiredEnvVar(name: string): string { const value = process.env[name]; if (!value) { throw new Error(`Missing required environment variable: ${name}`); } return value; } export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), profile(profile) { console.log("Keycloak profile:", profile); return { id: profile.sub, name: profile.name ?? profile.preferred_username, email: profile.email, first_name: profile.given_name ?? '', last_name: profile.family_name ?? '', username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', role: profile.groups ?? [], } }, }), ], callbacks: { async jwt({ token, account, profile }) { console.log("JWT callback - token:", token); console.log("JWT callback - account:", account); console.log("JWT callback - profile:", profile); if (account && profile) { token.accessToken = account.access_token; token.refreshToken = account.refresh_token; token.accessTokenExpires = account.expires_at! * 1000; token.role = (profile as any).groups ?? []; token.username = (profile as any).preferred_username ?? profile.email?.split('@')[0] ?? ''; token.first_name = (profile as any).given_name ?? ''; token.last_name = (profile as any).family_name ?? ''; return token; } // Return previous token if not expired if (Date.now() < (token.accessTokenExpires as number)) { return token; } try { // Token has expired, try to refresh it const clientId = getRequiredEnvVar("KEYCLOAK_CLIENT_ID"); const clientSecret = getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"); console.log("Attempting to refresh token..."); const response = await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "refresh_token", client_id: clientId, client_secret: clientSecret, refresh_token: token.refreshToken as string, }), } ); const tokens = await response.json(); console.log("Token refresh response:", tokens); if (!response.ok) { console.error("Token refresh failed:", tokens); throw new Error("RefreshAccessTokenError"); } return { ...token, accessToken: tokens.access_token, refreshToken: tokens.refresh_token ?? token.refreshToken, accessTokenExpires: Date.now() + tokens.expires_in * 1000, }; } catch (error) { console.error("Error refreshing token:", error); // Return token with error flag - this will trigger a redirect to sign-in return { ...token, error: "RefreshAccessTokenError", }; } }, async session({ session, token }) { console.log("Session callback - session:", session); console.log("Session callback - token:", token); if (token.error) { console.error("Token error detected:", token.error); // Force sign out if there was a refresh error throw new Error("RefreshAccessTokenError"); } session.accessToken = token.accessToken; session.user = { ...session.user, id: token.sub as string, first_name: token.first_name ?? '', last_name: token.last_name ?? '', username: token.username ?? '', role: token.role ?? [], }; return session; }, }, events: { async signOut({ token }) { console.log("Sign out event - token:", token); if (token.refreshToken) { try { await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/logout`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), client_secret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), refresh_token: token.refreshToken as string, }), } ); } catch (error) { console.error("Error during logout:", error); } } }, }, debug: true, // Enable debug logging }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };