push update socket data dan token
This commit is contained in:
No files matched your search
@@ -1,242 +1,261 @@
|
||||
const config = useRuntimeConfig();
|
||||
// Define session duration (default to 1 hour if not specified in config)
|
||||
const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60 * 24;
|
||||
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);
|
||||
try {
|
||||
const config = useRuntimeConfig();
|
||||
const query = getQuery(event);
|
||||
|
||||
console.log('🔄 === KEYCLOAK CALLBACK STARTED ===');
|
||||
console.log('📋 Query parameters:', query);
|
||||
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');
|
||||
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);
|
||||
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();
|
||||
|
||||
let idTokenPayload;
|
||||
try {
|
||||
idTokenPayload = JSON.parse(
|
||||
Buffer.from(tokens.id_token.split('.')[1], 'base64').toString()
|
||||
);
|
||||
} catch (decodeError) {
|
||||
console.error('❌ Failed to decode ID token:', decodeError);
|
||||
const errorMsg = encodeURIComponent('Invalid ID token format');
|
||||
return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
|
||||
}
|
||||
|
||||
// Store minimal session data in cookie to reduce size
|
||||
// The ID token contains user info, so we can decode it when needed
|
||||
const sessionData = {
|
||||
// Store only essential user info (can be decoded from ID token if needed)
|
||||
user: {
|
||||
id: idTokenPayload.sub,
|
||||
email: idTokenPayload.email,
|
||||
name: idTokenPayload.name || idTokenPayload.preferred_username,
|
||||
preferred_username: idTokenPayload.preferred_username,
|
||||
},
|
||||
// Store tokens - these are necessary for API calls
|
||||
// Note: These JWT tokens are large, but necessary for authentication
|
||||
accessToken: tokens.access_token,
|
||||
idToken: tokens.id_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
// Session metadata
|
||||
expiresAt: Date.now() + (SESSION_DURATION * 1000),
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
// Determine if we should use secure cookies
|
||||
// For localhost, always use secure: false
|
||||
const isSecure = process.env.NODE_ENV === 'production' &&
|
||||
event.node.req.headers['x-forwarded-proto'] === 'https';
|
||||
|
||||
console.log('🔗 Setting session cookie with secure flag:', isSecure);
|
||||
console.log('⏱️ Session duration:', SESSION_DURATION, 'seconds');
|
||||
console.log('🌐 Request host:', event.node.req.headers.host);
|
||||
console.log('🔒 Protocol:', event.node.req.headers['x-forwarded-proto'] || 'http');
|
||||
|
||||
// Set cookie with proper settings for localhost
|
||||
// For localhost HTTP, we need secure: false and sameSite: 'lax'
|
||||
// IMPORTANT: Ensure domain is not set for localhost (allows cookie to work)
|
||||
const cookieOptions: any = {
|
||||
httpOnly: true,
|
||||
secure: isSecure,
|
||||
sameSite: 'lax' as const,
|
||||
maxAge: SESSION_DURATION,
|
||||
path: '/',
|
||||
// Explicitly don't set domain for localhost - this is important!
|
||||
// Setting domain to 'localhost' can cause cookies to not work
|
||||
};
|
||||
|
||||
// For localhost, don't set domain (allows cookie to work on localhost)
|
||||
// Only set domain in production if needed
|
||||
if (process.env.NODE_ENV === 'production' && !event.node.req.headers.host?.includes('localhost')) {
|
||||
// Optionally set domain in production
|
||||
// cookieOptions.domain = '.yourdomain.com';
|
||||
}
|
||||
|
||||
// Store session in server-side store and use session ID in cookie
|
||||
// This avoids cookie size limits (4KB)
|
||||
const { createSession } = await import('~/server/utils/sessionStore');
|
||||
const sessionId = createSession(sessionData);
|
||||
|
||||
console.log('💾 Session stored server-side with ID:', sessionId.substring(0, 8) + '...');
|
||||
console.log('📦 Session ID cookie size: ~64 bytes (much smaller!)');
|
||||
|
||||
// Store only the session ID in the cookie (much smaller)
|
||||
setCookie(event, 'user_session', sessionId, cookieOptions);
|
||||
|
||||
console.log('✅ Session ID cookie set in response headers (will be available in next request)');
|
||||
|
||||
console.log('✅ Session cookie created successfully');
|
||||
console.log('🍪 Cookie details:');
|
||||
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}`);
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user