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

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:
matcherdefine rutas protegidas; admite:path*authorizedhace la comprobación mínima de sesión- La función
middlewareaplica 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:
authOptionsmal configurado o no pasado- Problemas de cookies (dominio, HTTPS)
- Uso incorrecto de
getServerSessiondentro de Middleware
Solución:
- Importar
authOptionscorrectamente - 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:
- 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
}
}
}
- 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ón | Solo Middleware | Defensa en profundidad |
|---|---|---|
| Seguridad | Baja, omitible | Alta, varias capas |
| Coste de desarrollo | Bajo, un solo sitio | Medio, comprobaciones en varios puntos |
| Mantenibilidad | Mala, lógica dispersa | Buena, configuración centralizada |
| Experiencia de usuario | Regular, error al pulsar | Buena, ocultar lo no permitido |
| Auditorías | Malas, un solo punto | Buenas, 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:
- ¿Solo proteges en Middleware?
- ¿API Route y Server Actions tienen validación propia?
- ¿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
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
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
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
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?
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?
• 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?
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?
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?
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?
• 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
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía práctica de Next.js Middleware: coincidencia de rutas, limitaciones de Edge Runtime y errores comunes
De un bug en producción a la solución completa: matcher, limitaciones de Edge Runtime y tres escenarios prácticos para evitar las trampas más habituales
Parte 11 de 51
Siguiente
Guía completa de OAuth en Next.js: Google, GitHub, WeChat y mejores prácticas
Explicamos OAuth 2.0 con palabras sencillas y te guiamos paso a paso para configurar inicio de sesión con Google, GitHub y WeChat en Next.js, con soluciones a redirect_uri_mismatch, riesgos de seguridad y errores habituales.
Parte 13 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario