Skip to Content
WorkerAutenticación

Autenticación

Sistema de autenticación propio implementado íntegramente con Web Crypto API — sin dependencias externas. El worker emite y verifica JWTs; el frontend los almacena en localStorage y los envía como Bearer token en cada request.


Arquitectura

Frontend Worker │ │ ├─ POST /auth/signup ───────────►│ 1. Verifica que el email no exista │ { email, password, │ 2. Hashea password (PBKDF2 + salt) │ fullName } │ 3. Guarda en tabla users (Supabase) │◄─ { token, user } ────────────┤ 4. Emite JWT firmado (HMAC-SHA256) │ │ ├─ POST /auth/signin ───────────►│ 1. Busca usuario por email │ { email, password } │ 2. Verifica PBKDF2 hash │◄─ { token, user } ────────────┤ 3. Emite JWT firmado │ localStorage.setItem("auth_token", token) ├─ GET /api/reservations ───────►│ Lee Authorization header │ Authorization: Bearer <tok> │ Verifica JWT con Web Crypto │◄─ { data: [...] } ────────────┤ Extrae userId del claim `sub`

Endpoints

POST /auth/signup

Crea un usuario nuevo. Devuelve un JWT listo para usar.

Body:

{ "email": "usuario@email.com", "password": "min8caracteres", "fullName": "Nombre Completo" }

Respuesta 201:

{ "data": { "token": "eyJhbGc...", "user": { "id": "uuid", "email": "usuario@email.com", "fullName": "Nombre Completo" } } }

Errores:

  • 409 — el email ya está registrado
  • 400 — campos faltantes o contraseña menor a 8 caracteres

POST /auth/signin

Autentica un usuario existente.

Body:

{ "email": "usuario@email.com", "password": "la-contraseña" }

Respuesta 200: misma estructura que /auth/signup.

Errores:

  • 401 — email no encontrado o contraseña incorrecta

Hashing de contraseñas (PBKDF2)

Implementado en src/auth.ts usando la Web Crypto API nativa de Cloudflare Workers.

ParámetroValor
AlgoritmoPBKDF2 con SHA-256
Iteraciones100,000
Salt16 bytes aleatorios (único por contraseña)
Longitud del hash256 bits

Formato de almacenamiento en Supabase:

Salt y hash se concatenan y se codifican en Base64 para guardarse en la columna password_hash:

stored = base64( salt[16 bytes] + hash[32 bytes] )

Por qué no bcrypt: bcrypt depende de la implementación nativa de Node.js, no disponible en el edge runtime de Cloudflare Workers. PBKDF2 con 100,000 iteraciones y SHA-256 ofrece seguridad equivalente y es nativo en Workers.


JWT (HMAC-SHA256 con Web Crypto)

Los tokens se firman con HMAC-SHA256 usando la Web Crypto API. La clave es JWT_SECRET, configurado como variable de entorno en el worker.

Claims del token

{ "sub": "uuid-del-usuario", "email": "usuario@email.com", "iat": 1748000000, "exp": 1750592000 }
  • sub — UUID del usuario (columna clerk_user_id en Supabase, reutilizada)
  • exp — Expiración: 30 días desde la emisión
  • iat — Timestamp de emisión

Verificación en el worker

Cada endpoint protegido llama a verifyAuth(request, env) definida en src/auth.ts:

  1. Lee el header Authorization: Bearer <token>
  2. Verifica la firma HMAC-SHA256 con JWT_SECRET
  3. Comprueba que exp no haya vencido
  4. Retorna el sub (userId) o null si el token es inválido/expirado

Schema de base de datos

La tabla users en Supabase necesita la columna password_hash y un constraint de unicidad en email. Ejecutar en el SQL Editor de Supabase:

-- Agregar columna para el hash de contraseña ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT; -- Unicidad en email (usar bloque DO para evitar error si ya existe) DO $$ BEGIN ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email); EXCEPTION WHEN duplicate_object THEN NULL; END; $$;

ALTER TABLE ADD CONSTRAINT IF NOT EXISTS no existe en PostgreSQL. El bloque DO con EXCEPTION es la forma correcta de añadir un constraint de forma idempotente.


Frontend — AuthProvider

El frontend expone el estado de autenticación a través de un React Context en apps/web/lib/auth-context.tsx.

Hook useAuth()

const { user, isLoaded, isSignedIn, getToken, signIn, signUp, signOut } = useAuth();
PropiedadTipoDescripción
user{ id, email, fullName } | nullDatos del usuario autenticado
isLoadedbooleanfalse mientras se verifica el token en localStorage
isSignedInbooleanDerivado de user !== null
getToken()() => string | nullRetorna el JWT almacenado (síncrono)
signIn()async (email, password) => voidLlama a /auth/signin y persiste token
signUp()async (email, password, fullName) => voidLlama a /auth/signup y persiste token
signOut()() => voidLimpia localStorage y resetea estado

Persistencia

El token y los datos del usuario se guardan en localStorage con las claves auth_token y auth_user. Al montar el AuthProvider, se verifica que el token no haya expirado leyendo el claim exp del payload JWT.

Last updated on