const config = useRuntimeConfig(); // Define session duration (default to 1 hour if not specified in config) const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60 * 24; // This is the MAIN SESSION duration. It controls how long a user stays logged in. // Current configuration: 1 hour (3600 seconds). export default defineEventHandler(async (event) => { try { const config = useRuntimeConfig(); const query = getQuery(event); console.log("🔄 === KEYCLOAK CALLBACK STARTED ==="); console.log("📋 Query parameters:", query); const code = query.code as string; const state = query.state as string; const error = query.error as string; const storedState = getCookie(event, "oauth_state"); if (error) { console.error("❌ OAuth error from Keycloak:", error); const errorDescription = query.error_description as string; console.error("❌ Error description:", errorDescription); const errorMsg = encodeURIComponent( `Keycloak error: ${error} - ${errorDescription || "Please try again"}`, ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } console.log("📝 Code received:", !!code); console.log("🎲 State from URL:", state); console.log("🎲 State from cookie:", storedState); console.log("🎲 State validation:", state === storedState); if (!state || state !== storedState) { console.error("❌ Invalid state parameter - possible CSRF attack"); console.error(" Expected:", storedState); console.error(" Received:", state); const errorMsg = encodeURIComponent( "Security validation failed. Please try logging in again.", ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } deleteCookie(event, "oauth_state"); if (!code) { console.error("❌ Authorization code not provided"); const errorMsg = encodeURIComponent( "No authorization code received from Keycloak.", ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } // Validate Keycloak configuration if (!config.keycloakIssuer) { console.error("❌ KEYCLOAK_ISSUER is not configured"); const errorMsg = encodeURIComponent( "Keycloak server is not configured. Please contact administrator.", ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } if (!config.keycloakClientId || !config.keycloakClientSecret) { console.error("❌ Keycloak client credentials are not configured"); const errorMsg = encodeURIComponent( "Keycloak client credentials are missing. Please contact administrator.", ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } const tokenUrl = `${config.keycloakIssuer}/protocol/openid-connect/token`; const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback`; console.log("🔗 Token URL:", tokenUrl); console.log("🔗 Redirect URI:", redirectUri); console.log( "🔑 Client ID:", config.keycloakClientId ? "***configured***" : "MISSING", ); const tokenPayload = new URLSearchParams({ grant_type: "authorization_code", client_id: config.keycloakClientId, client_secret: config.keycloakClientSecret, code, redirect_uri: redirectUri, }); let tokenResponse; try { // Create abort controller for timeout (compatible with all Node.js versions) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout tokenResponse = await fetch(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: tokenPayload, signal: controller.signal, }); clearTimeout(timeoutId); } catch (fetchError: any) { console.error("❌ Fetch error details:"); console.error(" - Error type:", fetchError.name); console.error(" - Error message:", fetchError.message); console.error(" - Token URL attempted:", tokenUrl); // Provide more specific error messages let errorMsg = "Failed to connect to authentication server."; if ( fetchError.name === "AbortError" || fetchError.message.includes("timeout") ) { errorMsg = "Authentication server timeout. Please try again."; } else if ( fetchError.message.includes("ENOTFOUND") || fetchError.message.includes("getaddrinfo") ) { errorMsg = "Cannot reach authentication server. Please check network connection."; } else if (fetchError.message.includes("ECONNREFUSED")) { errorMsg = "Authentication server refused connection. Server may be down."; } else if ( fetchError.message.includes("certificate") || fetchError.message.includes("SSL") ) { errorMsg = "SSL certificate error. Please contact administrator."; } const encodedError = encodeURIComponent(errorMsg); return sendRedirect(event, `/LoginPage?error=${encodedError}`); } if (!tokenResponse.ok) { const errorText = await tokenResponse.text(); console.error("❌ Token exchange failed:", errorText); const errorMsg = encodeURIComponent( `Token exchange failed: ${tokenResponse.status} - Please check Keycloak configuration`, ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } const tokens = await tokenResponse.json(); // Parse token payloads for immediate availability let accessTokenPayload; let idTokenPayloadFull; try { accessTokenPayload = JSON.parse( Buffer.from(tokens.access_token.split(".")[1], "base64").toString(), ); idTokenPayloadFull = JSON.parse( Buffer.from(tokens.id_token.split(".")[1], "base64").toString(), ); } catch (parseError) { console.error("❌ Failed to parse token payloads:", parseError); const errorMsg = encodeURIComponent("Invalid token format"); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } // Create session data const sessionData = { user: { id: idTokenPayloadFull.sub, email: idTokenPayloadFull.email, name: idTokenPayloadFull.name || idTokenPayloadFull.preferred_username, preferred_username: idTokenPayloadFull.preferred_username, }, accessToken: tokens.access_token, idToken: tokens.id_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + SESSION_DURATION * 1000, createdAt: Date.now(), scope: accessTokenPayload.scope || "openid email profile", status: "authenticated", }; // Store session in server-side store and get session ID const { createSession } = await import('~/server/utils/sessionStore'); const sessionId = createSession(sessionData); // Determine if we should use secure cookies const isSecure = process.env.NODE_ENV === "production" && event.node.req.headers["x-forwarded-proto"] === "https"; console.log("🔗 Setting session ID cookie"); console.log("⏱️ Session duration:", SESSION_DURATION, "seconds"); const cookieOptions: any = { httpOnly: true, secure: isSecure, sameSite: "lax" as const, maxAge: SESSION_DURATION, path: "/", }; // Store only session ID in cookie (small size, ~64 bytes) setCookie(event, "user_session", sessionId, cookieOptions); console.log("✅ Session ID cookie created successfully"); console.log("🍪 Cookie details:"); console.log(" - Name: user_session"); console.log(" - Size:", sessionId.length, "bytes"); console.log(" - Path: /"); console.log(" - Secure:", isSecure); console.log(" - SameSite: lax"); console.log(" - HttpOnly: true"); console.log(" - MaxAge:", SESSION_DURATION, "seconds"); console.log(" - Host:", event.node.req.headers.host); // Auto-sync user data to database (first time login check) // Pass session createdAt as loginTime to sync function try { const { syncUserFromTokens } = await import("~/server/utils/userSync"); const result = syncUserFromTokens( tokens.id_token, tokens.access_token, sessionData.createdAt, ); console.log( `✅ User auto-sync on login: ${result.action} - ${result.message}`, ); } catch (syncError: any) { // Don't fail the login if sync fails, just log it console.error("⚠️ Failed to auto-sync user on login:", syncError); } // IMPORTANT: Ensure cookie is set before redirect // The cookie should be in the Set-Cookie header of the redirect response console.log( "↪️ Redirecting to dashboard with cookie in response headers...", ); // Note: In H3/Nitro, setCookie automatically adds Set-Cookie header to response // The cookie will be available in the browser after the redirect // We can't verify it in the same request, but it should be set correctly // Use sendRedirect - it should include the Set-Cookie header // The browser will receive the cookie and include it in the next request return sendRedirect(event, "/dashboard?authenticated=true", 302); } catch (error: any) { console.error("❌ === CALLBACK ERROR ==="); console.error("❌ Error message:", error.message); console.error("❌ Error stack:", error.stack); console.error("❌ =================="); const errorMsg = encodeURIComponent( `Authentication failed: ${error.message}`, ); return sendRedirect(event, `/LoginPage?error=${errorMsg}`); } });