Authentication
Opt-in session login, CSRF, env-var bootstrap, and the setup-required flow.
Opt-in session login, CSRF, env-var bootstrap, and the setup-required flow.
Dashboard authentication is opt-in and off by default. Out of the box the dashboard serves openly — no setup screen, no login, no CSRF or roles — which suits local development on a trusted machine. Production deployments should enable authentication (or keep the dashboard on a private network behind their own auth).
from flexiq.dashboard import serve_dashboard
from myapp import queue
serve_dashboard(queue, auth_enabled=True)flexiq dashboard --app myapp:queue --authpython manage.py flexiq_dashboard --authOr set FLEXIQ_DASHBOARD_AUTH = True in settings.py to make it the
management command's default.
With auth disabled (the default), every API route serves without a
session and the auth endpoints respond 404 {"error": "auth_disabled"} —
except GET /api/auth/status, which reports
{"auth_enabled": false, "setup_required": false} so the SPA skips the
login flow entirely.
With auth enabled, the full session flow applies. Until the first
admin exists, every protected API route returns 503 setup_required and
the SPA shows the one-time setup form. Once an admin is registered the
dashboard requires a valid session cookie on every API call and a CSRF
token on every state-changing request. Everything below this point
describes the auth-enabled dashboard.
dashboard_settings
key/value table — no new schema, so SQLite, PostgreSQL, and Redis
backends are supported uniformly.hashlib.pbkdf2_hmac (SHA-256,
600,000 iterations, 16-byte random salt — the OWASP 2023+ PBKDF2
baseline). No third-party crypto dependency.auth:session:<random_token> with a 24-hour TTL. The token rides in
an HttpOnly + SameSite=Strict + Secure cookie named
flexiq_session (the Secure flag means HTTPS-only; for local HTTP
dev use serve_dashboard(secure_cookies=False) or
flexiq dashboard --insecure-cookies).flexiq_csrf carries a per-session token that the SPA reads and
echoes back via the X-CSRF-Token header on POST/PUT/DELETE. The
server rejects any state-changing request whose header doesn't match
both the cookie and the session-bound token.On a fresh database the dashboard refuses to do anything else until an admin exists.

The form submits to POST /api/auth/setup, which is allowed to run
exactly once — it returns 400 setup already complete after the first
user is created. The new admin is signed in automatically.
For headless deployments (Docker, Kubernetes, systemd) you usually don't want to visit a browser just to register the first user. Set both env vars before starting the dashboard:
export FLEXIQ_DASHBOARD_ADMIN_USER=admin
export FLEXIQ_DASHBOARD_ADMIN_PASSWORD='change-me-on-first-login'
flexiq dashboard --app myapp:queue --authThe bootstrap is idempotent — once a user with that name exists, subsequent dashboard restarts read the env vars but skip creation. The env vars are only read when auth is enabled.
Rotate the password after first login (use POST /api/auth/change-password
or the future UI). Leaving the env var in your deployment is fine for
recovery, but anyone with access to the env can re-bootstrap a fresh
install — keep it scoped accordingly.
After setup, every visit routes through the sign-in form.

# Login from the CLI — note the cookie jar so subsequent requests
# carry the session.
curl -c jar -X POST http://localhost:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"change-me-on-first-login"}'
# The CSRF token comes back in a non-HttpOnly cookie. Read it and
# echo it on writes.
CSRF=$(grep flexiq_csrf jar | awk '{print $7}')
curl -b jar -H "X-CSRF-Token: $CSRF" -X POST \
http://localhost:8080/api/queues/default/pauseThe browser SPA does this automatically — the api-client.ts wrapper
reads document.cookie and attaches the header on POST/PUT/DELETE.
All routes live under /api/auth/:
| Method | Path | What it does |
|---|---|---|
GET | /api/auth/status | Public. Returns {auth_enabled: bool, setup_required: bool} |
POST | /api/auth/setup | Public, locks itself after the first user |
POST | /api/auth/login | Returns the user + session and sets cookies |
POST | /api/auth/logout | Invalidates the current session, clears cookies |
GET | /api/auth/whoami | Returns the current user + CSRF token + expiry |
POST | /api/auth/change-password | Requires the current password |
Every other route under /api/ is auth-gated. Public exceptions:
/health (liveness — always open) and the static SPA assets. With auth
enabled, /readiness and /metrics (Prometheus) require either a valid
session or the FLEXIQ_DASHBOARD_METRICS_TOKEN bearer — point scrapers
at the token via Authorization: Bearer <token>. Without auth they stay
public unless that token is set.
The same endpoints work for any HTTP client — Slack bots, deployment scripts, custom dashboards. The minimal workflow:
POST /api/auth/login — save the Set-Cookie valuesflexiq_session cookieflexiq_session cookie + flexiq_csrf
cookie + matching X-CSRF-Token headerimport requests
s = requests.Session()
s.post(
"http://localhost:8080/api/auth/login",
json={"username": "admin", "password": "..."},
)
csrf = s.cookies.get("flexiq_csrf")
s.headers["X-CSRF-Token"] = csrf
# Reads — no CSRF needed.
stats = s.get("http://localhost:8080/api/stats").json()
# Writes — CSRF auto-attached via session headers.
s.post("http://localhost:8080/api/queues/default/pause")Webhook URLs entered through the dashboard are vetted before any delivery happens. By default the server rejects:
http/https schemeslocalhost, *.localhost, *.local, *.internal, *.intranet,
*.lan, *.private169.254.169.254)Set FLEXIQ_WEBHOOKS_ALLOW_PRIVATE=1 to disable the guard for local
development against http://localhost. Production should keep the
guard on.
Native sign-in with Google, GitHub, and any OIDC-compliant provider
(Okta, Auth0, Keycloak, Microsoft Entra) is available alongside
password auth — see SSO (OAuth & OIDC).
Operators can mix-and-match providers or run an OAuth-only deployment
by setting FLEXIQ_DASHBOARD_PASSWORD_AUTH_ENABLED=false.
Two roles exist, and the role is enforced server-side:
403 forbidden.The first user is always an admin; additional users can be created with either role.
POST /api/auth/change-password directly.