Cambiar tema

Protección de rutas y control de permisos en Next.js: guía completa de Middleware y defensa en profundidad

Easton editorial illustration: cache waterfall instrument

El mes pasado, en una auditoría de seguridad para un cliente, el revisor miró mi código de permisos y frunció el ceño: «¿Eso es todo? ¿Solo un bloqueo en Middleware?»

Me molestó un poco. ¿Middleware no existe precisamente para esto? Usuario sin sesión → redirección a login; con sesión → pasa. Parecía hermético.

El revisor abrió las herramientas de desarrollo del navegador y me demostró algo: saltarse las rutas del frontend, llamar la API de backend con Postman y obtener datos que debían estar protegidos.

Me quedé un poco aturdido.

Después de investigar entendí que el control de permisos en Next.js nunca ha sido cosa de un solo Middleware. Necesitas una arquitectura completa de defensa en profundidad.

En este artículo hablamos de: ¿cómo usar Middleware? ¿Qué relación tiene con getServerSession? ¿Cómo diseñar RBAC para un panel de administración? Y plantillas de código que puedes usar directamente.

Si estás implementando permisos en Next.js o tienes dudas sobre Middleware y getServerSession, esto debería ayudarte.

Por qué el control de permisos no puede depender solo de Middleware

Qué hace realmente Middleware

Primero, aclaremos el rol de Middleware.

Corre en Edge Runtime y es una de las primeras capas que ve una petición. Aquí puedes hacer un filtrado grueso: ¿hay sesión? ¿Es admin o usuario normal? ¿Redirigir o dejar pasar?

Es rápido y está muy arriba en la cadena. Suena ideal para permisos, ¿verdad?

Lo es, pero no basta solo con eso.

Un caso real de vulnerabilidad

En enero, investigadores de seguridad divulgaron CVE-2025-29927: en resumen, un atacante puede añadir el header especial x-middleware-subrequest y saltarse la comprobación de Middleware.

Toda la lógica que escribiste en Middleware queda inútil ante ese ataque.

No es un caso aislado. Como primera línea de defensa, Middleware puede omitirse, configurarse mal o no poder hacer comprobaciones complejas por limitaciones del entorno edge.

La documentación oficial de Next.js lo subraya: «While Middleware can be useful for initial checks, it should not be your only line of defense.»

Traducido: no pongas todas las fichas en Middleware.

El frontend bloqueado, ¿y el backend?

Hice un panel de administración donde Middleware comprobaba la sesión: sin login, /admin redirigía a login.

Parecía seguro. Menús y navegación pasaban por Middleware.

¿El problema? La API no estaba protegida.

Un compañero de QA abrió el panel Network, vio que borrar usuarios era POST /api/users/delete y lo probó con curl y parámetros inventados.

Borró con éxito.

La API Route no tenía comprobación de permisos. Solo bloqueé rutas del frontend; no contemplé llamadas directas a la API.

Ese es el riesgo de depender solo de Middleware: cierra la puerta del frontend, no la ventana del backend.

Lo que recomienda Next.js

En la documentación aparece el «Proximity Principle»: coloca las comprobaciones de permisos lo más cerca posible de los datos.

¿Qué significa?

Los datos están en la base de datos. Para protegerlos, comprueba permisos antes de acceder a ellos: no solo en rutas o páginas, sino en la capa de datos.

Eso no implica que Middleware sea inútil. Puede ser la primera capa: redirigir sin sesión, rechazar roles sin acceso obvio. Pero hace falta más:

  • Comprobaciones en Server Components: antes de renderizar, verificar acceso a la página
  • Comprobaciones en API Route y Server Actions: validar permisos en cada operación de datos
  • Comprobaciones en la capa de consulta: Row-Level Security o filtros para que cada usuario solo vea lo permitido

Defensa en profundidad: si una capa cae, queda la siguiente.

Al principio me pareció pesado. Después de pisar varias trampas, entendí que donde a ti te molesta la complejidad, al atacante le interesa la simplicidad.

Cómo combinar Middleware y getServerSession correctamente

Por qué no puedes usar getServerSession en Middleware

Cuando empecé con NextAuth, me pasó lo mismo.

En Server Components la sesión se obtiene con getServerSession(authOptions). Lo natural era usarlo también en Middleware.

Error.

Tras investigar: Middleware corre en Edge Runtime y getServerSession necesita Node.js Runtime. No son compatibles.

Edge Runtime es un entorno ligero de Vercel: sin la API completa de Node.js, pero rápido y distribuido globalmente. Middleware eligió rendimiento a costa de no poder usar todo el ecosistema Node.

¿Cómo obtener la sesión en Middleware entonces?

La forma correcta: getToken o withAuth

NextAuth ofrece dos APIs pensadas para Middleware:

Opción 1: usar getToken

// middleware.ts
import { getToken } from "next-auth/jwt"
import { NextResponse } from "next/server"

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
  
  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  
  // Comprobar rol
  if (req.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
    return NextResponse.redirect(new URL('/403', req.url))
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

getToken parsea el JWT de las cookies. Solo funciona con estrategia de sesión JWT; con sesión en base de datos no sirve.

Opción 2: usar el HOC withAuth

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      // Sin sesión
      if (!token) return false
      
      // Rutas /admin solo para admin
      if (req.nextUrl.pathname.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      return true
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

withAuth envuelve la lógica y gestiona redirecciones. En authorized devuelves true o false; él redirige al login.

Personalmente prefiero withAuth: menos código.

¿Dónde usar getServerSession?

En Server Components, API Route y Server Actions.

En Server Component:

// app/admin/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"

export default async function AdminPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  if (session.user.role !== 'admin') {
    redirect('/403')
  }
  
  // Renderizar página
  return <UsersList />
}

Aquí compruebas si el usuario puede ver la página. Sin permiso, no renderizas.

En API Route:

// app/api/users/route.ts
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { NextResponse } from "next/server"

export async function DELETE(req: Request) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
  }
  
  // Ejecutar borrado
  // ...
  
  return NextResponse.json({ success: true })
}

En Server Action:

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session || session.user.role !== 'admin') {
    throw new Error('Unauthorized')
  }
  
  // Ejecutar borrado
  // ...
}

División de responsabilidades

En resumen:

  • Middleware (con getToken): intercepción gruesa de rutas — redirección sin sesión, comprobación básica de rol
  • getServerSession: control fino antes de tocar datos

Metáfora: Middleware es el vigilante del portal; getServerSession es la cerradura de tu puerta. Aunque el vigilante deje pasar, sin llave no entras.

Eso es defensa en profundidad.

Diseño RBAC completo para un panel de administración

Hemos visto Middleware y getServerSession, pero en un panel real necesitas un sistema RBAC (control de acceso basado en roles) completo.

El modelo RBAC

La idea central: usuario → rol → permiso.

  • Usuario (User): Ana, Luis
  • Rol (Role): administrador, editor, lector
  • Permiso (Permission): ver lista de usuarios, editar artículos, borrar comentarios

Un usuario puede tener varios roles; un rol agrupa permisos. Ana es admin con todo; Luis es editor y solo edita artículos y ve usuarios.

La granularidad puede ser:

  • Nivel página: ¿puede entrar en /admin/users?
  • Nivel función: ¿puede pulsar «Eliminar»?
  • Nivel datos: ¿puede ver solo sus propios artículos?

Un schema típico (Prisma):

model User {
  id    String @id @default(cuid())
  email String @unique
  roles Role[]
}

model Role {
  id          String       @id @default(cuid())
  name        String       @unique
  permissions Permission[]
  users       User[]
}

model Permission {
  id    String @id @default(cuid())
  name  String @unique // p. ej. "user:view", "user:edit"
  roles Role[]
}

En proyectos pequeños, con pocos roles (admin, editor, lector), a veces basta definirlos en código.

Arquitectura de cuatro capas

Cómo llevar las comprobaciones a cada nivel.

Primera capa: Middleware — intercepción gruesa

// middleware.ts
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token, req }) => {
      if (!token) return false
      
      const path = req.nextUrl.pathname
      
      // /admin solo para admin
      if (path.startsWith('/admin')) {
        return token.role === 'admin'
      }
      
      // /dashboard solo requiere sesión
      if (path.startsWith('/dashboard')) {
        return true
      }
      
      return false
    }
  }
})

export const config = {
  matcher: ['/admin/:path*', '/dashboard/:path*']
}

Solo roles básicos, sin permisos de función concretos.

Segunda capa: Server Component — permisos a nivel página

// app/admin/users/page.tsx
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"
import { redirect } from "next/navigation"

export default async function UsersPage() {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    redirect('/login')
  }
  
  // Comprobar permiso para ver lista de usuarios
  const hasPermission = await checkPermission(session.user.id, 'user:view')
  
  if (!hasPermission) {
    redirect('/403')
  }
  
  // Renderizar página
  return <UsersList />
}

Sin permiso de página, no se muestra.

Tercera capa: UI condicional — permisos a nivel función

// components/UsersList.tsx
'use client'

import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function UsersList() {
  const { data: session } = useSession()
  
  const canEdit = hasPermission(session, 'user:edit')
  const canDelete = hasPermission(session, 'user:delete')
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <span>{user.name}</span>
          {canEdit && <button>Editar</button>}
          {canDelete && <button>Eliminar</button>}
        </div>
      ))}
    </div>
  )
}

Ocultar botones según permiso mejora la UX; quien no ve el botón no lo pulsa.

Pero esto es UX, no seguridad. Alguien técnico puede mostrar el botón en la consola. La comprobación real va en la capa siguiente.

Cuarta capa: Server Action / API — validación antes de mutar datos

// app/actions.ts
'use server'

import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { checkPermission } from "@/lib/permissions"

export async function deleteUser(userId: string) {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    throw new Error('Unauthorized')
  }
  
  // Debe tener permiso de borrado
  const hasPermission = await checkPermission(session.user.id, 'user:delete')
  
  if (!hasPermission) {
    throw new Error('Forbidden')
  }
  
  // Ejecutar borrado
  await db.user.delete({ where: { id: userId } })
  
  return { success: true }
}

Última línea de defensa: al operar datos, vuelve a validar permisos.

Centralizar la configuración de permisos

Repetir comprobaciones en cada sitio cansa. Yo concentro definición y lógica en un archivo.

// lib/permissions.ts
export const PERMISSIONS = {
  USER_VIEW: 'user:view',
  USER_EDIT: 'user:edit',
  USER_DELETE: 'user:delete',
  POST_VIEW: 'post:view',
  POST_EDIT: 'post:edit',
  POST_DELETE: 'post:delete',
} as const

export const ROLES = {
  ADMIN: {
    name: 'admin',
    permissions: Object.values(PERMISSIONS) // admin tiene todo
  },
  EDITOR: {
    name: 'editor',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
      PERMISSIONS.POST_EDIT,
    ]
  },
  VIEWER: {
    name: 'viewer',
    permissions: [
      PERMISSIONS.USER_VIEW,
      PERMISSIONS.POST_VIEW,
    ]
  }
} as const

// Comprobación en servidor
export async function checkPermission(userId: string, permission: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { roles: true }
  })
  
  if (!user) return false
  
  // Comprobar si el rol incluye el permiso
  const userRole = ROLES[user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}
// lib/permissions-client.ts (versión cliente)
import { Session } from "next-auth"

export function hasPermission(session: Session | null, permission: string) {
  if (!session?.user) return false
  
  const userRole = ROLES[session.user.role as keyof typeof ROLES]
  return userRole?.permissions.includes(permission) ?? false
}

Así, altas y bajas de permisos se hacen en un solo sitio.

Ventajas de esta arquitectura

¿Merece escribir más capas?

Sí.

  • Seguridad: si una capa falla, hay respaldo
  • Mantenibilidad: permisos centralizados
  • UX: botones ocultos cuando no hay permiso
  • Auditorías: comprobaciones explícitas en cada capa

Montar la arquitectura lleva tiempo al inicio; al añadir funciones o cambiar reglas, ahorras mucho.

Casos prácticos y plantillas de código

Principio y arquitectura ya vistos; aquí van plantillas listas para usar y cómo depurar problemas habituales.

Plantilla completa de Middleware

Configuración con varios roles y rutas dinámicas:

// middleware.ts
import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server"

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token
    const path = req.nextUrl.pathname
    
    // Control fino por ruta y rol
    if (path.startsWith('/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    if (path.startsWith('/editor') && !['admin', 'editor'].includes(token?.role as string)) {
      return NextResponse.redirect(new URL('/403', req.url))
    }
    
    return NextResponse.next()
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token
    }
  }
)

export const config = {
  matcher: [
    '/admin/:path*',
    '/editor/:path*',
    '/dashboard/:path*',
    '/api/admin/:path*',
    '/api/editor/:path*'
  ]
}

Puntos clave:

  1. matcher define rutas protegidas; admite :path*
  2. authorized hace la comprobación mínima de sesión
  3. La función middleware aplica reglas de rol más finas

Menú dinámico

Generar menú según permisos es habitual en paneles admin:

// components/Sidebar.tsx
'use client'

import { useSession } from "next-auth/react"
import Link from "next/link"
import { hasPermission } from "@/lib/permissions-client"

const menuItems = [
  {
    label: 'Gestión de usuarios',
    href: '/admin/users',
    permission: 'user:view'
  },
  {
    label: 'Gestión de artículos',
    href: '/admin/posts',
    permission: 'post:view'
  },
  {
    label: 'Configuración del sistema',
    href: '/admin/settings',
    permission: 'setting:manage'
  }
]

export function Sidebar() {
  const { data: session } = useSession()
  
  // Filtrar ítems por permiso
  const visibleItems = menuItems.filter(item => 
    hasPermission(session, item.permission)
  )
  
  return (
    <nav>
      {visibleItems.map(item => (
        <Link key={item.href} href={item.href}>
          {item.label}
        </Link>
      ))}
    </nav>
  )
}

Menú y permisos quedan ligados. Nuevo ítem: añádelo al array menuItems.

Hook reutilizable de permisos

Para componentes cliente:

// hooks/usePermission.ts
import { useSession } from "next-auth/react"
import { hasPermission } from "@/lib/permissions-client"

export function usePermission(permission: string) {
  const { data: session, status } = useSession()
  
  const isLoading = status === 'loading'
  const isAllowed = hasPermission(session, permission)
  
  return { isAllowed, isLoading }
}

Uso sencillo:

// components/DeleteButton.tsx
'use client'

import { usePermission } from "@/hooks/usePermission"

export function DeleteButton({ userId }: { userId: string }) {
  const { isAllowed, isLoading } = usePermission('user:delete')
  
  if (isLoading) return <div>Loading...</div>
  if (!isAllowed) return null
  
  return (
    <button onClick={() => deleteUser(userId)}>
      Eliminar
    </button>
  )
}

Depuración de problemas habituales

Problema 1: redirección infinita en Middleware

Síntoma: el navegador reporta demasiadas redirecciones.

Causa: la página de login también está protegida por Middleware.

Solución: excluir login y páginas públicas en matcher.

export const config = {
  matcher: [
    /*
     * Coincidir con todas las rutas excepto:
     * - /login (página de login)
     * - /api/auth (API de NextAuth)
     * - /_next (internos de Next.js)
     * - /favicon.ico, /robots.txt (estáticos)
     */
    '/((?!login|api/auth|_next|favicon.ico|robots.txt).*)',
  ]
}

Problema 2: getServerSession devuelve null

Síntoma: hay sesión en el navegador pero getServerSession devuelve null.

Causas:

  1. authOptions mal configurado o no pasado
  2. Problemas de cookies (dominio, HTTPS)
  3. Uso incorrecto de getServerSession dentro de Middleware

Solución:

  • Importar authOptions correctamente
  • Usar solo en Server Component o API Route, no en Middleware
  • En desarrollo, revisar NEXTAUTH_URL
// Uso correcto
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/app/api/auth/[...nextauth]/route"

const session = await getServerSession(authOptions)

Problema 3: rendimiento de comprobaciones de permisos

Síntoma: cada carga de página es lenta por consultas a la BD.

Causa: sin caché; cada petición consulta permisos.

Solución:

  1. Meter rol y permisos en el JWT
// app/api/auth/[...nextauth]/route.ts
export const authOptions: NextAuthOptions = {
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.permissions = user.permissions
      }
      return token
    },
    async session({ session, token }) {
      session.user.role = token.role
      session.user.permissions = token.permissions
      return session
    }
  }
}
  1. Caché en cliente con React Query o SWR
// hooks/usePermissions.ts
import useSWR from 'swr'

export function usePermissions() {
  const { data: permissions } = useSWR('/api/me/permissions', {
    revalidateOnFocus: false,
    dedupingInterval: 60000 // no repetir en 1 minuto
  })
  
  return permissions
}

Lista de comprobación rápida

Antes de desplegar, revisa:

  • ¿Middleware solo hace comprobaciones gruesas?
  • ¿Todas las páginas en Server Components validan permisos?
  • ¿Todas las API Route validan permisos?
  • ¿Todas las Server Actions validan permisos?
  • ¿Los botones sensibles se ocultan según permiso?
  • ¿La configuración de permisos está centralizada?
  • ¿El JWT incluye información de rol?
  • ¿Login y páginas públicas quedan fuera del matcher de Middleware?

Si marcas todo, el control de permisos está en buen camino.

Conclusión

Volviendo a la pregunta inicial: ¿cómo hacer control de permisos en Next.js?

No hay una sola bala mágica.

Middleware importa: es la primera línea y frena gran parte del tráfico no autorizado. Pero no es suficiente. Comprueba permisos de página en Server Components, operaciones en Server Actions y API Route, y optimiza la UX en la capa de interfaz.

La arquitectura en capas parece más trabajo. En seguridad no hay atajos: si una capa cae, las demás sostienen el sistema.

Solo Middleware vs defensa en profundidad

DimensiónSolo MiddlewareDefensa en profundidad
SeguridadBaja, omitibleAlta, varias capas
Coste de desarrolloBajo, un solo sitioMedio, comprobaciones en varios puntos
MantenibilidadMala, lógica dispersaBuena, configuración centralizada
Experiencia de usuarioRegular, error al pulsarBuena, ocultar lo no permitido
AuditoríasMalas, un solo puntoBuenas, trazas por capa

Más capas implica más código, pero los beneficios son reales.

Acción inmediata

Si tienes un proyecto Next.js, comprueba ahora:

  1. ¿Solo proteges en Middleware?
  2. ¿API Route y Server Actions tienen validación propia?
  3. ¿Los permisos están repartidos por muchos archivos?

Si alguna respuesta es sí, conviene migrar a defensa en profundidad. No hace falta hacerlo todo de golpe: empieza por las operaciones de datos críticas y completa el resto después.

En seguridad, arreglar pronto siempre gana a arreglar tarde.

¿Más dudas?

Este artículo cubre la arquitectura central y casos prácticos, pero no todos los escenarios. Si en tu proyecto te encuentras con:

  • Permisos a nivel de datos (solo ver lo que creaste)
  • Row-Level Security con Prisma
  • Aislamiento de permisos en sistemas multi-tenant

déjalas en comentarios y las vemos.

El control de permisos no pasa de moda. Espero que esto te ahorre algunas trampas.

Flujo completo de configuración de permisos en capas en Next.js

Pasos completos desde la comprobación básica en Middleware hasta la validación detallada con getServerSession y la verificación de permisos en base de datos

⏱️ Estimated time: 3 hr

  1. 1

    Step 1: Primera capa: comprobación básica en Middleware

    Crear middleware.ts:
    ```ts
    import { withAuth } from 'next-auth/middleware'

    export default withAuth({
    pages: {
    signIn: '/login'
    }
    })

    export const config = {
    matcher: ['/dashboard/:path*', '/admin/:path*']
    }
    ```

    Función:
    • Comprobar si el usuario ha iniciado sesión
    • Redirigir a login si no hay sesión
    • Rápido y temprano en la cadena

    Limitaciones:
    • Se puede omitir (vulnerabilidad x-middleware-subrequest)
    • Solo filtrado grueso
    • No permite comprobaciones de permisos detalladas

    Punto clave: Middleware es la primera capa, no la única.
  2. 2

    Step 2: Segunda capa: validación detallada con getServerSession

    En la página:
    ```tsx
    import { getServerSession } from 'next-auth'
    import { authOptions } from '@/app/api/auth/[...nextauth]/route'

    export default async function DashboardPage() {
    const session = await getServerSession(authOptions)

    if (!session) {
    redirect('/login')
    }

    // Comprobar rol del usuario
    if (session.user.role !== 'admin') {
    redirect('/unauthorized')
    }

    return <div>Dashboard</div>
    }
    ```

    Función:
    • Validar la identidad del usuario en detalle
    • Comprobar el rol
    • Control de permisos granular

    Ventajas:
    • Más seguro que solo Middleware
    • Permite comprobaciones detalladas
    • No se puede omitir fácilmente

    Punto clave: getServerSession es la segunda capa de validación detallada.
  3. 3

    Step 3: Tercera capa: comprobación de permisos en base de datos

    Comprobar en API Route:
    ```ts
    import { getServerSession } from 'next-auth'
    import { db } from '@/lib/db'

    export async function GET(request: Request) {
    const session = await getServerSession(authOptions)

    if (!session) {
    return new Response('Unauthorized', { status: 401 })
    }

    // Comprobación en base de datos
    const user = await db.user.findUnique({
    where: { id: session.user.id },
    include: { role: { include: { permissions: true } } }
    })

    const hasPermission = user.role.permissions.some(
    p => p.resource === 'users' && p.action === 'read'
    )

    if (!hasPermission) {
    return new Response('Forbidden', { status: 403 })
    }

    // Devolver datos
    return Response.json({ users: [...] })
    }
    ```

    Función:
    • Validación final de permisos
    • Comprobar permisos persistidos en BD
    • Garantizar seguridad de datos

    Punto clave: la base de datos es la tercera capa de validación final.
  4. 4

    Step 4: Implementar un sistema RBAC

    Schema de base de datos:
    ```prisma
    model User {
    id String @id @default(cuid())
    email String @unique
    role Role @relation(fields: [roleId], references: [id])
    roleId String
    }

    model Role {
    id String @id @default(cuid())
    name String @unique
    permissions Permission[]
    users User[]
    }

    model Permission {
    id String @id @default(cuid())
    resource String
    action String
    roles Role[]
    }
    ```

    Comprobar permisos:
    ```ts
    async function checkPermission(
    userId: string,
    resource: string,
    action: string
    ) {
    const user = await db.user.findUnique({
    where: { id: userId },
    include: { role: { include: { permissions: true } } }
    })

    return user.role.permissions.some(
    p => p.resource === resource && p.action === action
    )
    }
    ```

    Puntos clave:
    • El usuario tiene roles
    • Los roles tienen permisos
    • Los permisos controlan el acceso a recursos

FAQ

¿Por qué el control de permisos no puede depender solo de Middleware?
Motivo: Middleware se puede omitir.

Caso de vulnerabilidad:
• CVE-2025-29927: un atacante puede añadir el header x-middleware-subrequest y saltarse la comprobación de Middleware
• Llamar la API de backend con Postman y obtener datos protegidos

Rol de Middleware:
• Corre en Edge Runtime, filtrado grueso
• Comprueba sesión y rol (admin vs usuario normal)
• Rápido y temprano, pero no basta solo

Solución: defensa en profundidad
• Primera capa: Middleware (comprobación básica)
• Segunda capa: getServerSession (validación detallada)
• Tercera capa: comprobación en base de datos (validación final)

Punto clave: tres capas para mayor seguridad; ninguna debe omitirse.
¿Cuál es la diferencia entre Middleware y getServerSession?
Middleware:
• Corre en Edge Runtime
• Rápido y temprano
• Solo filtrado grueso (sesión, rol básico)
• Se puede omitir (x-middleware-subrequest)

getServerSession:
• Corre en Node.js Runtime
• Validación detallada
• Comprueba rol y permisos
• No se omite con el mismo vector

Uso conjunto:
• Middleware: comprobación básica (primera capa)
• getServerSession: validación detallada (segunda capa)
• Base de datos: validación final (tercera capa)

Ejemplo:
```ts
// middleware.ts (primera capa)
export default withAuth({
pages: { signIn: '/login' }
})

// page.tsx (segunda capa)
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}

// api/route.ts (tercera capa)
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```
¿Cómo implementar un sistema RBAC?
RBAC = Role-Based Access Control (control de acceso basado en roles)

Schema de base de datos:
```prisma
model User {
id String @id
email String @unique
role Role @relation(fields: [roleId], references: [id])
roleId String
}

model Role {
id String @id
name String @unique
permissions Permission[]
users User[]
}

model Permission {
id String @id
resource String // recurso: users, posts, etc.
action String // acción: read, write, delete
roles Role[]
}
```

Comprobar permisos:
```ts
async function checkPermission(
userId: string,
resource: string,
action: string
) {
const user = await db.user.findUnique({
where: { id: userId },
include: { role: { include: { permissions: true } } }
})

return user.role.permissions.some(
p => p.resource === resource && p.action === action
)
}
```

Puntos clave:
• El usuario tiene roles
• Los roles tienen permisos
• Los permisos controlan el acceso a recursos
¿Cómo implementar una arquitectura de defensa en profundidad?
Tres capas:

Primera capa: Middleware (comprobación básica)
```ts
// middleware.ts
export default withAuth({
pages: { signIn: '/login' }
})
```

Segunda capa: getServerSession (validación detallada)
```tsx
// page.tsx
const session = await getServerSession(authOptions)
if (session.user.role !== 'admin') {
redirect('/unauthorized')
}
```

Tercera capa: comprobación en base de datos (validación final)
```ts
// api/route.ts
const hasPermission = await checkPermission(userId, 'users', 'read')
if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}
```

Puntos clave:
• Tres capas para mayor seguridad
• Ninguna capa debe omitirse
• Middleware filtra en grueso, getServerSession en fino, la BD valida al final
¿Cómo prevenir la vulnerabilidad x-middleware-subrequest?
Vulnerabilidad: un atacante añade el header x-middleware-subrequest y omite Middleware.

Medidas:
• No depender solo de Middleware
• Comprobar permisos también en API Route
• Validar con getServerSession
• Validación final en base de datos

Ejemplo:
```ts
// api/route.ts
export async function GET(request: Request) {
// Aunque se omita Middleware, aquí se comprueba
const session = await getServerSession(authOptions)

if (!session) {
return new Response('Unauthorized', { status: 401 })
}

// Comprobación en base de datos
const hasPermission = await checkPermission(session.user.id, 'users', 'read')

if (!hasPermission) {
return new Response('Forbidden', { status: 403 })
}

return Response.json({ users: [...] })
}
```

Puntos clave:
• Defensa en profundidad
• API Route también debe comprobar permisos
• La BD hace la validación final

Recomendación: nunca confíes solo en Middleware; valida permisos en API Route.
¿Cuáles son las mejores prácticas de control de permisos?
Tres capas:
• Middleware: comprobación básica (primera capa)
• getServerSession: validación detallada (segunda capa)
• Base de datos: validación final (tercera capa)

Sistema RBAC:
• El usuario tiene roles
• Los roles tienen permisos
• Los permisos controlan recursos

Organización del código:
• Funciones utilitarias de comprobación de permisos
• Uso uniforme en API Route
• Evitar código duplicado

Recomendaciones de seguridad:
• Nunca solo Middleware
• Comprobar permisos en API Route
• Validación final en base de datos
• Revisar la configuración de permisos con regularidad

Puntos clave:
• Defensa en profundidad
• Ninguna capa omitible
• Mejora continua del sistema de permisos

Recuerda: el control de permisos es la base de la seguridad; no lo subestimes.

14 min de lectura · Publicado el: 19 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog