API Reference
Version 1 · Initial API reference for consuming apps integrating against QDBAuth's JSON API, derived from apps/auth/static/auth/openapi.yaml and apps/auth/views.py
QDBAuth API Reference
Full interactive docs: GET /api/docs/ (Swagger UI over
apps/auth/static/auth/openapi.yaml). This page is the condensed version for
quick lookup while integrating a consuming app.
Every endpoint below except /api/whoami/ requires an X-QDB-Client-Secret
header matching the shared secret (QDB_SSO_SHARED_SECRET) — missing or wrong
returns 401. /api/whoami/ instead takes a Bearer <access_token>
Authorization header.
POST /api/login/
Check email + password.
Body: {"email": "...", "password": "...", "site": "CMS"} — site is a
free-text label. For accounts with per-site credentials
(site_credentials.py), it must exactly match a registered site.site_name or
the lookup falls through to the account's "siteless" credential (if any).
200 success: {"success": true, "pending_token": "...", "enrolled": true}
400 (wrong password, or account not yet 2FA-enrolled):
{"success": false, "error": "..."}
Caveat: this is step 1 of 2 — real accounts always require the 2FA step next, there is no single-step login.
POST /api/verify-2fa/
Check the 6-digit authenticator code, exchange for tokens.
Body: {"pending_token": "...", "code": "482913"}
200 success:
{
"success": true,
"access_token": "...",
"refresh_token": "...",
"refresh_token_expires_at": "2026-09-10T12:00:00",
"expires_in": 900,
"user": { "id": 1, "email": "...", "first_name": "...", "last_name": "...", "role": "ADMIN" }
}
400 (invalid code, or expired/unknown pending_token):
{"success": false, "error": "..."}
Caveat: access_token/refresh_token are NOT JWTs — they're Django signing
values (HMAC-signed, salted). A generic JWT decoder cannot read them; the only
supported way to inspect one from outside QDBAuth is /api/whoami/.
POST /api/token/refresh/
Trade a refresh token for a new access token. Refresh tokens rotate — the
one you send is invalidated and a new one comes back. Always persist the
latest refresh_token from the response, not the one you sent.
Body: {"refresh_token": "..."}
200 success: same shape as Verify2FASuccess minus user.
401 — invalid, expired, or already-used (rotated) token, or bad client secret. 403 — token is valid but the account has been deactivated.
POST /api/token/revoke/
Call on logout so the refresh token can never be used again.
Body: {"refresh_token": "..."} → {"success": true|false} (false if
it was already dead — not an error).
GET /api/whoami/
Check an access token and see who it belongs to. Authorization: Bearer <access_token>.
200: {"user_name": "[email protected]", "user_permissions": "ADMIN"}
401 — missing/invalid/expired token → call /api/token/refresh/.
Note: user_permissions is the account's role, uppercased — for a site with
its own role vocabulary (see ROLE_GROUPS in apps/auth/users.py), this will
be that site's role string (e.g. "SUPER ADMIN" for TD), not the global
admin/developer/user set.
Token lifetimes (env-configurable)
| Token | Default lifetime | Env var |
|---|---|---|
| Access token | 15 min | QDB_ACCESS_TOKEN_MAX_AGE (seconds) |
| Refresh token | 30 days | QDB_REFRESH_TOKEN_TTL_DAYS |
Pending-login token (between /api/login/ and /api/verify-2fa/) |
5 min | fixed, not env-configurable |
Integration pattern for a new consuming app
- Register the site in the dashboard's Manage Sites page (
site_name+site_url) — needed for per-site credential lookups and for the browser SSO flow'sreturn_url→ site matching. - Build a two-step login UI: password screen → TOTP code screen. There is no single-step API for real accounts.
- Store
access_token+refresh_token; silently call/api/token/refresh/before the access token expires. - Call
/api/token/revoke/on logout.