port-nimara-client-portal/server/api/auth/keycloak/callback.ts

135 lines
4.4 KiB
TypeScript

import { keycloakClient } from '~/server/utils/keycloak-client'
export default defineEventHandler(async (event) => {
const startTime = Date.now()
const query = getQuery(event)
const { code, state, error } = query
console.log('[KEYCLOAK] Callback received:', {
code: !!code,
state,
error,
requestId: event.node.req.headers['x-request-id'] || 'unknown'
})
if (error) {
const errorMsg = `Authentication failed: ${error}`
console.error('[KEYCLOAK] OAuth error:', errorMsg)
// Add timing info for debugging
const duration = Date.now() - startTime
console.error(`[KEYCLOAK] Failed after ${duration}ms`)
throw createError({
statusCode: 400,
statusMessage: errorMsg
})
}
if (!code) {
const errorMsg = 'No authorization code received'
console.error('[KEYCLOAK] ' + errorMsg)
const duration = Date.now() - startTime
console.error(`[KEYCLOAK] Failed after ${duration}ms`)
throw createError({
statusCode: 400,
statusMessage: errorMsg
})
}
try {
console.log('[KEYCLOAK] Starting token exchange...')
const redirectUri = 'https://client.portnimara.dev/api/auth/keycloak/callback'
// Use the new Keycloak client with retry logic and circuit breaker
const tokenResponse = await keycloakClient.exchangeCodeForTokens(code as string, redirectUri)
const tokenExchangeDuration = Date.now() - startTime
console.log(`[KEYCLOAK] Token exchange successful in ${tokenExchangeDuration}ms:`, {
hasAccessToken: !!tokenResponse.access_token,
hasRefreshToken: !!tokenResponse.refresh_token,
expiresIn: tokenResponse.expires_in
})
// Get user info with retry logic
console.log('[KEYCLOAK] Fetching user info...')
const userInfoStartTime = Date.now()
const userInfo = await keycloakClient.getUserInfo(tokenResponse.access_token)
const userInfoDuration = Date.now() - userInfoStartTime
console.log(`[KEYCLOAK] User info retrieved in ${userInfoDuration}ms:`, {
sub: userInfo.sub,
email: userInfo.email,
username: userInfo.preferred_username,
name: userInfo.name
})
// Set session cookie with proper configuration
const sessionData = {
user: {
id: userInfo.sub,
email: userInfo.email,
username: userInfo.preferred_username || userInfo.email,
name: userInfo.name || userInfo.preferred_username || userInfo.email,
authMethod: 'keycloak'
},
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
expiresAt: Date.now() + (tokenResponse.expires_in * 1000),
createdAt: Date.now()
}
// Create session cookie with proper session duration (8 hours = 28800 seconds)
// Not tied to access token lifetime since we'll refresh tokens automatically
const sessionDuration = 8 * 60 * 60; // 8 hours in seconds
const cookieDomain = process.env.COOKIE_DOMAIN || '.portnimara.dev';
setCookie(event, 'nuxt-oidc-auth', JSON.stringify(sessionData), {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: sessionDuration,
domain: cookieDomain,
path: '/'
})
const totalDuration = Date.now() - startTime
console.log(`[KEYCLOAK] Authentication completed successfully in ${totalDuration}ms`)
console.log('[KEYCLOAK] Session cookie set, redirecting to dashboard...')
// Redirect to dashboard
await sendRedirect(event, '/dashboard')
} catch (error: any) {
const duration = Date.now() - startTime
console.error(`[KEYCLOAK] Authentication failed after ${duration}ms:`, {
message: error.message,
status: error.status,
statusMessage: error.statusMessage,
data: error.data
})
// Log circuit breaker status for debugging
const circuitStatus = keycloakClient.getCircuitBreakerStatus()
if (circuitStatus.isOpen) {
console.error('[KEYCLOAK] Circuit breaker is OPEN:', circuitStatus)
}
// Provide more specific error messages
let errorParam = 'auth_failed'
if (error.status === 503) {
errorParam = 'service_unavailable'
} else if (error.status >= 500) {
errorParam = 'server_error'
} else if (error.status === 401 || error.status === 403) {
errorParam = 'access_denied'
}
// Redirect to login with specific error
await sendRedirect(event, `/login?error=${errorParam}`)
}
})