Manage users, API keys, JWT tokens, and role-based access control.
duh supports two authentication methods:
- JWT Bearer tokens -- for users who log in with email and password
- API keys -- for programmatic access via the
X-API-Keyheader
Both methods are checked by the API middleware. If no API keys exist in the database and no JWT is provided, the API runs in open mode (no authentication required).
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"password": "strong-password-here",
"display_name": "Alice"
}'Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"user_id": "a1b2c3d4-...",
"role": "contributor"
}New users are assigned the contributor role by default. Registration can be disabled in config after your initial users are created.
!!! warning "Disable registration in production"
After creating your admin user, set registration_enabled = false in config.toml to prevent unauthorized signups.
The CLI lets you create users with a specific role, including admin:
duh user-create \
--email admin@example.com \
--password 'strong-password' \
--name "Admin User" \
--role adminAvailable roles: admin, contributor, viewer.
duh user-listOutput:
a1b2c3d4 admin@example.com Admin User role=admin active
e5f6a7b8 alice@example.com Alice role=contributor active
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"password": "strong-password-here"
}'Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"user_id": "a1b2c3d4-...",
"role": "contributor"
}curl http://localhost:8080/api/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response:
{
"id": "a1b2c3d4-...",
"email": "alice@example.com",
"display_name": "Alice",
"role": "contributor",
"is_active": true
}Include the token in the Authorization header:
curl http://localhost:8080/api/threads \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."- Algorithm: HS256
- Payload:
sub(user ID),exp(expiry),iat(issued at) - Default expiry: 24 hours (configurable)
Tokens are validated on every request by the API key middleware. An expired or invalid token returns HTTP 401.
[auth]
token_expiry_hours = 24Set a shorter expiry for higher security. Users will need to call /api/auth/login again after the token expires.
API keys provide a simpler authentication method for scripts and integrations. They are passed via the X-API-Key header.
curl http://localhost:8080/api/threads \
-H "X-API-Key: duh_abc123..."- Keys are stored as SHA-256 hashes in the database (the raw key is never stored)
- Keys can be revoked by setting a
revoked_attimestamp - Keys can optionally be linked to a user via
user_id
!!! note "API key CLI"
API key management is available through the database. A dedicated duh key create CLI command is planned for a future release.
The following paths do not require authentication:
| Path | Purpose |
|---|---|
/api/health |
Basic health check |
/api/health/detailed |
Detailed health check |
/api/metrics |
Prometheus metrics |
/api/auth/register |
User registration |
/api/auth/login |
User login |
/docs |
OpenAPI documentation |
/openapi.json |
OpenAPI spec |
/redoc |
ReDoc documentation |
/api/share/* |
Shared content |
All other /api/ and /ws/ paths require either a JWT token or API key.
duh uses a hierarchical role system: admin > contributor > viewer.
| Capability | Viewer | Contributor | Admin |
|---|---|---|---|
| Read threads and decisions | Yes | Yes | Yes |
| Create consensus queries | No | Yes | Yes |
| Create threads | No | Yes | Yes |
| Manage users | No | No | Yes |
| Full API access | No | No | Yes |
Endpoints use the require_role dependency to enforce minimum role levels:
require_viewer-- any authenticated userrequire_contributor-- contributors and adminsrequire_admin-- admins only
A user with a higher role automatically passes lower role checks. For example, an admin can access all contributor endpoints.
As a viewer (read-only access):
# List threads -- works for viewers
curl http://localhost:8080/api/threads \
-H "Authorization: Bearer $VIEWER_TOKEN"
# Create a query -- fails with 403
curl -X POST http://localhost:8080/api/ask \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"question": "test"}'
# {"detail": "Requires contributor role"}As a contributor (create and view):
# Create a consensus query -- works for contributors
curl -X POST http://localhost:8080/api/ask \
-H "Authorization: Bearer $CONTRIBUTOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"question": "What are the trade-offs of microservices?"}'As an admin (full access):
# List users -- admin only
curl http://localhost:8080/api/auth/me \
-H "Authorization: Bearer $ADMIN_TOKEN"All authentication settings live in the [auth] section of config.toml:
[auth]
jwt_secret = "" # REQUIRED in production -- set via env or config
token_expiry_hours = 24 # how long JWT tokens remain valid
registration_enabled = true # set to false after creating your admin userSet DUH_JWT_SECRET as an environment variable instead of putting it in the config file:
export DUH_JWT_SECRET=$(openssl rand -hex 32)!!! warning "Never commit your JWT secret" Use environment variables or a secrets manager for the JWT secret. Never check it into version control.
Rate limits apply per identity. The middleware identifies callers in this priority order:
- User ID (from JWT token)
- API key ID (from
X-API-Keyheader) - IP address (fallback)
Configure rate limits in config.toml:
[api]
rate_limit = 60 # requests per minute
rate_limit_window = 60 # window in secondsWhen the limit is exceeded, the API returns HTTP 429 with a Retry-After header.
Every response includes rate limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Key: user:a1b2c3d4-...
The web UI integrates with the backend authentication system. It detects whether auth is required and adapts accordingly.
When no API keys or users exist in the database, the API runs in open mode. The web UI detects this via GET /api/auth/status and automatically logs in as a guest user. No login page is shown.
This is the default behavior when you first run duh serve -- you can start using the web UI immediately without setting up users.
Once you create a user or API key, the web UI requires authentication:
- Redirect to login: All routes except
/share/:idredirect to/loginif the user is not authenticated - Login form: Enter email and password to receive a JWT token
- Token storage: The JWT token is stored in
localStorage(key:duh_token) - Auto-injection: The API client automatically includes the token in all requests via the
Authorization: Bearerheader - WebSocket auth: The token is included in the initial WebSocket handshake message
- Session expiry: On 401 responses, the stored token is cleared and the user is redirected to login
When authenticated, the top bar shows the user's display name and role badge. Clicking it reveals a dropdown with the user's email and a sign-out button.
The login page includes a toggle to switch between "Sign In" and "Create Account" modes. Registration can be disabled server-side by setting registration_enabled = false in config.toml.
- Generate a strong JWT secret:
openssl rand -hex 32 - Disable registration after creating your first admin user
- Use HTTPS -- never expose the API over plain HTTP
- Rotate API keys periodically and revoke unused ones
- Set short token expiry for high-security environments
- Restrict CORS origins to your actual domain
- Production Deployment -- Full deployment guide with PostgreSQL, Docker, nginx
- Monitoring -- Health checks, metrics, alerting