188 lines
6.2 KiB
TypeScript
188 lines
6.2 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...')
|
|
|
|
// Return HTML with client-side redirect for SPA compatibility
|
|
setHeader(event, 'Content-Type', 'text/html')
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Authentication Successful - Port Nimara Portal</title>
|
|
<meta http-equiv="refresh" content="0;url=/dashboard">
|
|
<script>
|
|
// Immediate redirect
|
|
window.location.href = '/dashboard';
|
|
</script>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: linear-gradient(135deg, #387bca 0%, #2c5aa0 100%);
|
|
color: white;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
margin: 0;
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
.spinner {
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
border-top: 3px solid white;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 1rem auto;
|
|
}
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="spinner"></div>
|
|
<h2>Authentication successful!</h2>
|
|
<p>Redirecting to dashboard...</p>
|
|
<p><small>If you are not redirected automatically, <a href="/dashboard" style="color: #ffffff;">click here</a>.</small></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
} 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}`)
|
|
}
|
|
})
|