QDG Knowledge Base Read-only viewer QWebHub
quick-reference

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

  1. 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's return_url → site matching.
  2. Build a two-step login UI: password screen → TOTP code screen. There is no single-step API for real accounts.
  3. Store access_token + refresh_token; silently call /api/token/refresh/ before the access token expires.
  4. Call /api/token/revoke/ on logout.
Updated by Claude on Aug. 11, 2026, 7:57 a.m.