wandres.dev
AUTH Y MIDDLEWARE · sesiones y protección

Un flujo de auth completo: login, sesión, middleware y logout

La síntesis del nivel: montar la autenticación de punta a punta con las piezas ya vistas. Un `login` modelado como `action` que verifica credenciales, siembra la sesión con `session.update` y hace `throw redirect`; un formulario con mejora progresiva que muestra el error devuelto vía `useSubmission`; un middleware que puebla `event.locals.user` desde la sesión como acelerador de la petición; el guardia `requireUser` que decide junto al dato sin depender del middleware; y un `logout` que hace `session.clear` y redirige. Cinco piezas, un solo bucle: la autoridad se acuña una vez y se verifica en todas partes.

⏱ 18 min

Tienes todas las piezas sueltas: sesiones selladas, middleware con event.locals, cookies endurecidas y guardias junto al dato. Esta lección las ensambla en un flujo de autenticación completo y las hace hablar entre sí. Verás cómo un login se modela como una action que verifica credenciales y acuña autoridad sembrando la sesión; cómo un middleware puebla event.locals.user en cada petición de servidor para que el resto del código no relea la sesión; cómo el guardia decide junto al dato sin depender jamás de que el middleware haya corrido; y cómo un logout revoca esa autoridad limpiando la sesión. Cinco verbos —acuñar, portar, poblar, decidir, revocar— y un solo bucle que se cierra sobre sí mismo. Este es el patrón que sostiene la mayoría de las apps con usuarios de SolidStart.

🎯 Al terminar esta lección sabrás
  • Modelar el login como action que verifica, siembra la sesión con session.update y hace throw redirect.
  • Construir un formulario con mejora progresiva que muestre el error devuelto por la acción vía useSubmission.
  • Poblar event.locals.user desde un middleware como acelerador, sin que la seguridad dependa de él.
  • Cerrar el círculo con un logout que hace session.clear y una ruta protegida por requireUser.

El mapa del flujo

Antes del código, la coreografía. El usuario envía sus credenciales; la acción de servidor las verifica y, si son válidas, escribe su identidad en la sesión sellada y lo redirige a la zona privada. En cada petición posterior, el middleware lee esa sesión y deja el usuario resuelto en event.locals, y el guardia de la ruta vuelve a confirmar junto al dato. El logout limpia la sesión y devuelve al login.

sequenceDiagram
participant U as Usuario
participant F as Formulario login
participant A as loginAction en servidor
participant S as Sesion sellada
participant M as Middleware
participant P as Ruta panel
U->>F: envia email y password
F->>A: dispara la action
A->>S: session update con userId
A-->>U: throw redirect a panel
U->>M: peticion a panel
M->>S: lee la sesion
M->>M: puebla event locals user
P->>P: requireUser confirma junto al dato
P-->>U: panel renderizado

Login: una action que verifica y siembra la sesión

El login es una mutación: cambia el estado del servidor, así que se modela con action. Verifica las credenciales; si fallan, devuelve un Error para que el formulario lo pinte; si aciertan, siembra la sesión y lanza una redirección. Esa asimetría es deliberada: devolver comunica un resultado que la interfaz muestra; lanzar un redirect cambia de página.

// src/lib/auth.ts
import { action, redirect } from "@solidjs/router";
import { usarSesion } from "./sesion";
import { verificarCredenciales } from "./db";

export const loginAction = action(async (formData: FormData) => {
  "use server";
  const email = String(formData.get("email"));
  const password = String(formData.get("password"));

  const user = await verificarCredenciales(email, password);
  if (!user) return new Error("Credenciales invalidas"); // el form lo muestra

  const sesion = await usarSesion();
  await sesion.update({ userId: user.id });   // acuna autoridad: siembra la sesion
  throw redirect("/panel");                   // exito: cambia de pagina
}, "login");

export const logoutAction = action(async () => {
  "use server";
  const sesion = await usarSesion();
  await sesion.clear();       // revoca la autoridad de este cliente
  throw redirect("/login");
}, "logout");

El formulario ata la acción con action={loginAction} y method="post", lo que le da mejora progresiva: funciona incluso sin JavaScript, porque es un envío de formulario real. useSubmission expone el envío en vuelo —su pending para deshabilitar el botón, su result para leer el Error devuelto—.

// src/routes/login.tsx
import { useSubmission } from "@solidjs/router";
import { Show } from "solid-js";
import { loginAction } from "~/lib/auth";

export default function Login() {
  const enviando = useSubmission(loginAction);
  return (
    <form action={loginAction} method="post">
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button disabled={enviando.pending}>Entrar</button>
      <Show when={enviando.result instanceof Error}>
        <p role="alert">{(enviando.result as Error).message}</p>
      </Show>
    </form>
  );
}

El middleware que puebla el usuario

Con la sesión sembrada, cada petición de servidor puede resolver al usuario una sola vez y dejarlo listo para todos. Ese es el trabajo del middleware: leer la sesión y depositar el resultado en event.locals.user. Es un acelerador, no un guardia; su función es ahorrar relecturas, no autorizar.

// src/middleware/index.ts
import { createMiddleware } from "@solidjs/start/middleware";
import { usarSesion } from "~/lib/sesion";
import { buscarUsuario } from "~/lib/db";

export default createMiddleware({
  onRequest: async (event) => {
    const sesion = await usarSesion();
    const userId = sesion.data.userId;
    event.locals.user = userId ? await buscarUsuario(userId) : null;
  },
});

Recuerda el getUser de la lección anterior: usaba event.locals.user como atajo pero volvía a leer la sesión si el atajo faltaba. Ahí está la robustez del diseño: si el middleware corrió, el guardia va rápido; si no corrió —una navegación de cliente—, el guardia lee la sesión él mismo. La corrección nunca cuelga del middleware; el middleware solo la abarata.

Logout y la ruta protegida: el círculo se cierra

El logout es la acción inversa del login: session.clear revoca la autoridad y redirect devuelve al login. Y la ruta protegida reutiliza requireUser en su preload, de modo que solo un usuario con sesión válida la ve. El bucle está cerrado: se entra sembrando la sesión, se permanece porque el guardia la valida, se sale limpiándola.

// src/routes/panel.tsx
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { logoutAction } from "~/lib/auth";
import { requireUser } from "~/lib/guardias";

export const route = {
  preload: () => requireUser(),   // guardia: sin sesion, redirige a login
} satisfies RouteDefinition;

export default function Panel() {
  const user = createAsync(() => requireUser());
  return (
    <>
      <h1>Hola, {user()?.email}</h1>
      <form action={logoutAction} method="post">
        <button>Salir</button>
      </form>
    </>
  );
}
🪙

Acuñar en el login

La action verifica y siembra la sesion con session update. Ahi nace la autoridad, una sola vez.

⚙️

Poblar en el middleware

onRequest deja event locals user resuelto para la peticion. Acelera, no autoriza.

🔁

Revocar en el logout

session clear borra la sesion y redirige. La misma cookie que dio acceso lo retira.

⚠️
Devolver errores, lanzar redirecciones: las dos salidas de una acción

Una action tiene dos formas de terminar y conviene no mezclarlas. Devuelve los errores que el usuario debe ver —credenciales inválidas, un campo mal— para que aparezcan en useSubmission().result y el formulario se re-renderice con el mensaje, algo que funciona incluso sin JavaScript. Lanza las redirecciones —throw redirect(...)— porque son respuestas HTTP que el router ejecuta como navegación. Y valida siempre en el servidor: la validación de cliente mejora la experiencia, pero un atacante envía el formulario que quiere, así que la comprobación real de credenciales vive dentro del "use server", nunca en el navegador.

La autenticación es un ciclo de autoridad: acuñar una vez, verificar siempre, revocar al final

Si te llevas una sola imagen de todo este nivel, que sea esta: la autenticación es la gestión del ciclo de vida de una autoridad, y cada pieza que has montado ocupa un momento distinto de ese ciclo. La autoridad se acuña una única vez, en el login, cuando el servidor verifica quién eres y sella tu identidad en una cookie con un secreto que solo él conoce; a partir de ese instante, la cookie es tu credencial. Esa credencial se porta de forma ambiental: el navegador la adjunta a cada petición sin que nadie se lo pida, y por eso las lecciones de cookies importaban tanto, porque endurecer esa cookie —httpOnly, secure, sameSite— es controlar bajo qué condiciones el navegador está dispuesto a rendir la credencial que porta. En cada petición, la autoridad se puebla: el middleware la resuelve una vez y la deja en event.locals para que el resto del servidor la lea barata, pero —y esta es la lección que separa lo robusto de lo frágil— poblar no es decidir. La decisión, el verificar, ocurre siempre junto al dato, en el guardia que hace throw redirect cuando no hay sesión, porque ese es el único punto por el que todo acceso pasa forzosamente y por tanto el único donde una comprobación no se puede eludir. El middleware que a veces no corre acelera esa verificación pero jamás la sustituye; si el atajo falta, el guardia relee la sesión y decide igual. Y al final la autoridad se revoca, en el logout que limpia la sesión, cerrando el bucle que el login abrió. Acuñar, portar, poblar, verificar, revocar: cinco momentos, cinco piezas, una sola sustancia que fluye entre ellas. Cuando ves la autenticación así —no como una colección de recetas inconexas sino como el gobierno del ciclo de una credencial— dejas de preguntarte «¿qué función uso para esto?» y empiezas a preguntarte «¿en qué momento del ciclo estoy y quién tiene la autoridad aquí?». Con esa pregunta, cualquier requisito de auth que te echen —roles, permisos finos, sesiones en base de datos, tokens de terceros— encuentra su sitio en el ciclo sin romperlo, porque solo estás añadiendo detalle a un esqueleto que ya entiendes de arriba abajo.

⚔️ Cierra el bucle de la autoridad
  1. Implementa loginAction que verifique credenciales, devuelva un Error si fallan y siembre la sesión con session.update antes de throw redirect("/panel").
  2. Monta el formulario de login con action={loginAction} y useSubmission; desactiva el botón con pending y pinta el Error devuelto; pruébalo con y sin JavaScript.
  3. Añade el middleware que puebla event.locals.user desde la sesión y confirma con getRequestEvent que otras funciones de servidor lo leen sin relecturas.
  4. Desconecta a propósito el middleware y comprueba que la ruta protegida sigue segura porque requireUser relee la sesión: la seguridad no dependía de él.
  5. Implementa logoutAction con session.clear y verifica que, tras salir, el preload de /panel te redirige a /login: el círculo acuñar–verificar–revocar queda cerrado.