Files
web-antrean/server/api/auth/keycloak-callback.get.ts
2026-02-10 09:51:17 +07:00

262 lines
9.6 KiB
TypeScript

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}`);
}
});