import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; import { jwtDecode } from "jwt-decode"; interface KeycloakProfile { sub: string; email?: string; name?: string; roles?: string[]; preferred_username?: string; given_name?: string; family_name?: string; realm_access?: { roles: string[]; }; } interface DecodedToken { realm_access?: { roles: string[]; }; [key: string]: any; } 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; nextcloudToken: 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 { const value = process.env[name]; if (!value) { throw new Error(`Missing required environment variable: ${name}`); } 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", }; } } // Add NextCloud token exchange logic async function exchangeKeycloakTokenForNextCloud(token: string): Promise { const response = await fetch(`${process.env.NEXTCLOUD_URL}/apps/oauth2/api/v1/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', subject_token: token, subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', requested_token_type: 'urn:ietf:params:oauth:token-type:access_token' }) }); if (!response.ok) { throw new Error('Failed to exchange token with NextCloud'); } const data = await response.json(); return data.access_token; } export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), authorization: { params: { scope: "openid profile email roles" } }, profile(profile) { console.log('Keycloak profile callback:', { rawProfile: profile, rawRoles: profile.roles, realmAccess: profile.realm_access, groups: profile.groups }); // Get roles from realm_access const roles = profile.realm_access?.roles || []; console.log('Profile callback raw roles:', roles); // Clean up roles by removing ROLE_ prefix and converting to lowercase const cleanRoles = roles.map((role: string) => role.replace(/^ROLE_/, '').toLowerCase() ); console.log('Profile callback cleaned roles:', cleanRoles); 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: cleanRoles, } }, }), ], session: { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30 days }, callbacks: { async jwt({ token, account }) { if (account?.access_token) { token.accessToken = account.access_token; // Only set refresh token if it exists if (account.refresh_token) { token.refreshToken = account.refresh_token; } // Set expiry if it exists, otherwise set a default token.accessTokenExpires = account.expires_at ? account.expires_at * 1000 : Date.now() + 3600 * 1000; } return token; }, async session({ session, token }) { if (token) { session.accessToken = token.accessToken; // We'll handle Nextcloud authentication separately using app passwords session.user = { ...session.user, id: token.sub || '', role: token.role || [], username: token.username || '', first_name: token.first_name || '', last_name: token.last_name || '', }; } return session; } }, pages: { signIn: '/signin', error: '/signin', }, debug: process.env.NODE_ENV === 'development', }; 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[]; }