diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..c18fe77
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,799 @@
+# TODO - OAuth2/OIDC Improvements
+
+Este documento lista las funcionalidades OAuth2/OIDC que faltan en el template actual y que podrían implementarse en futuras versiones.
+
+## 🔴 Críticas (Seguridad)
+
+### 1. State Parameter (CSRF Protection)
+**Estado:** ❌ No implementado
+**Prioridad:** ALTA
+**Riesgo:** Vulnerable a ataques CSRF
+
+**Descripción:**
+El parámetro `state` es un token aleatorio que se envía en la URL de autorización y debe ser verificado en el callback para prevenir ataques CSRF.
+
+**Implementación sugerida:**
+```typescript
+// En authentik.get.ts - antes del redirect
+const state = generateRandomString(32)
+await setUserSession(event, { oauthState: state })
+
+const authorizationUrl = withQuery(
+ `${config.serverUrl}/application/o/authorize/`,
+ {
+ client_id: config.clientId,
+ redirect_uri: config.redirectURL,
+ response_type: 'code',
+ scope: config.scope.join(' '),
+ state: state // Agregar state
+ }
+)
+
+// En callback - verificar state
+if (query.state !== session.oauthState) {
+ throw new Error('Invalid state parameter')
+}
+```
+
+**Referencias:**
+- [RFC 6749 - Section 10.12](https://tools.ietf.org/html/rfc6749#section-10.12)
+- [OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
+
+---
+
+### 2. PKCE (Proof Key for Code Exchange)
+**Estado:** ❌ No implementado
+**Prioridad:** MEDIA
+**Beneficio:** Protección adicional contra intercepción del authorization code
+
+**Descripción:**
+PKCE es una extensión de OAuth 2.0 que protege contra ataques de intercepción del código de autorización. Es especialmente recomendado para aplicaciones públicas.
+
+**Implementación sugerida:**
+```typescript
+// Generar code_verifier y code_challenge
+import { createHash, randomBytes } from 'crypto'
+
+const codeVerifier = randomBytes(32).toString('base64url')
+const codeChallenge = createHash('sha256')
+ .update(codeVerifier)
+ .digest('base64url')
+
+// Guardar code_verifier en sesión
+await setUserSession(event, { codeVerifier })
+
+// Agregar a URL de autorización
+const authorizationUrl = withQuery(url, {
+ // ... otros params
+ code_challenge: codeChallenge,
+ code_challenge_method: 'S256'
+})
+
+// En token exchange, agregar code_verifier
+body: new URLSearchParams({
+ // ... otros params
+ code_verifier: session.codeVerifier
+})
+```
+
+**Referencias:**
+- [RFC 7636 - PKCE](https://tools.ietf.org/html/rfc7636)
+- [OAuth 2.1 (draft) - PKCE obligatorio](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07)
+
+---
+
+## 🟡 Importantes (Funcionalidad)
+
+### 3. Refresh Tokens
+**Estado:** ❌ No implementado
+**Prioridad:** ALTA
+**Problema:** Usuario debe re-autenticarse cuando expira el access token
+
+**Descripción:**
+Actualmente no guardamos ni usamos refresh tokens, lo que significa que cuando el access token expira, el usuario debe volver a autenticarse completamente.
+
+**Implementación sugerida:**
+```typescript
+// Guardar tokens en sesión
+await setUserSession(event, {
+ user: { ... },
+ accessToken: tokens.access_token,
+ refreshToken: tokens.refresh_token,
+ expiresAt: Date.now() + (tokens.expires_in * 1000),
+ loggedInAt: Date.now()
+})
+
+// Crear endpoint para refresh
+// server/api/auth/refresh.post.ts
+export default defineEventHandler(async (event) => {
+ const session = await getUserSession(event)
+
+ const tokenResponse = await $fetch(tokenUrl, {
+ method: 'POST',
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token: session.refreshToken,
+ client_id: config.clientId,
+ client_secret: config.clientSecret,
+ })
+ })
+
+ // Actualizar sesión con nuevos tokens
+ await setUserSession(event, {
+ ...session,
+ accessToken: tokenResponse.access_token,
+ refreshToken: tokenResponse.refresh_token || session.refreshToken,
+ expiresAt: Date.now() + (tokenResponse.expires_in * 1000)
+ })
+
+ return { success: true }
+})
+```
+
+**Consideraciones:**
+- Implementar refresh automático en cliente antes de que expire
+- Manejar caso cuando refresh token también expira
+- Considerar refresh token rotation para mayor seguridad
+
+---
+
+### 4. Token Storage & Expiration Handling
+**Estado:** ⚠️ Parcialmente implementado
+**Prioridad:** ALTA
+**Problema:** Solo guardamos info del usuario, no los tokens ni su expiración
+
+**Descripción:**
+Actualmente solo extraemos la información del usuario y descartamos los tokens. Necesitamos guardarlos para poder renovarlos y detectar cuándo expiran.
+
+**Implementación sugerida:**
+```typescript
+// Extender UserSession interface
+declare module '#auth-utils' {
+ interface UserSession {
+ user: User
+ accessToken: string
+ refreshToken?: string
+ idToken?: string
+ expiresAt: number
+ loggedInAt: number
+ }
+}
+
+// Guardar todos los tokens
+await setUserSession(event, {
+ user: { ... },
+ accessToken: tokens.access_token,
+ refreshToken: tokens.refresh_token,
+ idToken: tokens.id_token,
+ expiresAt: Date.now() + (tokens.expires_in * 1000),
+ loggedInAt: Date.now()
+})
+
+// Middleware para verificar expiración
+export default defineNuxtRouteMiddleware(async (to, from) => {
+ const { loggedIn } = useUserSession()
+ const session = await $fetch('/api/auth/session')
+
+ if (loggedIn.value && session.expiresAt < Date.now()) {
+ // Token expirado, intentar refresh
+ try {
+ await $fetch('/api/auth/refresh', { method: 'POST' })
+ } catch {
+ // Refresh falló, redirigir a login
+ return navigateTo('/login')
+ }
+ }
+})
+```
+
+---
+
+### 5. Single Sign-Out (SSO Logout)
+**Estado:** ❌ No implementado
+**Prioridad:** MEDIA
+**Problema:** Solo cerramos sesión local, no en Authentik
+
+**Descripción:**
+Actualmente al cerrar sesión solo limpiamos la sesión local, pero el usuario sigue autenticado en Authentik. Al volver a iniciar sesión, entra automáticamente sin pedir credenciales.
+
+**Implementación sugerida:**
+```typescript
+// server/api/auth/logout.get.ts
+export default defineEventHandler(async (event) => {
+ const session = await getUserSession(event)
+ const runtimeConfig = useRuntimeConfig(event)
+
+ // Limpiar sesión local
+ await clearUserSession(event)
+
+ // Redirigir a logout de Authentik
+ const logoutUrl = withQuery(
+ `${runtimeConfig.oauth.authentik.serverUrl}/application/o/end-session/`,
+ {
+ id_token_hint: session.idToken,
+ post_logout_redirect_uri: runtimeConfig.public.appUrl
+ }
+ )
+
+ return sendRedirect(event, logoutUrl)
+})
+```
+
+**Consideraciones:**
+- Requiere guardar el `id_token` en la sesión
+- Authentik debe tener configurada la URL de post-logout redirect
+- Opcionalmente agregar parámetro `state` para validar el retorno
+
+---
+
+### 6. Redirect After Login
+**Estado:** ⚠️ Parcialmente implementado
+**Prioridad:** MEDIA
+**Problema:** No recordamos a dónde quería ir el usuario antes del login
+
+**Descripción:**
+Cuando el middleware redirige al login, incluye el parámetro `redirect` en la URL, pero el flujo OAuth no lo preserva. El usuario siempre termina en `/` después de autenticarse.
+
+**Implementación sugerida:**
+```typescript
+// En login.vue - capturar redirect del query
+const route = useRoute()
+const redirectUrl = route.query.redirect || '/'
+
+const login = () => {
+ // Guardar en sessionStorage
+ if (process.client) {
+ sessionStorage.setItem('auth_redirect', redirectUrl)
+ }
+ navigateTo('/api/auth/authentik')
+}
+
+// En server/api/auth/authentik.get.ts - después de crear sesión
+export default defineEventHandler(async (event) => {
+ // ... código existente ...
+
+ await setUserSession(event, { ... })
+
+ // Enviar HTML con script para leer sessionStorage
+ return `
+
+
+
Redirecting...
+
+
+
+
+ `
+})
+```
+
+**Alternativa usando cookies:**
+```typescript
+// Guardar en cookie en lugar de sessionStorage
+setCookie(event, 'auth_redirect', redirectUrl, {
+ httpOnly: true,
+ secure: true,
+ maxAge: 300 // 5 minutos
+})
+
+// Leer en callback
+const redirectUrl = getCookie(event, 'auth_redirect') || '/'
+deleteCookie(event, 'auth_redirect')
+return sendRedirect(event, redirectUrl)
+```
+
+---
+
+## 🟢 Nice to Have (UX)
+
+### 7. Error Messages Específicos
+**Estado:** ⚠️ Implementación básica
+**Prioridad:** BAJA
+**Problema:** Solo tenemos `/?error=auth_failed` genérico
+
+**Descripción:**
+Actualmente todos los errores muestran el mismo mensaje. Sería mejor tener mensajes específicos para cada tipo de error.
+
+**Implementación sugerida:**
+```typescript
+// En server/api/auth/authentik.get.ts
+try {
+ // ... token exchange ...
+} catch (error: any) {
+ console.error('Authentik OAuth error:', error)
+
+ let errorCode = 'auth_failed'
+
+ if (error.statusCode === 401) {
+ errorCode = 'invalid_credentials'
+ } else if (error.statusCode === 403) {
+ errorCode = 'access_denied'
+ } else if (error.code === 'ETIMEDOUT') {
+ errorCode = 'server_timeout'
+ } else if (error.code === 'ECONNREFUSED') {
+ errorCode = 'server_unavailable'
+ }
+
+ return sendRedirect(event, `/?error=${errorCode}`)
+}
+
+// En login.vue - mostrar mensajes apropiados
+const errorMessages = {
+ auth_failed: 'Error de autenticación. Por favor intenta nuevamente.',
+ invalid_credentials: 'Credenciales inválidas.',
+ access_denied: 'Acceso denegado. No tienes permisos para acceder.',
+ server_timeout: 'Timeout del servidor. Por favor intenta más tarde.',
+ server_unavailable: 'Servidor no disponible. Por favor intenta más tarde.'
+}
+
+const route = useRoute()
+const errorMessage = computed(() => {
+ const error = route.query.error as string
+ return error ? errorMessages[error] || errorMessages.auth_failed : null
+})
+```
+
+---
+
+### 8. Loading States
+**Estado:** ❌ No implementado
+**Prioridad:** BAJA
+**Problema:** No hay feedback visual durante el callback OAuth
+
+**Descripción:**
+Durante el callback OAuth (cuando hay `?code=...` en la URL), no hay indicación visual de que se está procesando. El usuario ve la página anterior brevemente.
+
+**Implementación sugerida:**
+```typescript
+// Crear app/pages/auth-callback.vue
+
+
+
+
+
Completando inicio de sesión...
+
+
+
+
+// En server/api/auth/authentik.get.ts
+// Detectar callback y redirigir a página de loading primero
+if (query.code) {
+ // Si es primera vez con el code, redirigir a loading page
+ if (!query.processed) {
+ return sendRedirect(
+ event,
+ `/auth-callback?code=${query.code}&state=${query.state}&processed=true`
+ )
+ }
+
+ // Procesar el code
+ try {
+ // ... código existente ...
+ }
+}
+```
+
+**Alternativa más simple:**
+```typescript
+// En login.vue, detectar callback en mounted
+onMounted(() => {
+ const route = useRoute()
+ if (route.query.code) {
+ // Mostrar loading
+ isLoading.value = true
+ }
+})
+```
+
+---
+
+### 9. Role-Based Access Control (RBAC)
+**Estado:** ⚠️ Datos disponibles pero no implementado
+**Prioridad:** MEDIA
+**Problema:** No hay protección por roles/grupos
+
+**Descripción:**
+Actualmente guardamos los grupos de Authentik pero no los usamos para proteger rutas o funcionalidades.
+
+**Implementación sugerida:**
+```typescript
+// app/middleware/admin.ts
+export default defineNuxtRouteMiddleware((to, from) => {
+ const { user } = useUserSession()
+
+ if (!user.value?.groups?.includes('authentik Admins')) {
+ return navigateTo('/unauthorized')
+ }
+})
+
+// app/middleware/requires-group.ts
+export default defineNuxtRouteMiddleware((to, from) => {
+ const { user } = useUserSession()
+ const requiredGroup = to.meta.requiredGroup as string
+
+ if (requiredGroup && !user.value?.groups?.includes(requiredGroup)) {
+ return navigateTo('/unauthorized')
+ }
+})
+
+// Usar en páginas
+
+
+// O con grupo específico
+definePageMeta({
+ middleware: ['auth', 'requires-group'],
+ requiredGroup: 'editors'
+})
+
+// Composable para verificar permisos
+export const usePermissions = () => {
+ const { user } = useUserSession()
+
+ const hasGroup = (group: string) => {
+ return user.value?.groups?.includes(group) ?? false
+ }
+
+ const hasAnyGroup = (groups: string[]) => {
+ return groups.some(group => hasGroup(group))
+ }
+
+ const hasAllGroups = (groups: string[]) => {
+ return groups.every(group => hasGroup(group))
+ }
+
+ const isAdmin = computed(() => hasGroup('authentik Admins'))
+
+ return {
+ hasGroup,
+ hasAnyGroup,
+ hasAllGroups,
+ isAdmin
+ }
+}
+
+// Usar en componentes
+const { isAdmin, hasGroup } = usePermissions()
+
+
+ Eliminar Usuario
+
+```
+
+**En el backend:**
+```typescript
+// server/api/admin/users.delete.ts
+export default defineEventHandler(async (event) => {
+ const session = await requireUserSession(event)
+
+ // Verificar que el usuario es admin
+ if (!(session.user as any).groups?.includes('authentik Admins')) {
+ throw createError({
+ statusCode: 403,
+ message: 'Forbidden: Admin access required'
+ })
+ }
+
+ // ... lógica para eliminar usuario ...
+})
+
+// O crear utility
+export const requireAdmin = async (event: H3Event) => {
+ const session = await requireUserSession(event)
+
+ if (!(session.user as any).groups?.includes('authentik Admins')) {
+ throw createError({
+ statusCode: 403,
+ message: 'Forbidden: Admin access required'
+ })
+ }
+
+ return session
+}
+
+// Usar
+export default defineEventHandler(async (event) => {
+ const session = await requireAdmin(event)
+ // ... rest of handler ...
+})
+```
+
+---
+
+### 10. Session Timeout Warning
+**Estado:** ❌ No implementado
+**Prioridad:** BAJA
+**Problema:** No avisamos cuando va a expirar la sesión
+
+**Descripción:**
+Sería bueno avisar al usuario antes de que expire su sesión para que pueda renovarla o guardar su trabajo.
+
+**Implementación sugerida:**
+```typescript
+// composables/useSessionTimeout.ts
+export const useSessionTimeout = () => {
+ const { loggedIn } = useUserSession()
+ const showWarning = ref(false)
+ const timeRemaining = ref(0)
+ let warningTimeout: NodeJS.Timeout
+ let checkInterval: NodeJS.Timeout
+
+ const checkExpiration = async () => {
+ if (!loggedIn.value) return
+
+ try {
+ const session = await $fetch('/api/auth/session')
+ const remaining = session.expiresAt - Date.now()
+ timeRemaining.value = Math.floor(remaining / 1000)
+
+ // Mostrar warning 5 minutos antes de expirar
+ if (remaining < 5 * 60 * 1000 && remaining > 0) {
+ showWarning.value = true
+ }
+
+ // Si ya expiró, redirigir a login
+ if (remaining <= 0) {
+ await navigateTo('/login?error=session_expired')
+ }
+ } catch (error) {
+ console.error('Error checking session:', error)
+ }
+ }
+
+ const renewSession = async () => {
+ try {
+ await $fetch('/api/auth/refresh', { method: 'POST' })
+ showWarning.value = false
+ useToast().add({
+ title: 'Sesión renovada',
+ color: 'success'
+ })
+ } catch (error) {
+ useToast().add({
+ title: 'Error al renovar sesión',
+ color: 'error'
+ })
+ }
+ }
+
+ onMounted(() => {
+ if (loggedIn.value) {
+ checkExpiration()
+ // Verificar cada minuto
+ checkInterval = setInterval(checkExpiration, 60 * 1000)
+ }
+ })
+
+ onUnmounted(() => {
+ if (checkInterval) clearInterval(checkInterval)
+ if (warningTimeout) clearTimeout(warningTimeout)
+ })
+
+ return {
+ showWarning,
+ timeRemaining,
+ renewSession
+ }
+}
+
+// Usar en app.vue o layout
+
+
+
+
+
+
+
+
+ Tu sesión está por expirar
+
+
+
+ Tu sesión expirará en {{ Math.floor(timeRemaining / 60) }} minutos.
+ ¿Deseas extender tu sesión?
+
+
+
+
+
+ Cerrar
+
+
+ Extender sesión
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+### 11. Token Refresh Automático
+**Estado:** ❌ No implementado
+**Prioridad:** MEDIA
+**Problema:** No renovamos tokens automáticamente en background
+
+**Descripción:**
+Relacionado con refresh tokens (#3) y session timeout (#10), debería haber un mecanismo automático que renueve los tokens antes de que expiren, sin intervención del usuario.
+
+**Implementación sugerida:**
+```typescript
+// composables/useAutoRefresh.ts
+export const useAutoRefresh = () => {
+ const { loggedIn } = useUserSession()
+ let refreshTimeout: NodeJS.Timeout
+
+ const scheduleRefresh = async () => {
+ if (!loggedIn.value) return
+
+ try {
+ const session = await $fetch('/api/auth/session')
+ const timeUntilExpiry = session.expiresAt - Date.now()
+
+ // Renovar 5 minutos antes de que expire
+ const refreshTime = timeUntilExpiry - (5 * 60 * 1000)
+
+ if (refreshTime > 0) {
+ refreshTimeout = setTimeout(async () => {
+ try {
+ await $fetch('/api/auth/refresh', { method: 'POST' })
+ console.log('Token refreshed automatically')
+ // Programar próximo refresh
+ scheduleRefresh()
+ } catch (error) {
+ console.error('Auto-refresh failed:', error)
+ // Opcionalmente redirigir a login
+ }
+ }, refreshTime)
+ }
+ } catch (error) {
+ console.error('Error scheduling refresh:', error)
+ }
+ }
+
+ onMounted(() => {
+ if (loggedIn.value) {
+ scheduleRefresh()
+ }
+ })
+
+ onUnmounted(() => {
+ if (refreshTimeout) clearTimeout(refreshTimeout)
+ })
+
+ watch(loggedIn, (newValue) => {
+ if (newValue) {
+ scheduleRefresh()
+ } else {
+ if (refreshTimeout) clearTimeout(refreshTimeout)
+ }
+ })
+}
+
+// Usar en app.vue
+
+```
+
+**Alternativa con Web Worker:**
+```typescript
+// public/refresh-worker.js
+let refreshTimer = null
+
+self.addEventListener('message', (event) => {
+ const { type, expiresAt } = event.data
+
+ if (type === 'schedule') {
+ if (refreshTimer) clearTimeout(refreshTimer)
+
+ const timeUntilExpiry = expiresAt - Date.now()
+ const refreshTime = timeUntilExpiry - (5 * 60 * 1000)
+
+ if (refreshTime > 0) {
+ refreshTimer = setTimeout(() => {
+ self.postMessage({ type: 'refresh-needed' })
+ }, refreshTime)
+ }
+ } else if (type === 'cancel') {
+ if (refreshTimer) clearTimeout(refreshTimer)
+ }
+})
+
+// Usar desde composable
+const worker = new Worker('/refresh-worker.js')
+worker.onmessage = async (event) => {
+ if (event.data.type === 'refresh-needed') {
+ await $fetch('/api/auth/refresh', { method: 'POST' })
+ // Re-schedule
+ const session = await $fetch('/api/auth/session')
+ worker.postMessage({ type: 'schedule', expiresAt: session.expiresAt })
+ }
+}
+```
+
+---
+
+## 📊 Prioridades Recomendadas
+
+### Para Template de Producción (Crítico):
+1. ✅ **State Parameter** - Seguridad crítica (CSRF protection)
+2. ✅ **Refresh Tokens** - Mejor UX, evita re-autenticación constante
+3. ✅ **Token Storage & Expiration** - Base para refresh tokens
+4. ✅ **SSO Logout** - Logout completo en Authentik
+
+### Importante (Alta prioridad):
+5. ✅ **Redirect After Login** - UX básica esperada
+6. ✅ **Error Messages** - Debugging y mejor UX
+7. ✅ **RBAC** - Control de acceso por grupos
+
+### Nice to Have (Media/Baja prioridad):
+8. ⭕ **PKCE** - Seguridad adicional (cada vez más recomendado)
+9. ⭕ **Loading States** - Mejor UX durante OAuth flow
+10. ⭕ **Session Timeout Warning** - UX avanzada
+11. ⭕ **Auto Refresh** - UX premium, sesión sin interrupciones
+
+---
+
+## 🎯 Roadmap Sugerido
+
+### Fase 1 - Seguridad Básica (1-2 días)
+- [ ] Implementar State Parameter
+- [ ] Mejorar manejo de errores con mensajes específicos
+
+### Fase 2 - Token Management (2-3 días)
+- [ ] Guardar todos los tokens en sesión
+- [ ] Implementar refresh token endpoint
+- [ ] Implementar detección de expiración
+
+### Fase 3 - UX Mejorada (2-3 días)
+- [ ] Redirect after login
+- [ ] SSO Logout con Authentik
+- [ ] Loading states durante OAuth
+
+### Fase 4 - RBAC (1-2 días)
+- [ ] Middleware para roles
+- [ ] Composable de permisos
+- [ ] Ejemplos de protección por grupos
+
+### Fase 5 - Advanced (3-4 días)
+- [ ] PKCE implementation
+- [ ] Auto-refresh automático
+- [ ] Session timeout warning
+- [ ] Token refresh automático en background
+
+---
+
+## 📚 Referencias
+
+- [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
+- [OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
+- [PKCE RFC 7636](https://tools.ietf.org/html/rfc7636)
+- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
+- [OAuth 2.1 (draft)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07)
+- [Authentik OAuth Provider Docs](https://docs.goauthentik.io/docs/providers/oauth2/)
+- [nuxt-auth-utils Documentation](https://github.com/Atinux/nuxt-auth-utils)
+
+---
+
+## 💡 Notas
+
+- Algunas de estas funcionalidades son opcionales dependiendo del caso de uso
+- Para aplicaciones internas simples, la implementación actual puede ser suficiente
+- Para aplicaciones de producción orientadas al público, se recomienda implementar al menos la Fase 1 y 2
+- PKCE está convirtiéndose en estándar (OAuth 2.1 lo hace obligatorio)
+- Considerar usar librerías como `jose` para manejo de JWT si se necesita validación de tokens en el cliente