200 lines
9.0 KiB
TypeScript
200 lines
9.0 KiB
TypeScript
// server/api/auth/keycloak-callback.ts - EXTENDED SESSION FIX
|
|
|
|
// Add this at the top of the file (after imports)
|
|
const SESSION_DURATION = 1 * 60 * 60; // 7 days in seconds (customize as needed)
|
|
// Or use one of these alternatives:
|
|
// const SESSION_DURATION = 24 * 60 * 60; // 1 day
|
|
// const SESSION_DURATION = 30 * 24 * 60 * 60; // 30 days
|
|
// const SESSION_DURATION = 12 * 60 * 60; // 12 hours
|
|
|
|
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();
|
|
|
|
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}`);
|
|
}
|
|
|
|
const sessionData = {
|
|
user: {
|
|
id: idTokenPayload.sub,
|
|
email: idTokenPayload.email,
|
|
name: idTokenPayload.name || idTokenPayload.preferred_username,
|
|
preferred_username: idTokenPayload.preferred_username,
|
|
given_name: idTokenPayload.given_name,
|
|
family_name: idTokenPayload.family_name,
|
|
},
|
|
accessToken: tokens.access_token,
|
|
idToken: tokens.id_token,
|
|
refreshToken: tokens.refresh_token,
|
|
// CHANGED: Use custom session duration instead of Keycloak's token expiry
|
|
expiresAt: Date.now() + (SESSION_DURATION * 1000),
|
|
createdAt: Date.now(),
|
|
};
|
|
|
|
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');
|
|
|
|
setCookie(event, 'user_session', JSON.stringify(sessionData), {
|
|
httpOnly: true,
|
|
secure: isSecure,
|
|
sameSite: 'lax',
|
|
// CHANGED: Use custom session duration (7 days default)
|
|
maxAge: SESSION_DURATION,
|
|
path: '/',
|
|
});
|
|
|
|
console.log('✅ Session cookie created successfully');
|
|
|
|
// 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);
|
|
}
|
|
|
|
const testCookie = getCookie(event, 'user_session');
|
|
console.log('🧪 Cookie test - can read back in this handler (Expected False):', !!testCookie);
|
|
|
|
console.log('↪️ Redirecting to dashboard...');
|
|
return sendRedirect(event, '/dashboard?authenticated=true');
|
|
|
|
} 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}`);
|
|
}
|
|
}); |