import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; import { prisma } from '@/lib/prisma'; import { ExtendedJWT, ExtendedSession, ServiceToken, invalidateServiceTokens, clearAllCookies } from '@/lib/session'; import { Session } from "next-auth"; declare module "next-auth" { interface Session extends ExtendedSession {} interface JWT { accessToken?: string; refreshToken?: string; accessTokenExpires?: number; role?: string[]; username?: string; first_name?: string; last_name?: string; name?: string | null; email?: string | null; serviceTokens: { rocketChat?: ServiceToken; leantime?: ServiceToken; calendar?: ServiceToken; mail?: ServiceToken; [key: string]: ServiceToken | undefined; }; } } 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: process.env.KEYCLOAK_CLIENT_ID!, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, issuer: process.env.KEYCLOAK_ISSUER!, 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 ?? [], } }, }), ], session: { strategy: "jwt", maxAge: 8 * 60 * 60, // 8 hours }, 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: 8 * 60 * 60 // 8 hours } }, 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: 8 * 60 * 60 // 8 hours } }, 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: 8 * 60 * 60 // 8 hours } } }, callbacks: { async signIn({ user, account, profile }) { if (!user.email) { console.error('No email provided in user profile'); return false; } try { // Create or update user in local database 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; } }, 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 ?? {}; } return extendedSession; }, async jwt({ token, user, account }) { const extendedToken = token as ExtendedJWT; 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()).toISOString(), // Expire immediately } as ExtendedSession); // Force clear all cookies on signout if (typeof window !== 'undefined') { clearAllCookies(); } } } }, pages: { signIn: '/signin', error: '/signin', signOut: '/signin', // Redirect to signin after signout }, debug: process.env.NODE_ENV === 'development', }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };