Next.js
Authorization y protección real
Explica autorización por actor, acción, recurso, tenant y estado, con políticas deny-by-default, consultas scoped, transacciones y defensa en profundidad.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica autorización por actor, acción, recurso, tenant y estado, con políticas deny-by-default, consultas scoped, transacciones y defensa en profundidad.
Authorization decide si una identidad autenticada puede ejecutar una acción concreta sobre un recurso concreto dentro de un tenant y estado determinados. La protección real vive en servidor, cerca de los datos y la mutación; ocultar UI, usar una ruta /admin o redirigir desde un layout solo mejora la experiencia.
principal
+ action
+ resource
+ tenant
+ current state
+ policy
→ allow or denyEjemplo:
user 42
+ cancel
+ order 900
+ organization acme
+ order.status = pending
+ role = manager
→ allowedCambiar cualquiera de estas variables puede cambiar la decisión.
authentication
→ ¿quién eres?
authorization
→ ¿puedes hacer esto aquí y ahora?Una sesión válida no autoriza todos los pedidos. Un rol admin tampoco debería saltar límites de tenant sin una política explícita.
Oculta o deshabilita controles:
{permissions.canCancelOrder && <CancelOrderButton />}Aporta claridad, pero el cliente puede modificar el DOM o invocar la operación directamente.
Redirige temprano y evita render innecesario. Es una comprobación optimista basada en sesión/claims.
Consulta solo recursos accesibles:
const order = await db.order.findFirst({
where: {
id: orderId,
organizationId: session.organizationId,
organization: {
members: { some: { userId: session.userId } },
},
},
});Verifica la acción y estado justo antes de mutar.
Constraints y Row-Level Security pueden añadir defensa en profundidad.
Una política segura comienza negando:
type OrderAction = "read" | "update" | "cancel" | "refund";
function can(action: OrderAction, context: PolicyContext): boolean {
switch (action) {
case "read":
return context.isMember;
case "cancel":
return context.isMember && context.order.status === "pending";
case "refund":
return context.role === "owner" && context.order.status === "paid";
default:
return false;
}
}No uses return true como fallback para acciones nuevas.
Role-Based Access Control agrupa permisos por rol:
viewer → read
staff → read, update
owner → read, update, billing, membersAttribute-Based Access Control evalúa atributos:
user.department == resource.department
AND resource.classification <= user.clearance
AND request.time within scheduleAporta granularidad, pero las políticas son más difíciles de explicar, probar y auditar.
Relationship-Based Access Control se basa en relaciones:
user is member of organization
organization owns project
project contains document
→ user may read documentEs natural para SaaS, colaboración y jerarquías. Puede requerir un motor de políticas cuando las relaciones crecen.
return resource.ownerId === principal.userId;Ownership es una regla, no un sistema completo. Considera delegación, equipos, recursos compartidos y administradores.
Toda query privada debe incluir tenant:
await db.project.findFirst({
where: {
id: projectId,
organizationId: session.organizationId,
},
});Nunca:
await db.project.findUnique({ where: { id: projectId } });Y después confiar en que la UI solo envió IDs del tenant.
Un miembro accede al recurso de otra organización cambiando el ID.
Un usuario obtiene una acción reservada a un rol superior.
Prueba ambas.
No basta canAccessOrder:
read order
edit address
cancel order
refund payment
view internal notesCada acción puede requerir reglas distintas. Define permisos orientados a capacidades del dominio.
if (order.status !== "pending") {
return { status: "conflict" };
}La misma persona puede cancelar antes del envío, pero no después. La política se evalúa con el estado actual dentro de la transacción o inmediatamente antes de escribir.
Time-of-check to time-of-use:
check permission
→ another request changes membership/order
→ mutation executes using stale decisionPara operaciones críticas:
UPDATE.No caches decisiones de autorización sensibles durante demasiado tiempo.
"use server";
export async function cancelOrderAction(orderId: string) {
const session = await requireSession();
const result = await db.$transaction(async (tx) => {
const order = await tx.order.findFirst({
where: {
id: orderId,
organizationId: session.organizationId,
},
});
if (!order) return { status: "notFound" as const };
if (!canCancelOrder(session, order)) {
return { status: "forbidden" as const };
}
if (order.status !== "pending") {
return { status: "conflict" as const };
}
await tx.order.update({
where: { id: order.id },
data: { status: "cancelled" },
});
return { status: "success" as const };
});
if (result.status === "success") {
updateTag(`order:${orderId}`);
}
return result;
}La Action no confía en un role o organizationId del cliente.
No existe autenticación válida.
Existe identidad, pero no permiso.
El recurso no existe o decides ocultar su existencia.
Para recursos privados, responder 404 a usuarios sin relación puede reducir enumeración. Internamente registra la razón real.
Next.js ofrece convenciones unauthorized/forbidden bajo APIs que deben verificarse por versión; algunas siguen requiriendo flags experimentales.
Guardar roles en cookie/JWT reduce consultas, pero puede quedar obsoleto después de revocar acceso.
Opciones:
sessionVersion.Peligro:
async function getProject(projectId: string) {
"use cache";
return db.project.findUnique({ where: { id: projectId } });
}Si devuelve datos privados sin scope, la entrada puede compartirse.
Patrones seguros:
No caches un boolean canRefund por horas si el rol puede revocarse inmediatamente.
PostgreSQL RLS puede filtrar filas según contexto de sesión/tenant.
RLS complementa DAL y tests; no reemplaza autorización de acciones ni minimización de DTOs.
export function canManageMembers(
principal: Principal,
organization: OrganizationPolicyView,
): boolean {
return principal.organizationId === organization.id && principal.role === "owner";
}Buenas propiedades:
No disperses if (role === ...) por componentes y handlers.
Acciones sensibles deben registrar:
who
what action
which resource/tenant
when
result
request ID
relevant before/after stateNo registres secretos. Los logs de auditoría deben ser append-only y protegidos contra modificación si son requisito de compliance.
Una función “ver como usuario” necesita:
No reemplaces el principal original por completo.
Cuando cambia un rol:
Una pestaña abierta puede conservar UI vieja; el servidor sigue bloqueando la operación.
Matriz mínima:
same tenant + right role → allow
same tenant + wrong role → deny
other tenant + right role → deny
owner resource → expected
resource state changed → conflict/deny
revoked session → denyIncluye tests de Action/Handler directos, no solo navegación.
Fuzz IDs y prueba enumeración.
El cliente es manipulable.
Nunca es autoridad.
IDOR/escalación horizontal.
TOCTOU.
Rompe aislamiento de tenant.
Revocaciones no surten efecto.
No cubre todas las operaciones.
Inconsistencia y difícil auditoría.
canRefund por un día?Data Access Layer y DTOs centraliza estas políticas cerca de consultas y controla exactamente qué datos llegan al árbol React.