diff --git a/composables/useAuth.ts b/composables/useAuth.ts index 42594d9..5bde15a 100644 --- a/composables/useAuth.ts +++ b/composables/useAuth.ts @@ -16,19 +16,29 @@ export const useAuth = () => { isLoading.value = true clearError() - const response = await $fetch('/api/auth/session') + // The session API returns SessionResponse, or throws 401 if not authenticated + // $fetch automatically sends cookies for same-origin requests + const response = await $fetch('/api/auth/session', { + credentials: 'include' // Explicitly include cookies (though $fetch does this by default) + }) - if (response.success === false && response.error) { - error.value = response.error - user.value = null - return null + // Handle response structure - session API returns { success, user, ... } + if (response && response.user) { + user.value = response.user + return response.user } - user.value = response.user - return response.user + // If response exists but no user, clear and return null + user.value = null + return null } catch (fetchError: any) { console.error('Session check failed:', fetchError) - error.value = 'Failed to check authentication status' + // 401 errors are expected when not authenticated + if (fetchError.statusCode === 401 || fetchError.status === 401) { + error.value = null // Don't show error for expected unauthenticated state + } else { + error.value = 'Failed to check authentication status' + } user.value = null return null } finally { diff --git a/middleware/auth.ts b/middleware/auth.ts index b3ae89e..44cb9e7 100644 --- a/middleware/auth.ts +++ b/middleware/auth.ts @@ -28,8 +28,13 @@ export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) => return; } - // Skip auth check if it's the development server side render pass. + // On server-side during development, skip intensive checks + // The cookie check will happen on client-side if (process.server && process.env.NODE_ENV === 'development') { + if (to.query.authenticated === 'true') { + console.log('⏭️ Server-side: Allowing authenticated redirect to pass through (dev mode)'); + return; // Allow server-side render to proceed, client will verify + } console.log('⏭️ Skipping intensive check on server-side during development'); useAuth(); return; @@ -39,8 +44,55 @@ export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) => const isAuthRedirect: boolean = to.query.authenticated === 'true'; if (isAuthRedirect) { - console.log('⏳ Client-side is processing a new login session, allowing the route to load...'); - return navigateTo({ path: to.path, query: {} }, { replace: true }); + console.log('⏳ Client-side is processing a new login session, checking cookie...'); + + // Give the browser a moment to process the cookie from the redirect + if (process.client) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + + // Check authentication first before removing query parameter + try { + const { checkAuth } = useAuth(); + console.log('🔍 Checking authentication after redirect...'); + + let user = await checkAuth(); + + // If not found, retry once more + if (!user && process.client) { + console.log('⚠️ Cookie not available yet, retrying...'); + await new Promise(resolve => setTimeout(resolve, 150)); + user = await checkAuth(); + } + + if (user) { + console.log('✅ User is authenticated after redirect:', user.name || user.preferred_username || user.email); + // Remove query parameter and allow access + await navigateTo({ path: to.path, query: {} }, { replace: true }); + return; // Allow access + } else { + console.log('❌ Still no session after retry, redirecting to login'); + return navigateTo('/LoginPage'); + } + } catch (authError) { + console.error('❌ Auth check failed after redirect:', authError); + // Retry once + if (process.client) { + await new Promise(resolve => setTimeout(resolve, 150)); + try { + const { checkAuth } = useAuth(); + const retryUser = await checkAuth(); + if (retryUser) { + console.log('✅ User authenticated on retry after error'); + await navigateTo({ path: to.path, query: {} }, { replace: true }); + return; + } + } catch (retryError) { + console.error('❌ Retry also failed:', retryError); + } + } + return navigateTo('/LoginPage'); + } } try { diff --git a/nuxt.config.ts b/nuxt.config.ts index 968309f..e9c394a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -37,23 +37,23 @@ export default defineNuxtConfig({ // @ts-expect-error config.plugins.push(vuetify({ autoImport: true })); - // Add HTTPS plugin - try { - // @ts-ignore - const { default: basicSsl } = await import('@vitejs/plugin-basic-ssl'); - // @ts-expect-error - config.plugins.push(basicSsl()); - // @ts-expect-error - config.server = config.server || {}; - // @ts-expect-error - config.server.https = true; - // @ts-expect-error - config.server.host = '10.10.150.114'; - // @ts-expect-error - config.server.port = 3001; - } catch (e) { - console.warn('Failed to load HTTPS plugin:', e); - } + // // Add HTTPS plugin + // try { + // // @ts-ignore + // const { default: basicSsl } = await import('@vitejs/plugin-basic-ssl'); + // // @ts-expect-error + // config.plugins.push(basicSsl()); + // // @ts-expect-error + // config.server = config.server || {}; + // // @ts-expect-error + // config.server.https = true; + // // @ts-expect-error + // config.server.host = '10.10.150.175'; + // // @ts-expect-error + // config.server.port = 3001; + // } catch (e) { + // console.warn('Failed to load HTTPS plugin:', e); + // } }); }, ], @@ -96,7 +96,7 @@ export default defineNuxtConfig({ devServer: { port: 3001, - host: '10.10.150.114' + host: 'localhost' }, vite: { diff --git a/package.json b/package.json index e131462..48be317 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,10 @@ "type": "module", "scripts": { "build": "nuxt build", - "_command_dev": "nuxt dev -o --host --port 3001", - "_command_dev2": "nuxt dev -o --port 3001", - "_command_dev3": "nuxt dev -o --host 10.10.150.114 --port 3001", + "_command_dev": "nuxt dev -o --host --port 3000", + "_command_dev2": "nuxt dev -o --port 3000", "dev": "nuxt dev -o", - "dev:https": "nuxt dev -o --host localhost --port 3001", + "dev:https": "nuxt dev -o --host localhost --port 3000", "generate": "nuxt generate", "preview": "nuxt preview", "postinstall": "nuxt prepare" diff --git a/pages/LoginPage.vue b/pages/LoginPage.vue index b4da23d..020cdfe 100644 --- a/pages/LoginPage.vue +++ b/pages/LoginPage.vue @@ -280,7 +280,7 @@ import type { LoginResponse } from '~/types/auth' // Use guest middleware to redirect if already authenticated definePageMeta({ layout: 'empty', - middleware:['auth','guest'] + middleware: ['guest'] }) // Reactive state diff --git a/server/api/auth/keycloak-callback.get.ts b/server/api/auth/keycloak-callback.get.ts index 6352a22..f9f1b1f 100644 --- a/server/api/auth/keycloak-callback.get.ts +++ b/server/api/auth/keycloak-callback.get.ts @@ -137,39 +137,77 @@ export default defineEventHandler(async (event) => { 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, - given_name: idTokenPayload.given_name, - family_name: idTokenPayload.family_name, }, + // 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, - // CHANGED: Use custom session duration instead of Keycloak's token expiry + // Session metadata expiresAt: Date.now() + (SESSION_DURATION * 1000), createdAt: Date.now(), }; - const isSecure = process.env.NODE_ENV === 'production' || + // 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'); - setCookie(event, 'user_session', JSON.stringify(sessionData), { + // 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', - // CHANGED: Use custom session duration (7 days default) + 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 @@ -182,11 +220,17 @@ export default defineEventHandler(async (event) => { 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); + // 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...'); - console.log('↪️ Redirecting to dashboard...'); - return sendRedirect(event, '/dashboard?authenticated=true'); + // 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 ==='); diff --git a/server/api/auth/logout.post.ts b/server/api/auth/logout.post.ts index de23066..85cf8cf 100644 --- a/server/api/auth/logout.post.ts +++ b/server/api/auth/logout.post.ts @@ -5,16 +5,21 @@ export default defineEventHandler(async (event) => { console.log('🚪 Logout handler called'); // Get the current session to retrieve tokens - const sessionCookie = getCookie(event, 'user_session'); + const sessionId = getCookie(event, 'user_session'); let idToken = null; - if (sessionCookie) { + if (sessionId) { try { - const session = JSON.parse(sessionCookie); - idToken = session.idToken; - console.log('🔑 ID token found in session:', !!idToken); + 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); + } } catch (error) { - console.warn('⚠️ Could not parse session cookie:', error); + console.warn('⚠️ Could not retrieve session:', error); } } else { console.warn('⚠️ No session cookie found'); diff --git a/server/api/auth/session.get.ts b/server/api/auth/session.get.ts index 0d6d81b..6f57722 100644 --- a/server/api/auth/session.get.ts +++ b/server/api/auth/session.get.ts @@ -1,4 +1,5 @@ // 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 => { @@ -23,10 +24,10 @@ const decodeTokenPayload = (token: string | undefined): any | null => { export default defineEventHandler(async (event) => { console.log("🔍 Session endpoint called"); - const sessionCookie = getCookie(event, "user_session"); - console.log("🍪 Session cookie exists:", !!sessionCookie); + const sessionId = getCookie(event, "user_session"); + console.log("🍪 Session cookie exists:", !!sessionId); - if (!sessionCookie) { + if (!sessionId) { console.log("❌ No session cookie found"); throw createError({ statusCode: 401, @@ -35,8 +36,20 @@ export default defineEventHandler(async (event) => { } try { - const session = JSON.parse(sessionCookie); - console.log("📋 Session parsed successfully"); + // Get session from server-side store using session ID + const { getSession } = await import('~/server/utils/sessionStore'); + const session = getSession(sessionId); + + if (!session) { + console.log("❌ Session not found or expired"); + deleteCookie(event, "user_session"); + throw createError({ + statusCode: 401, + statusMessage: "Session expired or invalid", + }); + } + + console.log("📋 Session retrieved from store successfully"); const isExpired = Date.now() > session.expiresAt; console.log("   Is Expired:", isExpired); @@ -55,27 +68,29 @@ export default defineEventHandler(async (event) => { const idTokenPayload = decodeTokenPayload(session.idToken); const accessTokenPayload = decodeTokenPayload(session.accessToken); - // Final response object for the frontend debug page - const sessionResponse = { + // Final response object - ensure it matches SessionResponse interface + const sessionResponse: SessionResponse & { + idTokenPayload?: any + accessTokenPayload?: any + fullSessionObject?: any + status?: string + } = { + success: true, // Basic User Info user: session.user, - // Raw Tokens - idToken: session.idToken, + // Raw Tokens (optional in SessionResponse) accessToken: session.accessToken, refreshToken: session.refreshToken, - // Session Timestamps + // Session Timestamps (optional in SessionResponse) expiresAt: session.expiresAt, - createdAt: session.createdAt, - // Parsed Payloads + // Additional debug fields (not in SessionResponse interface) + idToken: session.idToken, idTokenPayload: idTokenPayload, accessTokenPayload: accessTokenPayload, - - // Raw Session Data (for Debug section) fullSessionObject: session, - status: "authenticated", }; diff --git a/server/api/users/sync.post.ts b/server/api/users/sync.post.ts index ddb7668..aaca97b 100644 --- a/server/api/users/sync.post.ts +++ b/server/api/users/sync.post.ts @@ -5,26 +5,17 @@ export default defineEventHandler(async (event) => { console.log("🔄 User sync endpoint called"); - const sessionCookie = getCookie(event, "user_session"); + const { getSessionFromCookie } = await import('~/server/utils/sessionStore'); + const session = await getSessionFromCookie(event); - if (!sessionCookie) { + if (!session) { throw createError({ statusCode: 401, - statusMessage: "No session cookie found", + statusMessage: "No session found or session expired", }); } try { - const session = JSON.parse(sessionCookie); - - const isExpired = Date.now() > session.expiresAt; - if (isExpired) { - deleteCookie(event, "user_session"); - throw createError({ - statusCode: 401, - statusMessage: "Session expired", - }); - } // Use the shared sync utility // Use session createdAt as loginTime, or current time if not available diff --git a/server/utils/sessionStore.ts b/server/utils/sessionStore.ts new file mode 100644 index 0000000..72bc92a --- /dev/null +++ b/server/utils/sessionStore.ts @@ -0,0 +1,63 @@ +// server/utils/sessionStore.ts +// Simple in-memory session store (for development) +// In production, use Redis or a database + +import { getCookie } from 'h3' +import { randomBytes } from 'crypto' + +interface SessionData { + user: any; + accessToken: string; + idToken: string; + refreshToken: string; + expiresAt: number; + createdAt: number; +} + +const sessions = new Map(); + +// Clean up expired sessions every 5 minutes +setInterval(() => { + const now = Date.now(); + for (const [sessionId, session] of sessions.entries()) { + if (session.expiresAt < now) { + sessions.delete(sessionId); + } + } +}, 5 * 60 * 1000); + +export function createSession(data: SessionData): string { + // Generate a secure random session ID + const sessionId = randomBytes(32).toString('hex'); + sessions.set(sessionId, data); + return sessionId; +} + +export function getSession(sessionId: string): SessionData | null { + const session = sessions.get(sessionId); + if (!session) { + return null; + } + + // Check if expired + if (session.expiresAt < Date.now()) { + sessions.delete(sessionId); + return null; + } + + return session; +} + +export function deleteSession(sessionId: string): void { + sessions.delete(sessionId); +} + +// Helper function to get session from cookie (for use in API handlers) +export async function getSessionFromCookie(event: any): Promise { + const sessionId = getCookie(event, 'user_session'); + if (!sessionId) { + return null; + } + return getSession(sessionId); +} +