push update socket data dan token

This commit is contained in:
Fanrouver
2026-02-10 09:51:17 +07:00
parent 101dc38910
commit fb70237a12
22 changed files with 1630 additions and 827 deletions

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
// server/api/auth/clear-session.post.ts
// Endpoint to forcefully clear session cookies and logout from Keycloak
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig();
console.log('🧹 Clear session endpoint called');
// Get the current session to retrieve ID token for Keycloak logout
const sessionCookie = getCookie(event, 'user_session');
let idToken = null;
if (sessionCookie) {
try {
// Try to decode JWT-based session from cookie
const sessionJson = Buffer.from(sessionCookie, 'base64').toString('utf-8');
const session = JSON.parse(sessionJson);
idToken = session.idToken;
console.log('🔑 ID token found for Keycloak logout');
} catch (error) {
console.warn('⚠️ Could not parse session cookie (might be old format)');
// Continue anyway to clear cookies
}
}
// Clear all auth-related cookies
console.log('🧹 Clearing all session cookies...');
deleteCookie(event, 'user_session');
deleteCookie(event, 'oauth_state');
// Also clear with different path variations
deleteCookie(event, 'user_session', { path: '/' });
deleteCookie(event, 'oauth_state', { path: '/' });
console.log('✅ Local session cleared successfully');
// Build Keycloak logout URL
const logoutPath = config.keycloakLogoutUri || `${config.keycloakIssuer}/protocol/openid-connect/logout`;
const logoutUrl = new URL(logoutPath);
const postLogoutRedirectUri = config.postLogoutRedirectUri || `${config.public.authUrl}/LoginPage?logout=success`;
logoutUrl.searchParams.set('client_id', config.keycloakClientId);
logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
// Add ID token hint if available for proper Keycloak session termination
if (idToken) {
logoutUrl.searchParams.set('id_token_hint', idToken);
console.log('🔑 Added id_token_hint to Keycloak logout URL');
}
console.log('🔗 Keycloak logout URL:', logoutUrl.toString());
return {
success: true,
logoutUrl: logoutUrl.toString(),
message: 'Session cleared successfully. Redirecting to Keycloak logout...'
};
} catch (error: any) {
console.error('❌ Clear session error:', error);
// Even on error, provide a basic logout URL
const config = useRuntimeConfig();
const postLogoutRedirectUri = config.postLogoutRedirectUri || `${config.public.authUrl}/LoginPage?logout=success`;
const logoutPath = config.keycloakLogoutUri || `${config.keycloakIssuer}/protocol/openid-connect/logout`;
const fallbackLogoutUrl = new URL(logoutPath);
fallbackLogoutUrl.searchParams.set('client_id', config.keycloakClientId);
fallbackLogoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);
return {
success: false,
logoutUrl: fallbackLogoutUrl.toString(),
error: 'Error during session cleanup',
message: error.message
};
}
});
+249 -230
View File
@@ -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}`);
}
});
+5 -11
View File
@@ -7,14 +7,15 @@ export default defineEventHandler(async (event) => {
// === STALE SESSION CLEANUP ===
// Check for existing session and clean up if invalid/expired
const existingSessionId = getCookie(event, 'user_session')
const existingSessionCookie = getCookie(event, 'user_session')
if (existingSessionId) {
if (existingSessionCookie) {
console.log('🔍 Existing session cookie found, validating...')
try {
const { getSession, deleteSession } = await import('~/server/utils/sessionStore')
const session = getSession(existingSessionId)
// Decode JWT-based session from cookie
const sessionJson = Buffer.from(existingSessionCookie, 'base64').toString('utf-8');
const session = JSON.parse(sessionJson);
if (session) {
// Check if session is expired
@@ -22,22 +23,15 @@ export default defineEventHandler(async (event) => {
if (isExpired) {
console.log('🧹 Cleaning up expired session...')
deleteSession(existingSessionId)
deleteCookie(event, 'user_session')
deleteCookie(event, 'oauth_state')
console.log('✅ Expired session cleared')
} else {
console.log('⚠️ Valid session exists, clearing to allow fresh login...')
deleteSession(existingSessionId)
deleteCookie(event, 'user_session')
deleteCookie(event, 'oauth_state')
console.log('✅ Existing session cleared for fresh login')
}
} else {
console.log('🧹 Session cookie exists but no session in store, clearing cookie...')
deleteCookie(event, 'user_session')
deleteCookie(event, 'oauth_state')
console.log('✅ Stale cookie cleared')
}
} catch (error) {
console.warn('⚠️ Error during session cleanup:', error)
+9 -12
View File
@@ -4,22 +4,19 @@ export default defineEventHandler(async (event) => {
const config = useRuntimeConfig();
console.log('🚪 Logout handler called');
// Get the current session to retrieve tokens
const sessionId = getCookie(event, 'user_session');
// Get the current session to retrieve ID token for proper Keycloak logout
const sessionCookie = getCookie(event, 'user_session');
let idToken = null;
if (sessionId) {
if (sessionCookie) {
try {
const { getSession, deleteSession } = await import('~/server/utils/sessionStore');
const session = getSession(sessionId);
if (session) {
idToken = session.idToken;
console.log('🔑 ID token found in session:', !!idToken);
// Delete session from store
deleteSession(sessionId);
}
// Decode JWT-based session from cookie
const sessionJson = Buffer.from(sessionCookie, 'base64').toString('utf-8');
const session = JSON.parse(sessionJson);
idToken = session.idToken;
console.log('🔑 ID token found in session:', !!idToken);
} catch (error) {
console.warn('⚠️ Could not retrieve session:', error);
console.warn('⚠️ Could not parse session cookie:', error);
}
} else {
console.warn('⚠️ No session cookie found');
+71 -54
View File
@@ -1,74 +1,73 @@
// server/api/auth/session.get.ts
import type { SessionResponse } from '~/types/auth'
// Helper function to safely decode the JWT payload (Access Token or ID Token)
const decodeTokenPayload = (token: string | undefined): any | null => {
if (!token) return null;
try {
// Tokens are base64 encoded and separated by '.'
const parts = token.split(".");
if (parts.length < 2) return null; // Not a valid JWT format
const payloadBase64 = parts[1];
// Decode from base64 and parse the JSON
// Note: Using Buffer.from is standard in Node.js server environments (like Nitro/H3)
return JSON.parse(Buffer.from(payloadBase64, "base64").toString());
} catch (e) {
console.error("❌ Failed to decode token payload:", e);
return null;
}
};
// --- START OF THE SINGLE EXPORT DEFAULT HANDLER ---
export default defineEventHandler(async (event) => {
console.log("🔍 Session endpoint called");
console.log('🔍 Session endpoint called');
const sessionId = getCookie(event, "user_session");
console.log("🍪 Session cookie exists:", !!sessionId);
const sessionCookie = getCookie(event, 'user_session');
console.log('🍪 Session cookie exists:', !!sessionCookie);
if (!sessionId) {
console.log("❌ No session cookie found");
if (!sessionCookie) {
console.log('❌ No session cookie found');
throw createError({
statusCode: 401,
statusMessage: "No session cookie found",
statusMessage: 'No session cookie found',
});
}
try {
// Get session from server-side store using session ID
// Get session from store using session ID
const { getSession } = await import('~/server/utils/sessionStore');
const session = getSession(sessionId);
const session = getSession(sessionCookie);
if (!session) {
console.log("❌ Session not found or expired");
deleteCookie(event, "user_session");
console.log('❌ Session not found in store or expired');
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: "Session expired or invalid",
statusMessage: 'Session not found or expired',
});
}
console.log("📋 Session retrieved from store successfully");
console.log('📋 Session retrieved from store successfully');
// Parse token payloads on-demand from tokens
let accessTokenPayload = null;
let idTokenPayload = null;
try {
if (session.accessToken) {
const accessParts = session.accessToken.split('.');
if (accessParts.length >= 2) {
accessTokenPayload = JSON.parse(Buffer.from(accessParts[1], 'base64').toString());
}
}
if (session.idToken) {
const idParts = session.idToken.split('.');
if (idParts.length >= 2) {
idTokenPayload = JSON.parse(Buffer.from(idParts[1], 'base64').toString());
}
}
} catch (parseError) {
console.warn('⚠️ Failed to parse token payloads:', parseError);
}
const isExpired = Date.now() > session.expiresAt;
console.log("   Is Expired:", isExpired);
console.log(' Is Expired:', isExpired);
// Check if the token has expired
// Check if the session has expired
if (isExpired) {
console.log("⏰ Session has expired, clearing cookie");
deleteCookie(event, "user_session");
console.log('⏰ Session has expired, clearing cookie');
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: "Session expired",
statusMessage: 'Session expired',
});
}
// Decode tokens and prepare the enhanced response data
const idTokenPayload = decodeTokenPayload(session.idToken);
const accessTokenPayload = decodeTokenPayload(session.accessToken);
// Final response object - ensure it matches SessionResponse interface
// Return the full session data (already includes parsed payloads)
const sessionResponse: SessionResponse & {
idTokenPayload?: any
accessTokenPayload?: any
@@ -76,37 +75,55 @@ export default defineEventHandler(async (event) => {
status?: string
remainingSeconds?: number
idToken?: string
scope?: string
createdAt?: number
} = {
success: true,
// Basic User Info
user: session.user,
// Raw Tokens (optional in SessionResponse)
// Raw Tokens
accessToken: session.accessToken,
refreshToken: session.refreshToken,
idToken: session.idToken,
// Session Timestamps (optional in SessionResponse)
// Session Timestamps
expiresAt: session.expiresAt,
createdAt: session.createdAt,
remainingSeconds: Math.max(0, Math.floor((session.expiresAt - Date.now()) / 1000)),
// Additional debug fields (not in SessionResponse interface)
idToken: session.idToken,
// Parsed token payloads (parsed on-demand, not stored in cookie)
idTokenPayload: idTokenPayload,
accessTokenPayload: accessTokenPayload,
// Full session for debugging
fullSessionObject: session,
status: "authenticated",
// OAuth metadata
scope: session.scope,
status: session.status || 'authenticated',
};
console.log("✅ Session is valid, returning full session data");
console.log('✅ Session is valid, returning full session data');
return sessionResponse;
} catch (parseError) {
console.error("❌ Failed to parse session cookie:", parseError);
// If JSON parsing fails or any other error occurs, the session is invalid
deleteCookie(event, "user_session");
} catch (parseError: any) {
console.error('❌ Failed to parse session cookie:', parseError);
// Check if this is an old session ID format (not base64 JSON)
if (parseError.message?.includes('Unexpected token') || parseError.message?.includes('JSON')) {
console.log('🧹 Detected old session format, clearing cookie...');
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: 'Old session format detected. Please login again.',
});
}
// If parsing fails, the session is invalid
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: "Invalid session data",
statusMessage: 'Invalid session data',
});
}
});
// --- END OF THE SINGLE EXPORT DEFAULT HANDLER ---
+7 -8
View File
@@ -1,22 +1,23 @@
// server/api/auth/validate-session.post.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const sessionId = getCookie(event, 'user_session')
const sessionCookie = getCookie(event, 'user_session')
console.log('🔍 Session validation endpoint called')
console.log('🍪 Session cookie exists:', !!sessionId)
console.log('🍪 Session cookie exists:', !!sessionCookie)
if (!sessionId) {
if (!sessionCookie) {
console.log('❌ No session cookie found')
return { valid: false, reason: 'no_session' }
}
try {
const { getSession, deleteSession } = await import('~/server/utils/sessionStore')
const session = getSession(sessionId)
// Decode JWT-based session from cookie
const sessionJson = Buffer.from(sessionCookie, 'base64').toString('utf-8');
const session = JSON.parse(sessionJson);
if (!session) {
console.log('❌ Session not found in store')
console.log('❌ Session not found or invalid')
deleteCookie(event, 'user_session')
return { valid: false, reason: 'session_not_found' }
}
@@ -24,7 +25,6 @@ export default defineEventHandler(async (event) => {
// Check local expiry
if (Date.now() > session.expiresAt) {
console.log('⏰ Session has expired locally')
deleteSession(sessionId)
deleteCookie(event, 'user_session')
return { valid: false, reason: 'session_expired' }
}
@@ -53,7 +53,6 @@ export default defineEventHandler(async (event) => {
// If Keycloak returns 401, the session is invalid on their side
if (keycloakError.status === 401 || keycloakError.statusCode === 401) {
console.log('❌ Keycloak session has expired (401)')
deleteSession(sessionId)
deleteCookie(event, 'user_session')
return { valid: false, reason: 'keycloak_session_expired' }
}
+77
View File
@@ -0,0 +1,77 @@
import { defineEventHandler, createError } from 'h3';
import { getSessionFromCookie } from '~/server/utils/sessionStore';
export default defineEventHandler(async (event) => {
console.log('🌐 Proxy: External token validation requested');
const config = useRuntimeConfig();
try {
// 1. Get current session to retrieve access token
const session = await getSessionFromCookie(event);
if (!session || !session.accessToken) {
console.log('❌ Proxy: No valid session or access token found');
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized: No valid session found',
});
}
const accessToken = session.accessToken;
// Log token details for debugging audience and claims
try {
const payloadPart = accessToken.split('.')[1];
const payload = JSON.parse(Buffer.from(payloadPart, 'base64').toString());
console.log('🎫 Full Token Payload:', JSON.stringify(payload, null, 2));
console.log('🎫 Token Audience (aud):', payload.aud);
} catch (e) {
console.warn('⚠️ Proxy: Failed to parse token for logging');
}
const externalApiUrl = `${config.externalApiBaseUrl}/api/v1/auth/me`;
console.log(`📡 Proxy: Calling external API: ${externalApiUrl}`);
// 2. Call external API with Bearer token
// We use $fetch from ofetch (auto-imported in Nuxt/Nitro)
const response = await $fetch(externalApiUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
},
timeout: config.externalApiTimeout || 10000,
});
console.log('✅ Proxy: External API call successful');
return {
success: true,
data: response,
timestamp: new Date().toISOString()
};
} catch (error: any) {
console.error('❌ Proxy: External API call failed:', error.message);
// Check if it's an HTTP error from the external API
const statusCode = error.response?.status || 500;
const statusText = error.response?.statusText || 'External API Error';
const errorData = error.response?._data || error.data || null;
console.error(` Status: ${statusCode} ${statusText}`);
if (errorData) {
console.error(' Error Data:', JSON.stringify(errorData));
}
return {
success: false,
error: statusText,
statusCode: statusCode,
details: errorData || error.message,
timestamp: new Date().toISOString()
};
}
});