akbar first commit

This commit is contained in:
Fanrouver
2025-10-01 11:28:06 +07:00
parent 55d8920a82
commit d735824d57
35 changed files with 6091 additions and 890 deletions

View File

@@ -0,0 +1,25 @@
// server/api/auth/[...].ts
import { NuxtAuthHandler } from '#auth'
export default NuxtAuthHandler({
secret: useRuntimeConfig().authSecret,
providers: [
{
id: 'keycloak',
name: 'Keycloak',
type: 'oidc',
issuer: useRuntimeConfig().keycloakIssuer,
clientId: useRuntimeConfig().keycloakClientId,
clientSecret: useRuntimeConfig().keycloakClientSecret,
checks: ['pkce', 'state'],
profile(profile) {
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
email: profile.email,
image: profile.picture,
}
},
}
]
})

View File

@@ -0,0 +1,141 @@
// server/api/auth/keycloak-callback.ts - FIX APPLIED
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}`);
}
const tokenUrl = `${config.keycloakIssuer}/protocol/openid-connect/token`;
const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback`;
// ... (Token exchange logic remains the same) ...
const tokenPayload = new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.keycloakClientId,
client_secret: config.keycloakClientSecret,
code,
redirect_uri: redirectUri,
});
const tokenResponse = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: tokenPayload,
});
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();
// ... (Token decoding and sessionData creation remains the same) ...
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,
expiresAt: Date.now() + (tokens.expires_in * 1000),
createdAt: Date.now(),
};
// ----------------------------------------------------
// 👇 CRITICAL FIX FOR DEPLOYED HTTPS ENVIRONMENTS 👇
// ----------------------------------------------------
// Check if the request was originally HTTPS (via proxy)
const isSecure = process.env.NODE_ENV === 'production' ||
event.node.req.headers['x-forwarded-proto'] === 'https';
console.log('🔗 Setting session cookie with secure flag:', isSecure);
setCookie(event, 'user_session', JSON.stringify(sessionData), {
httpOnly: true,
// CRITICAL: Must be TRUE when operating over HTTPS (deployed)
secure: isSecure,
// Ensures cookie is sent on cross-site redirects (Keycloak -> Your App)
sameSite: 'lax',
maxAge: tokens.expires_in,
path: '/',
});
console.log('✅ Session cookie created successfully');
// Note: The following line will still log false because the cookie
// is in the response header, not the request header yet. This is expected.
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}`);
}
});

View File

@@ -0,0 +1,65 @@
// server/api/auth/keycloak-login.ts
import { randomBytes } from 'crypto'
export default defineEventHandler(async (event) => {
console.log('🔐 Keycloak Login Handler Called')
console.log('📍 Method:', getMethod(event))
try {
const config = useRuntimeConfig()
// Debug: Log runtime config (without secrets)
console.log('🔧 Runtime Config Check:')
console.log(' - Has keycloakIssuer:', !!config.keycloakIssuer)
console.log(' - Has keycloakClientId:', !!config.keycloakClientId)
console.log(' - Has keycloakSecret:', !!config.keycloakClientSecret)
console.log(' - Issuer value:', config.keycloakIssuer)
// Validate required configuration
if (!config.keycloakIssuer) {
throw new Error('KEYCLOAK_ISSUER is not configured')
}
if (!config.keycloakClientId) {
throw new Error('KEYCLOAK_CLIENT_ID is not configured')
}
// Generate state parameter for security
const state = randomBytes(32).toString('hex')
console.log('🎲 Generated state:', state.substring(0, 8) + '...')
// Store state in session cookie
setCookie(event, 'oauth_state', state, {
httpOnly: true,
secure: false,
sameSite: 'lax',
maxAge: 600 // 10 minutes
})
// Build Keycloak authorization URL
const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback`
const authUrl = new URL(`${config.keycloakIssuer}/protocol/openid-connect/auth`)
authUrl.searchParams.set('client_id', config.keycloakClientId)
authUrl.searchParams.set('redirect_uri', redirectUri)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', 'openid profile email')
authUrl.searchParams.set('state', state)
console.log('🏗️ Auth URL built:', authUrl.toString())
return {
success: true,
data: {
authUrl: authUrl.toString()
}
}
} catch (error: any) {
console.error('❌ Login Error:', error.message)
throw createError({
statusCode: 500,
statusMessage: `Failed to generate authorization URL: ${error.message}`
})
}
})

View File

@@ -0,0 +1,74 @@
// server/api/auth/logout.post.ts
export default defineEventHandler(async (event) => {
try {
const config = useRuntimeConfig();
console.log('🚪 Logout handler called');
// Get the current session to retrieve tokens
const sessionCookie = getCookie(event, 'user_session');
let idToken = null;
if (sessionCookie) {
try {
const session = JSON.parse(sessionCookie);
idToken = session.idToken;
console.log('🔑 ID token found in session:', !!idToken);
} catch (error) {
console.warn('⚠️ Could not parse session cookie:', error);
}
} else {
console.warn('⚠️ No session cookie found');
}
// Clear all auth-related cookies
console.log('🧹 Clearing session cookies...');
deleteCookie(event, 'user_session');
deleteCookie(event, 'oauth_state');
// Also clear with different path variations to be thorough
deleteCookie(event, 'user_session', { path: '/' });
deleteCookie(event, 'oauth_state', { path: '/' });
console.log('✅ Session cleared successfully');
// Construct the Keycloak logout URL with proper parameters
const logoutUrl = new URL(`${config.keycloakIssuer}/protocol/openid-connect/logout`);
// Add required parameters for proper Keycloak logout - REDIRECT TO LOGIN PAGE
logoutUrl.searchParams.set('client_id', config.keycloakClientId);
logoutUrl.searchParams.set('post_logout_redirect_uri', `${config.public.authUrl}/LoginPage?logout=success`);
// If we have an ID token, add it for proper session termination
if (idToken) {
logoutUrl.searchParams.set('id_token_hint', idToken);
console.log('🔑 Added id_token_hint to logout URL');
} else {
console.warn('⚠️ No ID token available for logout hint');
}
console.log('🔗 Keycloak logout URL constructed:', logoutUrl.toString());
// Return the logout URL to the client for redirect
// This approach gives better control to the client-side code
return {
success: true,
logoutUrl: logoutUrl.toString(),
message: 'Session cleared successfully'
};
} catch (error: any) {
console.error('❌ Logout error:', error);
console.error('❌ Error stack:', error.stack);
// Even if there's an error, try to provide a basic logout URL - REDIRECT TO LOGIN PAGE
const config = useRuntimeConfig();
const fallbackLogoutUrl = `${config.keycloakIssuer}/protocol/openid-connect/logout?client_id=${config.keycloakClientId}&post_logout_redirect_uri=${encodeURIComponent(config.public.authUrl + '/LoginPage?logout=success')}`;
return {
success: false,
logoutUrl: fallbackLogoutUrl,
error: 'Logout encountered an error, but providing fallback logout URL',
message: error.message
};
}
});

View File

@@ -0,0 +1,95 @@
// server/api/auth/session.get.ts
// 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');
const sessionCookie = getCookie(event, 'user_session');
console.log('🍪 Session cookie exists:', !!sessionCookie);
if (!sessionCookie) {
console.log('❌ No session cookie found');
throw createError({
statusCode: 401,
statusMessage: 'No session cookie found'
});
}
try {
const session = JSON.parse(sessionCookie);
console.log('📋 Session parsed successfully');
const isExpired = Date.now() > session.expiresAt;
console.log('   Is Expired:', isExpired);
// Check if the token has expired
if (isExpired) {
console.log('⏰ Session has expired, clearing cookie');
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: 'Session expired'
});
}
// Decode tokens and prepare the enhanced response data
const idTokenPayload = decodeTokenPayload(session.idToken);
const accessTokenPayload = decodeTokenPayload(session.accessToken);
// Final response object for the frontend debug page
const sessionResponse = {
// Basic User Info
user: session.user,
// Raw Tokens
idToken: session.idToken,
accessToken: session.accessToken,
refreshToken: session.refreshToken,
// Session Timestamps
expiresAt: session.expiresAt,
createdAt: session.createdAt,
// Parsed Payloads
idTokenPayload: idTokenPayload,
accessTokenPayload: accessTokenPayload,
// Raw Session Data (for Debug section)
fullSessionObject: session,
status: 'authenticated',
};
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');
throw createError({
statusCode: 401,
statusMessage: 'Invalid session data'
});
}
});
// --- END OF THE SINGLE EXPORT DEFAULT HANDLER ---

View File