export JWT_SECRET="minimum-32-character-secret-key"
Boundary’s user library handles the full authentication lifecycle:
user registration, login, JWT tokens, sessions, and multi-factor authentication.
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
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"}
}
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"}}}]
MFA uses TOTP (Time-based One-Time Passwords, compatible with Google Authenticator / Authy).
# 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"}'
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "secret", "mfaCode": "123456"}'
curl -X POST http://localhost:3000/api/auth/mfa/disable \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"verificationCode": "123456"}'
# 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>"
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:configRequest listener copies that token into the
X-CSRF-Token header automatically — no per-form work.
Plain <form method=post> forms carry it in a hidden __anti-forgery-token field
(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.
JWT_SECRET must 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 user library enforces account lockout after repeated failed login attempts
See user library for the full reference.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |