Authentication & JWT
Boundary’s user library handles the full authentication lifecycle:
user registration, login, JWT tokens, sessions, and multi-factor authentication.
Prerequisites
export JWT_SECRET="minimum-32-character-secret-key"
The JWT secret must be at least 32 characters. Tests that exercise auth also require this variable:
JWT_SECRET="dev-secret-32-chars-minimum" clojure -M:test:db/h2 :user
Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "secret"}'
Response:
{
"token": "eyJhbG...",
"user": {"id": "...", "email": "user@example.com", "role": "user"}
}
Protected routes
Add the auth interceptor to routes that require authentication:
[{:path "/api/profile"
:methods {:get {:handler 'handlers/get-profile
:interceptors ['auth/require-authenticated]
:summary "Get current user profile"}}}]
Multi-Factor Authentication (MFA)
MFA uses TOTP (Time-based One-Time Passwords, compatible with Google Authenticator / Authy).
Setup flow
# 1. Start MFA setup (returns a secret + QR code URL)
curl -X POST http://localhost:3000/api/auth/mfa/setup \
-H "Authorization: Bearer <token>"
# 2. Enable MFA (verify with first TOTP code)
curl -X POST http://localhost:3000/api/auth/mfa/enable \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"secret": "...", "verificationCode": "123456"}'
Login with MFA enabled
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "secret", "mfaCode": "123456"}'
Disable MFA
curl -X POST http://localhost:3000/api/auth/mfa/disable \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"verificationCode": "123456"}'
Session management
# List active sessions
curl http://localhost:3000/api/auth/sessions \
-H "Authorization: Bearer <token>"
# Revoke a specific session
curl -X DELETE http://localhost:3000/api/auth/sessions/{session-id} \
-H "Authorization: Bearer <token>"
CSRF protection
State-changing web requests are protected against Cross-Site Request Forgery by the
http-csrf-protection interceptor (in the default stack). A POST/PUT/DELETE/PATCH is
validated — and rejected with 403 on a missing or invalid token — when it is either
session-authenticated or targets a /web route. This covers the web UI, /web/admin,
and any session-authenticated /api route. Token-auth API clients that send only a
bearer token (no session cookie) are not CSRF-vulnerable and are not checked.
How a token reaches the browser and back:
-
On a page load the interceptor issues a session-bound token and the shared layout renders
<meta name="csrf-token" content="…">. -
For HTMX requests, a global
htmx:configRequestlistener copies that token into theX-CSRF-Tokenheader automatically — no per-form work. -
Plain
<form method=post>forms carry it in a hidden__anti-forgery-tokenfield (added via(boundary.platform.core.csrf/hidden-field)).
Unauthenticated flows (login, register, MFA) have no session yet, so the token is bound
to a SameSite=Strict csrf-session cookie minted on the page GET and validated on the
subsequent POST.
Configure under :boundary/http :security :csrf:
:security {:csrf {:enabled? true
:secret #or [#env CSRF_SECRET #env JWT_SECRET]
:exempt-paths ["/api/v1/payments/webhook"]}} ; webhooks/callbacks
The secret defaults to JWT_SECRET, so deployments are protected even without an
explicit :csrf block. Disable it (:enabled? false) only in test/dev. List endpoints
that legitimately cannot send a token (PSP webhooks, OAuth callbacks) under
:exempt-paths; a trailing /* matches on a path-segment boundary.
See platform library → CSRF protection for the interceptor internals and token format.
Key security notes
-
JWT_SECRETmust be set; missing it causes startup failures (it is also the default CSRF signing secret) -
CSRF protection is enforced for session-authenticated, state-changing requests — see CSRF protection
-
Internal entities use
:password-hash(kebab-case) — never:password_hash -
The
userlibrary enforces account lockout after repeated failed login attempts
See user library for the full reference.