import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; const KEYCLOAK_URL = process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'https://security.lions.dev'; const KEYCLOAK_REALM = process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'btpxpress'; const CLIENT_ID = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'btpxpress-frontend'; const REDIRECT_URI = process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback` : 'https://btpxpress.lions.dev/auth/callback'; const TOKEN_ENDPOINT = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`; export async function POST(request: NextRequest) { console.log('🔐 Token exchange API called'); try { const body = await request.json(); const { code, state } = body; if (!code) { console.error('❌ No authorization code provided'); return NextResponse.json( { error: 'Code d\'autorisation manquant' }, { status: 400 } ); } console.log('✅ Exchanging code with Keycloak...'); console.log('📍 Token endpoint:', TOKEN_ENDPOINT); console.log('📍 Client ID:', CLIENT_ID); console.log('📍 Redirect URI:', REDIRECT_URI); // Préparer les paramètres pour l'échange de code const params = new URLSearchParams({ grant_type: 'authorization_code', client_id: CLIENT_ID, code: code, redirect_uri: REDIRECT_URI, }); // Échanger le code contre des tokens const tokenResponse = await fetch(TOKEN_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }); if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error('❌ Keycloak token exchange failed:', tokenResponse.status, errorText); let errorData; try { errorData = JSON.parse(errorText); } catch { errorData = { error: 'Token exchange failed', details: errorText }; } return NextResponse.json( { error: errorData.error || 'Échec de l\'échange de token', error_description: errorData.error_description || 'Erreur lors de la communication avec Keycloak', details: errorData }, { status: tokenResponse.status } ); } const tokens = await tokenResponse.json(); console.log('✅ Tokens received from Keycloak'); // Stocker les tokens dans des cookies HttpOnly sécurisés const cookieStore = cookies(); const isProduction = process.env.NODE_ENV === 'production'; // Access token (durée: expires_in secondes) cookieStore.set('access_token', tokens.access_token, { httpOnly: true, secure: isProduction, sameSite: 'lax', maxAge: tokens.expires_in || 300, // Par défaut 5 minutes path: '/', }); // Refresh token (durée plus longue) if (tokens.refresh_token) { cookieStore.set('refresh_token', tokens.refresh_token, { httpOnly: true, secure: isProduction, sameSite: 'lax', maxAge: tokens.refresh_expires_in || 1800, // Par défaut 30 minutes path: '/', }); } // ID token if (tokens.id_token) { cookieStore.set('id_token', tokens.id_token, { httpOnly: true, secure: isProduction, sameSite: 'lax', maxAge: tokens.expires_in || 300, path: '/', }); } // Stocker aussi le temps d'expiration cookieStore.set('token_expires_at', String(Date.now() + (tokens.expires_in * 1000)), { httpOnly: true, secure: isProduction, sameSite: 'lax', maxAge: tokens.expires_in || 300, path: '/', }); console.log('✅ Tokens stored in HttpOnly cookies'); // Retourner une réponse de succès (sans les tokens) return NextResponse.json({ success: true, message: 'Authentification réussie', expires_in: tokens.expires_in, }); } catch (error) { console.error('❌ Error in token exchange API:', error); return NextResponse.json( { error: 'Erreur serveur', message: error instanceof Error ? error.message : 'Erreur inconnue' }, { status: 500 } ); } }