// server/api/auth/keycloak-login.ts import { randomBytes } from 'crypto' export default defineEventHandler(async (event) => { console.log('๐Ÿ” Keycloak Login Handler Called') console.log('๐Ÿ“ Method:', getMethod(event)) // === STALE SESSION CLEANUP === // Check for existing session and clean up if invalid/expired const existingSessionCookie = getCookie(event, 'user_session') if (existingSessionCookie) { console.log('๐Ÿ” Existing session cookie found, validating...') try { // Decode JWT-based session from cookie const sessionJson = Buffer.from(existingSessionCookie, 'base64').toString('utf-8'); const session = JSON.parse(sessionJson); if (session) { // Check if session is expired const isExpired = Date.now() > session.expiresAt if (isExpired) { console.log('๐Ÿงน Cleaning up expired session...') deleteCookie(event, 'user_session') deleteCookie(event, 'oauth_state') console.log('โœ… Expired session cleared') } else { console.log('โš ๏ธ Valid session exists, clearing to allow fresh login...') deleteCookie(event, 'user_session') deleteCookie(event, 'oauth_state') console.log('โœ… Existing session cleared for fresh login') } } } catch (error) { console.warn('โš ๏ธ Error during session cleanup:', error) // Clear cookies anyway to be safe deleteCookie(event, 'user_session') deleteCookie(event, 'oauth_state') } } else { console.log('โ„น๏ธ No existing session found, proceeding with fresh login') } // === END STALE SESSION CLEANUP === try { const config = useRuntimeConfig() // Debug: Log runtime config (without secrets) console.log('๐Ÿ”ง Runtime Config Check:') console.log(' - Has keycloakIssuer:', !!config.keycloakIssuer) console.log(' - Has keycloakClientId:', !!config.keycloakClientId) console.log(' - Has keycloakSecret:', !!config.keycloakClientSecret) console.log(' - Issuer value:', config.keycloakIssuer) // Validate required configuration if (!config.keycloakIssuer) { throw new Error('KEYCLOAK_ISSUER is not configured') } if (!config.keycloakClientId) { throw new Error('KEYCLOAK_CLIENT_ID is not configured') } // Generate state parameter for security const state = randomBytes(32).toString('hex') console.log('๐ŸŽฒ Generated state:', state.substring(0, 8) + '...') const oauthDuration = (config.oauthStateDurationMinutes || 10) * 60; // Default to 10 minutes const isSecure = process.env.NODE_ENV === 'production' && event.node.req.headers['x-forwarded-proto'] === 'https'; // Store state in session cookie // IMPORTANT: This cookie is ONLY for the login flow (CSRF protection). // It is NOT the main session duration. It expires quickly to ensure security. setCookie(event, 'oauth_state', state, { httpOnly: true, secure: isSecure, sameSite: 'lax' as const, maxAge: oauthDuration }) // Build Keycloak authorization URL const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback` // Debug: Log the redirect URI being used console.log('๐Ÿ”ง AUTH_ORIGIN from config:', config.public.authUrl) console.log('๐Ÿ”— Redirect URI being sent to Keycloak:', redirectUri) const authUrl = new URL(`${config.keycloakIssuer}/protocol/openid-connect/auth`) authUrl.searchParams.set('client_id', config.keycloakClientId) authUrl.searchParams.set('redirect_uri', redirectUri) authUrl.searchParams.set('response_type', 'code') authUrl.searchParams.set('scope', 'openid profile email') authUrl.searchParams.set('state', state) console.log('๐Ÿ—๏ธ Auth URL built:', authUrl.toString()) return { success: true, data: { authUrl: authUrl.toString() } } } catch (error: any) { console.error('โŒ Login Error:', error.message) throw createError({ statusCode: 500, statusMessage: `Failed to generate authorization URL: ${error.message}` }) } })