Login y sesiones (web auth)
Engine v0.5.5+.
El web auth en Synsema es un juego chico de primitivas, cada una con la opción segura como default. Sin framework, sin middleware — un flujo de login son un puñado de líneas en un bloque serve.
-- Doc example: web auth primitives — password hashing, JWT, TOTP and the CSPRNG.
-- (Cookies/sessions ride on `serve`, which doesn't terminate — the serve page shows
-- the full login flow; this doctest asserts the primitives that flow is built from.)
intent: "doc example: web auth primitives"
require random
let phc be password_hash("hunter2")
print("phc → " + slice(phc, 0, 10) + "… verify → " + text(password_verify("hunter2", phc)))
print("session id → " + token())
test "password_hash gives a PHC string; verify is strict, wrong password is false"
let phc be password_hash("hunter2")
assert(starts_with(phc, "$argon2id$"))
assert_eq(password_verify("hunter2", phc), true)
assert_eq(password_verify("wrong", phc), false)
test "token() is 43 chars of base64url — 32 CSPRNG bytes, fine for session ids"
assert_eq(length(token()), 43)
assert_eq(length(random_bytes(16)), 16)
test "jwt: sign/verify roundtrip; wrong key or tampering → nothing"
let tok be jwt_sign({"sub": "u1", "role": "admin"}, "k", {"expires_in": 3600})
let claims be jwt_verify(tok, "k")
assert_eq(claims.sub, "u1")
assert(claims.exp > claims.iat)
assert_eq(jwt_verify(tok, "other-key"), nothing)
assert_eq(jwt_verify(tok + "x", "k"), nothing)
test "totp matches the RFC 6238 vector and verifies within the window"
let seed be bytes("12345678901234567890")
assert_eq(totp(seed, {"digits": 8, "at": 59}), "94287082")
assert_eq(totp_verify(seed, "94287082", {"digits": 8, "at": 59}), true)
assert_eq(totp_verify(seed, "00000000", {"digits": 8, "at": 59}), false)
test "base64url differs from base64: URL-safe alphabet, no padding"
assert_eq(decode(bytes([251, 255]), "base64url"), "-_8")
assert_eq(decode(bytes([251, 255]), "base64"), "+/8=")
assert_eq(bytes("-_8", "base64url"), bytes([251, 255]))
Cookies de sesión
require serve(8080)
require random -- gatea token()/random_bytes
task check_session(token, request) -- 2 params → recibe también el request
let sid be request.cookies.sid
when sid == nothing
give nothing -- nothing → 401
give state_get("sess:" + sid) -- el valor aterriza en request.user
serve on 8080
auth with check_session
route "POST /login"
when password_verify(request.json.pw, stored_phc)
let sid be token()
state_set("sess:" + sid, {"name": request.json.user})
give set_cookie(ok({"ok": true}), "sid", sid, {"max_age": 86400})
give fail(401, "bad credentials")
route "GET /me" requires auth
give request.user
route "POST /logout" requires auth
give clear_cookie(ok({"bye": true}), "sid")
set_cookie(resp, name, value, opts?)envuelve cualquier valor de respuesta. Los defaults son los seguros:Path=/; Secure; HttpOnly; SameSite=Lax. Opts:max_age(segundos),path,domain,secure,http_only,same_site("Strict"|"Lax"|"None"—Noneexigesecure: true).clear_cookie(resp, name, opts?)—path/domaindeben coincidir con los usados al setear.request.cookies— las cookies entrantes como map (sin decodificar; sin header → map vacío).with_header(resp, name, value)— cualquier header de respuesta extra; las llamadas acumulan, los nombres repetidos salen como líneas separadas (lo que elSet-Cookiemúltiple necesita). Los headers de framing/hop-by-hop y los CR/LF son error duro — la header injection no se puede escribir.- Un auth task de 1 parámetro conserva el contrato histórico (solo bearer token).
Contraseñas, tokens, 2FA
require random
let phc be password_hash(pw) -- "$argon2id$…" (parámetros OWASP) — se guarda tal cual
password_verify(pw, phc) -- bool; hash corrupto/desconocido → error, no false
let sid be token() -- 32 bytes del CSPRNG como base64url (43 chars)
let tok be jwt_sign({"sub": id}, secret("JWT_KEY"), {"expires_in": 3600})
jwt_verify(tok, secret("JWT_KEY")) -- map de claims, o nothing ante CUALQUIER falla
let seed be random_bytes(20) -- alta de TOTP
totp(seed) -- código de 6 dígitos actual (perfil sha1/30s)
totp_verify(seed, submitted) -- ventana de ±1 período, constant-time
random_bytes/tokennecesitanrequire random— la misma puerta deny-by-default derandom(). Los transforms puros (password_hash,jwt_,totp) no necesitan capability.jwt_verifypinea el algoritmo a HS256 — un token conalg: "none"o RS256 se rechaza, y toda falla (firma,exp, malformado) es el mismonothing. RS256/ES256 (OIDC de terceros) todavía no está soportado.- El secret de TOTP es bytes: un secret en base32 de una app de QR se decodifica con
bytes(s, "base32"). - CSRF para POSTs con cookies: emití un
token()por sesión, embebelo en el form, comparalo conconstant_time_eqen el handler. - Nunca guardes API keys vivas tal cual: guardá
sha256(key)y compará hashes.
Los argumentos de clave/contraseña aceptan un secret sellado, text o bytes.