soul v4.2.0

@jun/soul/auth

Auth

Login con Google, sesion JWT en cookie y guards para proteger rutas.

Que resuelve#

El ciclo completo de autenticacion: mandar al usuario a Google, recibirlo de vuelta, crear o encontrar su cuenta, dejarle una sesion firmada y ofrecer los guards para proteger rutas. Tu no escribes nada de OAuth.

En el primer login soul tambien crea la organizacion personal del usuario y su membresia como owner. Es la invariante que sostiene todo lo demas: todo usuario pertenece siempre al menos a una organizacion.

Montarlo#

TypeScript
import { createAuth } from "@jun/soul/auth";
import { createShell } from "@jun/soul/ui";

const { shell, errorPage } = createShell({ siteName: "Mi SaaS" });

export const auth = createAuth({ errorPage });
TypeScript
// en src/index.ts
app.route("/auth", auth.routes);   // GET /login, /callback, /logout

Opciones#

errorPage: (titulo, mensaje) => string
Requerido. La pagina de error que se muestra ante un fallo.
cookieName?: string
Nombre de la cookie de sesion. Por defecto "soul_session".
sessionTtlSeconds?: number
Vigencia de la sesion. Por defecto 30 dias.
postLoginRedirect?: string
A donde ir tras entrar. Por defecto "/dashboard".
postLogoutRedirect?: string
A donde ir tras salir. Por defecto "/".
loginPath?: string
Ruta del login, usada por los redirects. Por defecto "/auth/login".
statelessUser?: boolean
Por defecto true desde la v4.1. Ver abajo.
sessionHintCookie?: string
Cookie pista para landings estaticas. Ver Static.

Los guards#

auth.requireSession()
Exige sesion valida. Sin ella redirige a loginPath.
auth.requireAdmin()
Exige superadmin de plataforma. Responde 403 si no lo es.

Ambos dejan datos listos en el contexto:

TypeScript
app.get("/perfil", auth.requireSession(), (c) => {
  c.var.session;  // { userId, email, exp }
  c.var.user;     // { name, email, isAdmin }
  return c.text(`Hola ${c.var.user.name}`);
});

Sesiones sin tocar la base#

Desde la v4.1 statelessUser viene encendido. El nombre y el isAdmin viajan firmados como claims dentro del JWT, asi que requireSession() resuelve con cero consultas a D1. En un portal, eso es casi todo el trafico.

Como esta protegido#

  • JWT HS256 con el algoritmo pinneado (no se acepta lo que diga el token).
  • Cookie HttpOnly, SameSite=Lax, y Secure automatico si APP_URL es https.
  • Anti-CSRF en el flujo OAuth: un state aleatorio en cookie con 10 minutos de vida.
  • Del id_token de Google se validan aud y email_verified.