Autenticación y Gestión de Usuarios en Webs
Cuándo usar
Cuando tengas que añadir login, registro, sesión o gestión de usuarios a una web (dashboards privados,
apps con cuentas, APIs protegidas, áreas de cliente).
Decisión previa: ¿necesitas backoffice propio o un proveedor?
Antes de escribir código, decide el enfoque según el caso:
- Web estática / sin backend (GitHub Pages, Vercel, vite build): NUNCA pongas la validación en el navegador
(es trasteable). Usa un servicio BaaS (Supabase/ Firebase) o un provider OAuth — el login se valida en su servidor.
- App Node/backend propio: Auth.js (antes NextAuth) o un servidor con JWT + httpOnly cookie (patrón SOLIDO).
- Solo "área logueada" sin usuarios complejos: un provider OAuth (Google, GitHub) con un único botón.
Patrón recomendado: SPA + backend JWT en httpOnly cookie
El patrón más robusto para un SPA:
login (POST /auth/login → email+password)
→ backend verifica (bcrypt) y emite JWT firmado
→ se guarda en cookie httpOnly + SameSite=Lax + Secure
→ frontend llama a /me y /logout
- Password: hashear con bcrypt/argon2 (nunca en texto plano, nunca MD5/SHA1).
- Token: JWT con expiración corta (access ~15min) + refresh token en cookie httpOnly.
- Cookie:
httpOnly (no legible por JS → mitiga XSS), SameSite=Lax (mitiga CSRF), Secure (solo HTTPS).
- CSRF: con SameSite=Lax y peticiones con header custom, en la mayoría de casos es suficiente; si no, token anti-CSRF.
- Revalidación: nunca confíes solo en el frontend; cada endpoint protegido debe verificar el token en el server.
Alternativas listas (sin montar auth a mano)
| Opción |
Cuándo |
Pros |
| Supabase Auth |
Web/SPA con backend o sin él |
Gmail, GitHub, magic link, email+pass; JS SDK; RLS en Postgres para datos por usuario |
| Firebase Auth |
App móvil/web |
Integración ecosistema Firebase, social login, anónimo |
| Auth.js (NextAuth) |
App Next.js |
Middleware por proveedor, sesión con JWT o DB, Google/GitHub/OAuth + credentials |
| Clerk / Auth0 |
Sin querer gestionar |
Login gestionado, multi-tenant, listo para producto, con costo |
| Casdoor / Keycloak |
SSO corporativo/self-hosted |
AD/LDAP, OIDC, MFA, self-hosted |
Regla David: para dashboards temporales o demos, Supabase Auth (o un provider OAuth directo) es lo más rápido
y sin secretos en el repo. Si luego crece, migra a backend propio con JWT.
Supabase Auth — receta rápida
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, anonKey) // anonKey es público (RLS protege los datos)
// Registrar
await supabase.auth.signUp({ email, password })
// Login
await supabase.auth.signInWithPassword({ email, password })
// Provider social
await supabase.auth.signInWithOAuth({ provider: 'google' })
// Sesión actual / logout
supabase.auth.getSession(); await supabase.auth.signOut()
Protege los datos con Row Level Security (RLS) en Postgres:
alter table perfiles enable row level security;
create policy "cada usuario ve su perfil"
on perfiles for select using (auth.uid() = id);
Patrón multi-tenant (varios clientes/usuarios con roles)
- Añade columna
tenant_id/org_id + columna role ('admin','user','viewer').
- Usa RLS o middleware que filtre por el tenant del usuario en todo query.
- Nunca asumas el rol desde el frontend — verifícalo en el server.
Pitfalls
- XSS: en un SPA con token en localStorage, un XSS roba el token. Usa cookie httpOnly siempre que puedas.
- CSRF: cookies con
SameSite=Lax o envía header custom X-Requested-With.
- Secrets: la service_role / api key de servidor va en
.env, NUNCA en el repo. Ya es regla del sistema.
- JWT sin expiración o sin verificar firma = puerta abierta. Verifica siempre (firma + aud + exp).
- CORS: el frontend y el auth deben tener el origen correcto o el login falla silenciosamente.
- Migración: si ya tenías contraseñas en claro, fuerza reset; no hay forma segura de "adivinar" el hash.
Verificación
- Registro → login →
/me devuelve el usuario correcto.
- Deslogueado → endpoint protegido devuelve 401 (no datos).
- Token inválido/expirado → 401, no crash.
- Revisa en DevTools la cookie:
httpOnly, Secure, SameSite.
- Doble pestaña o incógnito → la sesión es por navegador/expira correctamente.
Referencia
1---2name: web-auth-user-management3description: Montar login y gestión de usuarios en webs y apps.4license: MIT5---67# Autenticación y Gestión de Usuarios en Webs89## Cuándo usar1011Cuando tengas que añadir **login, registro, sesión o gestión de usuarios** a una web (dashboards privados,12apps con cuentas, APIs protegidas, áreas de cliente).1314## Decisión previa: ¿necesitas backoffice propio o un proveedor?1516Antes de escribir código, decide el enfoque según el caso:17181. **Web estática / sin backend** (GitHub Pages, Vercel, vite build): **NUNCA pongas la validación en el navegador**19 (es trasteable). Usa un **servicio BaaS** (Supabase/ Firebase) o un **provider OAuth** — el login se valida en su servidor.202. **App Node/backend propio**: Auth.js (antes NextAuth) o un servidor con **JWT + httpOnly cookie** (patrón SOLIDO).213. **Solo "área logueada" sin usuarios complejos**: un **provider OAuth** (Google, GitHub) con un único botón.2223## Patrón recomendado: SPA + backend JWT en httpOnly cookie2425El patrón más robusto para un SPA:2627```28login (POST /auth/login → email+password)29 → backend verifica (bcrypt) y emite JWT firmado30 → se guarda en cookie httpOnly + SameSite=Lax + Secure31 → frontend llama a /me y /logout32```3334- **Password: hashear con bcrypt/argon2** (nunca en texto plano, nunca MD5/SHA1).35- **Token: JWT con expiración corta** (access ~15min) + **refresh token** en cookie httpOnly.36- **Cookie**: `httpOnly` (no legible por JS → mitiga XSS), `SameSite=Lax` (mitiga CSRF), `Secure` (solo HTTPS).37- **CSRF**: con SameSite=Lax y peticiones con header custom, en la mayoría de casos es suficiente; si no, token anti-CSRF.38- **Revalidación**: nunca confíes solo en el frontend; cada endpoint protegido debe verificar el token en el server.3940## Alternativas listas (sin montar auth a mano)4142| Opción | Cuándo | Pros |43|--------|--------|------|44| **Supabase Auth** | Web/SPA con backend o sin él | Gmail, GitHub, magic link, email+pass; JS SDK; RLS en Postgres para datos por usuario |45| **Firebase Auth** | App móvil/web | Integración ecosistema Firebase, social login, anónimo |46| **Auth.js (NextAuth)** | App Next.js | Middleware por proveedor, sesión con JWT o DB, Google/GitHub/OAuth + credentials |47| **Clerk / Auth0** | Sin querer gestionar | Login gestionado, multi-tenant, listo para producto, con costo |48| **Casdoor / Keycloak** | SSO corporativo/self-hosted | AD/LDAP, OIDC, MFA, self-hosted |4950**Regla David:** para dashboards temporales o demos, **Supabase Auth** (o un provider OAuth directo) es lo más rápido51y sin secretos en el repo. Si luego crece, migra a backend propio con JWT.5253## Supabase Auth — receta rápida5455```js56import { createClient } from '@supabase/supabase-js'57const supabase = createClient(url, anonKey) // anonKey es público (RLS protege los datos)5859// Registrar60await supabase.auth.signUp({ email, password })61// Login62await supabase.auth.signInWithPassword({ email, password })63// Provider social64await supabase.auth.signInWithOAuth({ provider: 'google' })65// Sesión actual / logout66supabase.auth.getSession(); await supabase.auth.signOut()67```6869Protege los datos con **Row Level Security (RLS)** en Postgres:7071```sql72alter table perfiles enable row level security;73create policy "cada usuario ve su perfil"74 on perfiles for select using (auth.uid() = id);75```7677## Patrón multi-tenant (varios clientes/usuarios con roles)7879- Añade columna `tenant_id`/`org_id` + columna `role` ('admin','user','viewer').80- Usa **RLS o middleware** que filtre por el tenant del usuario en todo query.81- Nunca asumas el rol desde el frontend — verifícalo en el server.8283## Pitfalls8485- **XSS**: en un SPA con token en localStorage, un XSS roba el token. Usa **cookie httpOnly** siempre que puedas.86- **CSRF**: cookies con `SameSite=Lax` o envía header custom `X-Requested-With`.87- **Secrets**: la service_role / api key de servidor va en `.env`, NUNCA en el repo. Ya es regla del sistema.88- **JWT sin expiración** o sin verificar firma = puerta abierta. Verifica siempre (firma + aud + exp).89- **CORS**: el frontend y el auth deben tener el origen correcto o el login falla silenciosamente.90- **Migración**: si ya tenías contraseñas en claro, fuerza reset; no hay forma segura de "adivinar" el hash.9192## Verificación93941. Registro → login → `/me` devuelve el usuario correcto.952. Deslogueado → endpoint protegido devuelve 401 (no datos).963. Token inválido/expirado → 401, no crash.974. Revisa en DevTools la cookie: `httpOnly`, `Secure`, `SameSite`.985. Doble pestaña o incógnito → la sesión es por navegador/expira correctamente.99100## Referencia101102- Supabase Auth: https://supabase.com/docs/guides/auth103- Auth.js: https://authjs.dev104- OWASP Auth Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html