Feat: Agregar middleware global de autenticación API
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m9s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m9s
- Valida Bearer token para rutas expuestas (/api/messages/send, /api/mcp) - Valida x-authentik-username para rutas protegidas por Authentik - Rutas públicas (/api/health) sin autenticación - Defensa en profundidad: cualquier endpoint /api/* está protegido
This commit is contained in:
96
server/middleware/01.api-auth.ts
Normal file
96
server/middleware/01.api-auth.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Middleware de autenticación para API
|
||||
*
|
||||
* Asegura que TODAS las rutas /api/* estén protegidas:
|
||||
* - Rutas con Authentik: Requieren header x-authentik-username
|
||||
* - Rutas sin Authentik (expuestas): Requieren Bearer token válido
|
||||
*/
|
||||
|
||||
import { query } from '../utils/database'
|
||||
|
||||
// Rutas que NO pasan por Authentik (expuestas con API Key)
|
||||
const API_KEY_ROUTES = [
|
||||
'/api/messages/send',
|
||||
'/api/mcp'
|
||||
]
|
||||
|
||||
// Rutas públicas (sin autenticación)
|
||||
const PUBLIC_ROUTES = [
|
||||
'/api/health'
|
||||
]
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const path = getRequestURL(event).pathname
|
||||
|
||||
// Solo aplicar a rutas /api/*
|
||||
if (!path.startsWith('/api/')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Rutas públicas - sin autenticación
|
||||
if (PUBLIC_ROUTES.some(route => path.startsWith(route))) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verificar si es ruta expuesta (sin Authentik)
|
||||
const isExposedRoute = API_KEY_ROUTES.some(route => path.startsWith(route))
|
||||
|
||||
if (isExposedRoute) {
|
||||
// Ruta expuesta: Requiere Bearer token
|
||||
const authHeader = getHeader(event, 'authorization')
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Authorization Bearer token required'
|
||||
})
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7)
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
// Validar contra Master API Key
|
||||
if (config.masterApiKey && token === config.masterApiKey) {
|
||||
// Token válido - continuar
|
||||
return
|
||||
}
|
||||
|
||||
// Validar contra API Keys en DB
|
||||
const crypto = await import('crypto')
|
||||
const keyHash = crypto.createHash('sha256').update(token).digest('hex')
|
||||
|
||||
const keyResult = await query(
|
||||
`SELECT id FROM api_keys
|
||||
WHERE key_hash = $1 AND is_active = TRUE
|
||||
AND (expires_at IS NULL OR expires_at > NOW())`,
|
||||
[keyHash]
|
||||
)
|
||||
|
||||
if (keyResult.rows.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Invalid API Key'
|
||||
})
|
||||
}
|
||||
|
||||
// Token válido - actualizar last_used
|
||||
await query(
|
||||
'UPDATE api_keys SET last_used_at = NOW() WHERE id = $1',
|
||||
[keyResult.rows[0].id]
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Ruta protegida por Authentik: Requiere header x-authentik-username
|
||||
const authentikUser = getHeader(event, 'x-authentik-username')
|
||||
|
||||
if (!authentikUser) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Authentication required'
|
||||
})
|
||||
}
|
||||
|
||||
// Usuario autenticado - continuar
|
||||
})
|
||||
Reference in New Issue
Block a user