From 01f7a0fd2aeeee8448a3579d781dbc44882308ca Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 17 Oct 2025 04:09:28 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20implementar=20verificaci=C3=B3n=20correc?= =?UTF-8?q?ta=20de=20sesi=C3=B3n=20con=20Authentik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problema: La verificación de sesión usaba HEAD /api/music que no es confiable porque: - Los headers de Authentik solo están disponibles en el servidor - El Service Worker puede servir respuestas cacheadas - No había headers no-cache para evitar respuestas obsoletas Solución: Implementar verificación correcta basada en plantillaNuxtAuthentikProxy: 1. Nuevo endpoint /api/auth/status.get.ts - Lee headers de Authentik directamente desde el servidor - Headers no-cache para verificación en tiempo real - Retorna estado autenticado + info de usuario 2. Actualizar useAuth.ts - checkAuth() ahora usa /api/auth/status - fetchUserInfo() también usa el nuevo endpoint - Lógica simplificada y más confiable Con esto, la verificación de sesión es precisa y en tiempo real, consultando directamente los headers de Authentik en el servidor. --- composables/useAuth.ts | 65 ++++++++++++++--------------------- server/api/auth/status.get.ts | 43 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 server/api/auth/status.get.ts diff --git a/composables/useAuth.ts b/composables/useAuth.ts index ce10e0e..a1baa76 100644 --- a/composables/useAuth.ts +++ b/composables/useAuth.ts @@ -16,7 +16,7 @@ let focusListener: (() => void) | null = null export const useAuth = () => { const checkAuth = async (force = false) => { // Si ya sabemos que NO estamos autenticados, no hacer más requests - // (evita loops de CORS con Authentik redirects) + // (evita loops innecesarios) if (!isAuthenticated.value && !force) { console.log('[Auth] Skipping check - already known to be unauthenticated') return @@ -32,55 +32,38 @@ export const useAuth = () => { isCheckingAuth.value = true try { - // Intenta hacer fetch a un endpoint protegido - const response = await fetch('/api/music', { - method: 'HEAD', // Solo headers, no body - credentials: 'include', // Include cookies for auth - cache: 'no-store', // No cachear la respuesta - redirect: 'error', // No seguir redirects (error si Authentik intenta redirigir) - signal: AbortSignal.timeout(5000) // 5s timeout + // Usar endpoint dedicado que lee headers de Authentik desde el servidor + // Este endpoint tiene headers no-cache y verifica la sesión en tiempo real + const response = await $fetch<{ authenticated: boolean; user: any | null; timestamp: string }>('/api/auth/status', { + // No usar redirect: 'error' aquí porque queremos la respuesta del servidor + // El endpoint responde con authenticated: false si no hay sesión }) const wasAuthenticated = isAuthenticated.value - isAuthenticated.value = response.ok + isAuthenticated.value = response.authenticated authChecked.value = true lastCheckTime.value = now // Log cambios de estado if (wasAuthenticated !== isAuthenticated.value) { console.log('[Auth] Status changed:', isAuthenticated.value ? 'authenticated' : 'unauthenticated') + if (response.user) { + console.log('[Auth] User:', response.user.username) + } } } catch (error: any) { - // Con redirect: 'error', cualquier redirect (302) genera "Failed to fetch" - // Authentik redirige a login cuando no estás autenticado = 302 - console.warn('[Auth] Check failed:', error.message) + console.warn('[Auth] Check failed:', error.message || error) const wasAuthenticated = isAuthenticated.value - // "Failed to fetch" con redirect: 'error' configurado = sesión expirada - // Authentik intenta redirigir (302) pero fetch lo rechaza y genera error - if (error.message && error.message.includes('Failed to fetch')) { - console.log('[Auth] Failed to fetch detected - marking as unauthenticated') - isAuthenticated.value = false - authChecked.value = true - if (wasAuthenticated) { - console.log('[Auth] Status changed: authenticated → unauthenticated (Authentik redirect detected)') - } else { - console.log('[Auth] Confirmed unauthenticated state') - } - } else if (error.name === 'TypeError' && error.message.includes('fetch')) { - // Otro tipo de TypeError fetch = también probablemente redirect - console.log('[Auth] TypeError fetch detected - marking as unauthenticated') - isAuthenticated.value = false - authChecked.value = true - if (wasAuthenticated) { - console.log('[Auth] Status changed: authenticated → unauthenticated (fetch error)') - } else { - console.log('[Auth] Confirmed unauthenticated state') - } - } else { - // Otro tipo de error (timeout, etc) - mantener estado actual - console.log('[Auth] Network error (not auth related), maintaining current state:', error.name) + // Si el fetch falla completamente (CORS, redirect, etc), asumir no autenticado + // Esto pasa cuando Authentik redirige antes de que el endpoint pueda responder + console.log('[Auth] Fetch failed - marking as unauthenticated') + isAuthenticated.value = false + authChecked.value = true + + if (wasAuthenticated) { + console.log('[Auth] Status changed: authenticated → unauthenticated') } lastCheckTime.value = now @@ -116,9 +99,13 @@ export const useAuth = () => { } try { - const info = await $fetch('/api/auth/userinfo') - userInfo.value = info - console.log('[Auth] User info loaded:', info) + const response = await $fetch<{ authenticated: boolean; user: any | null }>('/api/auth/status') + if (response.authenticated && response.user) { + userInfo.value = response.user + console.log('[Auth] User info loaded:', response.user) + } else { + userInfo.value = null + } } catch (error) { console.warn('[Auth] Failed to fetch user info:', error) userInfo.value = null diff --git a/server/api/auth/status.get.ts b/server/api/auth/status.get.ts new file mode 100644 index 0000000..f892a47 --- /dev/null +++ b/server/api/auth/status.get.ts @@ -0,0 +1,43 @@ +/** + * API endpoint para verificar el estado de autenticación en tiempo real + * Consulta los headers inyectados por Authentik Proxy Outpost + */ +export default defineEventHandler((event) => { + // Establecer headers para prevenir caching + setResponseHeaders(event, { + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }) + + // Leer headers de Authentik en tiempo real + const headers = getHeaders(event) + + const username = headers['x-authentik-username'] + const email = headers['x-authentik-email'] + const name = headers['x-authentik-name'] + const groups = headers['x-authentik-groups'] + const uid = headers['x-authentik-uid'] + + // Si no hay username, no hay sesión activa en Authentik + if (!username) { + return { + authenticated: false, + user: null, + timestamp: new Date().toISOString() + } + } + + // Sesión activa + return { + authenticated: true, + user: { + username, + email, + name, + groups: groups ? groups.split('|') : [], + uid + }, + timestamp: new Date().toISOString() + } +})