Frontend nextjs

Defensa en profundidad: proteger /admin en dos capas en Next.js

Proteger una ruta administrativa solo en el proxy es un solo punto de falla. Así se arma una segunda barrera independiente que no depende de la primera.

8 min
Next.js Seguridad Auth App Router

Proteger /admin con una verificación en el proxy es el primer instinto de casi todo el mundo, y no está mal — pero si es la única capa, tienes un solo punto de falla protegiendo el panel más sensible de tu aplicación. Este artículo muestra cómo armar una segunda barrera, completamente independiente de la primera, sin librerías externas de por medio (para que entiendas el mecanismo, no solo lo copies).

Si no leíste el contexto de seguridad detrás de esta recomendación, te recomiendo primero el cambio de middleware a proxy y el CVE que lo motivó.

Capa 1: verificación en el proxy (la primera línea, no la única)

// src/proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { isValidSessionToken, SESSION_COOKIE } from '@/lib/auth'

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl

  if (pathname.startsWith('/admin') && pathname !== '/admin/login') {
    const token = request.cookies.get(SESSION_COOKIE)?.value
    if (!isValidSessionToken(token)) {
      return NextResponse.redirect(new URL('/admin/login', request.url))
    }
  }

  return NextResponse.next()
}

Nota la excepción explícita para /admin/login — sin ella, la propia página de login quedaría bloqueada por su propia protección, un error común al implementar esto por primera vez.

La firma de sesión, sin librerías externas

Para entender el mecanismo de verdad (no solo usar una librería como caja negra), así se ve una sesión firmada con HMAC:

// src/lib/auth.ts
import crypto from 'node:crypto'

export const SESSION_COOKIE = 'admin_session'

export function createSessionToken(): string {
  const payload = `admin:${Date.now()}`
  const signature = crypto
    .createHmac('sha256', process.env.AUTH_SECRET!)
    .update(payload)
    .digest('hex')
  return Buffer.from(`${payload}.${signature}`).toString('base64url')
}

export function isValidSessionToken(token: string | undefined): boolean {
  if (!token) return false
  try {
    const decoded = Buffer.from(token, 'base64url').toString('utf-8')
    const [payload, signature] = decoded.split('.')
    const expected = crypto
      .createHmac('sha256', process.env.AUTH_SECRET!)
      .update(payload)
      .digest('hex')
    return signature === expected
  } catch {
    return false
  }
}

La lógica: el token guarda un payload (aquí, simplemente un timestamp) más una firma HMAC de ese payload usando un secreto que solo tu servidor conoce. Al validar, recalculas la firma esperada y la comparas — si alguien intenta fabricar un token sin conocer AUTH_SECRET, la firma jamás va a coincidir. node:crypto solo está disponible porque proxy.ts corre en runtime de Node.js, no en el Edge Runtime limitado de las versiones anteriores de middleware.

Capa 2: un layout protegido, con route groups

Aquí es donde entra la segunda barrera, completamente independiente del proxy. Usamos un route group — una carpeta entre paréntesis que organiza rutas sin afectar la URL final — para que /admin/login quede fuera de esta protección mientras todo lo demás bajo /admin sí la tiene.

src/app/admin/
  login/
    page.tsx              # pública, sin protección
  (protected)/
    layout.tsx             # segunda barrera
    productos/
      page.tsx              # → URL final: /admin/productos
// src/app/admin/(protected)/layout.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { isValidSessionToken, SESSION_COOKIE } from '@/lib/auth'

export default async function ProtectedAdminLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const token = (await cookies()).get(SESSION_COOKIE)?.value

  if (!isValidSessionToken(token)) {
    redirect('/admin/login')
  }

  return <>{children}</>
}

El nombre del carpeta (protected) no aparece en la URL — src/app/admin/(protected)/productos/page.tsx sirve exactamente /admin/productos. Es puramente organizacional, pero aquí cumple una función real: agrupa visualmente todas las rutas que deben pasar por esta segunda verificación.

Por qué esto no es “código redundante”

La objeción obvia es: “si el proxy ya verifica esto, ¿para qué repetirlo en el layout?”. La respuesta está en la naturaleza de las dos capas:

  • El proxy corre antes de que la petición llegue a cualquier lógica de la aplicación — es una capa de infraestructura de ruteo.
  • El layout corre como parte del árbol de React Server Components, con acceso directo al mismo request context, pero como código de aplicación, no de infraestructura.

Son mecanismos técnicamente distintos, ejecutados en momentos distintos del ciclo de vida del request. Un bug, una config de matcher mal ajustada, o incluso una vulnerabilidad futura del propio framework que afecte específicamente al proxy, no necesariamente afecta también al layout — porque no comparten el mismo código de verificación, solo la misma lógica de negocio (¿el token es válido?).

¿Cuándo NO vale la pena esta duplicación?

Para rutas de bajo riesgo (una página que solo personaliza contenido según si hay sesión, sin exponer datos sensibles), una sola capa puede ser suficiente — el costo de mantener dos verificaciones no siempre se justifica. La duplicación tiene sentido específicamente para superficies de alto impacto: paneles administrativos, rutas que exponen datos de otros usuarios, o acciones destructivas (eliminar, modificar en bloque).

Conclusión

Defensa en profundidad no significa “desconfiar de tu propio código” — significa reconocer que ninguna capa individual es infalible para siempre, y que el costo de una segunda verificación independiente es mucho menor que el costo de que una sola capa falle silenciosamente. Para algo tan sensible como un panel admin, ese costo extra de unas pocas líneas vale la pena.

¿Tu panel administrativo depende de una sola capa de autenticación? Agenda una auditoría de seguridad gratuita de 15 minutos y lo revisamos juntos.

¿Tienes un proyecto en mente?

Convierte tu idea en un producto real

Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.

Solicitar presupuesto Ver LinkedIn