Login Flow
Admin UI Flow
- User visits any
/admin/*route - Gate 1:
require_authcheck — if no auth collections exist andrequire_authistrue(default), returns a “Setup Required” page (HTTP 503). Setrequire_auth = falsein[admin]for open dev mode. - Auth middleware checks for
crap_sessionHttpOnly cookie (includesSecureflag whendev_mode = false) - If no valid cookie, tries custom auth strategies, then redirects to
/admin/login - Gate 2:
admin.accesscheck — if anaccessLua function is configured in[admin], it runs after successful authentication. If the function returnsfalse/nil, the user sees an “Access Denied” page (HTTP 403) with a logout button. - User submits email + password (protected by CSRF double-submit cookie)
- Server checks rate limiting — too many failed attempts for this email triggers a temporary lockout
- Server verifies credentials against the auth collection (constant-time, even for non-existent users)
- On success: clears rate limit counter, sets
crap_sessioncookie with JWT, redirects to/admin - On failure: records failed attempt, re-renders login page with error
Public admin routes (no auth required):
/admin/login/admin/logout/admin/forgot-password/admin/reset-password/admin/verify-email/static/*
Custom strategy flow: If custom strategies are configured, the middleware checks them before redirecting to login:
- JWT cookie check (fast path)
- Custom strategies in definition order
- Redirect to login (if all fail)
Security
Rate Limiting
Login and forgot-password endpoints enforce dual rate limiting — per-email and per-IP:
- Per-email: After
max_login_attempts(default: 5) failed attempts, further login attempts for that email are blocked forlogin_lockout_seconds(default: 300s). - Per-IP: After
max_ip_login_attempts(default: 20) failed attempts from the same IP, all login attempts from that IP are blocked. The higher threshold tolerates shared IPs (offices, NAT).
Forgot-password requests are similarly limited per-email (max_forgot_password_attempts) and per-IP (max_ip_login_attempts with forgot_password_window_seconds).
[auth]
max_login_attempts = 5 # per-email threshold
max_ip_login_attempts = 20 # per-IP threshold (login + forgot-password)
login_lockout_seconds = "5m" # lockout window for login
max_forgot_password_attempts = 3
forgot_password_window_seconds = "15m"
Rate limiting applies to the admin UI login, admin forgot-password, and the gRPC Login and ForgotPassword RPCs. Behind a reverse proxy, the admin UI reads the client IP from X-Forwarded-For.
CSRF Protection
All admin UI form submissions and HTMX requests are protected by a double-submit cookie pattern:
- A
crap_csrfcookie (SameSite=Strict, not HttpOnly) is set when absent (persists with a 24-hour Max-Age) - POST, PUT, PATCH, and DELETE requests must include a matching token via either:
X-CSRF-Tokenheader (used by HTMX requests)_csrfform field (used by plain form submissions)
- Mismatched or missing tokens return 403 Forbidden
This is handled automatically by JavaScript included in the admin templates.
Timing Safety
Login always performs a full Argon2id hash comparison, even when the requested email doesn’t exist. This prevents timing-based user enumeration attacks.
gRPC Flow
Login
grpcurl -plaintext -d '{
"collection": "users",
"email": "admin@example.com",
"password": "secret123"
}' localhost:50051 crap.ContentAPI/Login
Response:
{
"token": "eyJhbGciOi...",
"user": {
"id": "abc123",
"collection": "users",
"fields": { "name": "Admin", "email": "admin@example.com", "role": "admin" }
}
}
Authenticated Requests
Pass the token via authorization metadata:
grpcurl -plaintext \
-H "authorization: Bearer eyJhbGciOi..." \
-d '{"collection": "posts"}' \
localhost:50051 crap.ContentAPI/Find
Get Current User
grpcurl -plaintext -d '{
"token": "eyJhbGciOi..."
}' localhost:50051 crap.ContentAPI/Me
Multiple Auth Collections
You can have multiple auth collections (e.g., users and admins). The Login RPC takes a collection parameter to specify which one to authenticate against.
The admin UI login always tries all auth collections.
Password Reset Flow
When email is configured ([email] section in crap.toml):
Admin UI
- User clicks “Forgot password?” on the login page
- Enters their email address and selects the auth collection
- Server generates a nanoid reset token with 1-hour expiry
- Reset email is sent with a link to
/admin/reset-password?token=xxx - User clicks the link, enters a new password
- Server validates the token, updates the password, and redirects to login
gRPC
# Step 1: Request password reset
grpcurl -plaintext -d '{
"collection": "users",
"email": "admin@example.com"
}' localhost:50051 crap.ContentAPI/ForgotPassword
# Step 2: Reset password with token from email
grpcurl -plaintext -d '{
"collection": "users",
"token": "the-token-from-email",
"new_password": "newsecret123"
}' localhost:50051 crap.ContentAPI/ResetPassword
Note: ForgotPassword always returns success to prevent user enumeration.
Email Verification Flow
When verify_email: true is set on an auth collection:
Admin UI
- User is created (via admin form or gRPC)
- Verification email is sent automatically with a link to
/admin/verify-email?token=xxx - Verification tokens expire after 24 hours
- User clicks the verification link (expired tokens show an error)
- Login attempts before verification return “Please verify your email”
gRPC
grpcurl -plaintext -d '{
"collection": "users",
"token": "the-token-from-email"
}' localhost:50051 crap.ContentAPI/VerifyEmail