import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; interface RocketChatLoginResponse { status: string; data: { authToken: string; userId: string; }; } 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; rocketChatToken: string; rocketChatUserId: string; } interface JWT { accessToken: string; refreshToken: string; accessTokenExpires: number; role: string[]; username: string; first_name: string; last_name: string; rocketChatToken: string; rocketChatUserId: string; } } declare module "next-auth/jwt" { interface JWT { accessToken: string; refreshToken: string; accessTokenExpires: number; role: string[]; username: string; first_name: string; last_name: string; rocketChatToken: string; rocketChatUserId: string; } } 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) { // Filter out system roles and only keep valid user roles const validRoles = (profile.groups || []) .filter((role: string) => !role.startsWith('default-roles-') && !['offline_access', 'uma_authorization'].includes(role) ); 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: validRoles, } }, }), ], callbacks: { async jwt({ token, account, profile }) { if (account && profile) { // First set the basic token properties const newToken = { ...token, accessToken: account.access_token || '', refreshToken: account.refresh_token || '', accessTokenExpires: account.expires_at! * 1000, role: (profile as any).groups ?.filter((role: string) => !role.startsWith('default-roles-') && !['offline_access', 'uma_authorization'].includes(role) ) ?? [], username: (profile as any).preferred_username ?? profile.email?.split('@')[0] ?? '', first_name: (profile as any).given_name ?? '', last_name: (profile as any).family_name ?? '', rocketChatToken: '', rocketChatUserId: '', }; try { console.log('Attempting to get personal access tokens for user:', newToken.username); // First, let's verify the admin token is working const verifyTokenResponse = await fetch('https://parole.slm-lab.net/api/v1/me', { headers: { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, }, }); if (!verifyTokenResponse.ok) { console.error('Admin token verification failed'); return newToken; } // Get user's personal access tokens using admin credentials const tokensResponse = await fetch('https://parole.slm-lab.net/api/v1/users.getPersonalAccessTokens', { method: 'GET', headers: { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, 'Content-Type': 'application/json', }, }); if (!tokensResponse.ok) { console.error('Failed to get personal access tokens'); return newToken; } const tokensData = await tokensResponse.json(); console.log('Parsed tokens data:', tokensData); // Find or create a token for this user const tokenName = `keycloak-${newToken.username}`; let personalToken: string | null = null; let rocketChatUserId: string | null = null; // First, get the user's Rocket.Chat ID const userInfoResponse = await fetch(`https://parole.slm-lab.net/api/v1/users.info?username=${newToken.username}`, { headers: { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, }, }); if (!userInfoResponse.ok) { console.error('Failed to get user info from Rocket.Chat'); return newToken; } const userInfo = await userInfoResponse.json(); console.log('User info from Rocket.Chat:', userInfo); if (userInfo.user && userInfo.user._id) { rocketChatUserId = userInfo.user._id; } else { console.error('No user ID found in Rocket.Chat response'); return newToken; } if (tokensData.tokens && tokensData.tokens.length > 0) { // Use existing token const existingToken = tokensData.tokens.find((t: any) => t.name === tokenName); if (existingToken) { console.log('Found existing token:', existingToken); personalToken = existingToken.lastTokenPart; } } if (!personalToken) { console.log('Creating new personal access token'); // Create new token const createTokenResponse = await fetch('https://parole.slm-lab.net/api/v1/users.generatePersonalAccessToken', { method: 'POST', headers: { 'X-Auth-Token': process.env.ROCKET_CHAT_TOKEN!, 'X-User-Id': process.env.ROCKET_CHAT_USER_ID!, 'Content-Type': 'application/json', }, body: JSON.stringify({ tokenName, bypassTwoFactor: true, }), }); if (createTokenResponse.ok) { const createTokenData = await createTokenResponse.json(); console.log('Created token data:', createTokenData); personalToken = createTokenData.token; } else { console.error('Failed to create personal access token'); return newToken; } } if (personalToken && rocketChatUserId) { console.log('Setting Rocket.Chat credentials in token'); return { ...newToken, rocketChatToken: personalToken, rocketChatUserId: rocketChatUserId, }; } return newToken; } catch (error) { console.error('Error in Rocket.Chat authentication:', error); return newToken; } } // Return previous token if not expired if (Date.now() < (token.accessTokenExpires as number)) { return token; } try { 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: process.env.KEYCLOAK_CLIENT_ID!, client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, refresh_token: token.refreshToken, }), } ); const tokens = await response.json(); if (!response.ok) throw tokens; 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, error: "RefreshAccessTokenError" }; } }, async session({ session, token }) { if (token.error) { throw new Error("RefreshAccessTokenError"); } console.log('Session callback token:', { hasRocketChatToken: !!token.rocketChatToken, hasRocketChatUserId: !!token.rocketChatUserId, token: token }); return { ...session, accessToken: token.accessToken, rocketChatToken: token.rocketChatToken || '', rocketChatUserId: token.rocketChatUserId || '', user: { ...session.user, id: token.sub as string, first_name: token.first_name || '', last_name: token.last_name || '', username: token.username || '', role: token.role || [], }, }; }, }, events: { async signOut({ 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: process.env.KEYCLOAK_CLIENT_ID!, client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, refresh_token: token.refreshToken as string, }), } ); } catch (error) { console.error("Error during logout:", error); } } }, }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };