Auth Endpoints
The auth service handles email/password login and JWT token exchange. All endpoints are served through the API gateway at http://localhost:8000.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /auth/token | Exchange email + password for a JWT token (also sets the aegis_token cookie since R42a) |
POST | /auth/resolve | JWT → DB-fresh identity + roles + permissions (internal) |
GET | /auth/me | Caller’s identity, roles, permissions |
POST | /auth/refresh | Sliding refresh (re-mints tokens older than 1h) |
POST | /auth/logout | Expire the session cookie |
POST | /auth/logout-all | Revoke all of the caller’s outstanding tokens |
POST | /auth/admin/users | Create an invited user → one-time invite URL (admin) |
GET | /auth/admin/users | Admin user directory with status (admin) |
PATCH | /auth/admin/users/{id} | Edit display name / active status / roles (admin) |
POST | /auth/admin/users/{id}/reset-password | One-time reset URL, revokes sessions (admin) |
GET | /auth/invites/{token} | Validate an invite/reset token (public) |
POST | /auth/invites/redeem | Set password via token and log in (public) |
POST | /auth/validate | Validate a JWT token (legacy claims-only) |
GET | /auth/users | Tenant user directory (admin-only) |
POST /auth/token
Exchange email and password credentials for a JWT access token.
Request:
curl -i -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"email": "admin@aegis.local", "password": "aegis-dev-admin"}'Request body:
{
"email": "admin@aegis.local",
"password": "aegis-dev-admin"
}Response (200) — since R42b the JWT rides only in the Set-Cookie: aegis_token=... header; the body carries identity fields:
{
"expires_in": 86400,
"user_id": "52c8f756-...",
"roles": ["admin", "operator", "reviewer"],
"email": "admin@aegis.local",
"display_name": "Bootstrap Admin"
}Returns 401 Unauthorized with {"detail": "Invalid email or password"} when the credentials are wrong, the user is inactive, or the account is still invited (no password set). Email is matched case-insensitively.
The response also carries Set-Cookie: aegis_token=<jwt>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400 (HttpOnly/Secure were enabled at the R42b client cutover), and the JWT includes a tv token-version claim used for revocation.
For local development, log in with admin@aegis.local / aegis-dev-admin (the seeded bootstrap admin), which grants admin access.
POST /auth/validate
Legacy claims-only token check. Superseded by /auth/resolve (R42b) — the gateway now resolves DB-fresh identity + permissions rather than trusting the claims. Kept for backward compatibility; it decodes the JWT and returns its claims without any is_active / token_version check.
The token is passed as an authorization parameter (a Bearer <jwt> string), not a JSON body:
curl -X POST "http://localhost:8000/api/v1/auth/validate?authorization=Bearer%20eyJhbGciOiJIUzI1NiIs..."Response (200):
{
"valid": true,
"user_id": "9b2f1c4e-...",
"roles": ["admin", "operator", "reviewer"]
}An unparseable or empty token returns {"valid": false, "user_id": null, "roles": []}.
POST /auth/resolve
JWT → DB-fresh authorization state (R42a, spec D2). Internal: backs the perimeters; the gateway NotFounds /api/v1/auth/resolve from R42b. Enforces is_active and token_version (tv claim, missing = 0) behind a ~5s per-user cache — this is why role changes and deactivation bind within seconds.
Request: Authorization: Bearer <jwt> (or the aegis_token cookie), no body.
Response (200, always):
{
"valid": true,
"user_id": "9b2f1c4e-...",
"email": "admin@aegis.local",
"display_name": "Bootstrap Admin",
"tenant_id": "00000000-0000-0000-0000-000000000001",
"roles": ["admin", "operator", "reviewer"],
"permissions": ["conversations.use", "dashboards.read", "hitl.approve", "users.manage", "..."]
}Invalid tokens return {"valid": false, "reason": "signature|expired|revoked|inactive|unknown_user", "roles": [], "permissions": []}.
Session endpoints (R42a)
GET /auth/me— the caller’s{user_id, email, display_name, tenant_id, roles, permissions};401if the token doesn’t resolve. The UI’s identity source once the cookie is httpOnly (R42b).POST /auth/refresh— if the token is valid and older than 1 hour, mints a fresh 24h token (200+ Set-Cookie + body token); younger tokens get204.POST /auth/logout— expires theaegis_tokencookie. Unauthenticated, idempotent.POST /auth/logout-all— bumps the caller’stoken_version; every outstanding token then resolves{"valid": false, "reason": "revoked"}within the cache window.
Admin user lifecycle (R42a)
All gated on permissions resolved from the caller’s own JWT (users.manage; role changes additionally roles.grant). Accounts are deactivate-only — there is no delete endpoint.
POST /auth/admin/users — create an invited user:
{ "email": "jane@example.com", "display_name": "Jane", "roles": ["operator", "reviewer"] }Response (201): the user (status invited) plus a one-time invite_token / invite_url (7-day expiry). The raw token is never stored — share it out-of-band; there is no email delivery.
GET /auth/admin/users — directory rows with roles, is_active, and status (active / invited / disabled).
PATCH /auth/admin/users/{id} — any of display_name, is_active, roles. Deactivation bumps token_version (live sessions die ≤5s). Removing the last active admin returns 409.
POST /auth/admin/users/{id}/reset-password — voids open tokens, bumps token_version, returns a one-time reset_url (1-hour expiry).
Invite redemption (public)
GET /auth/invites/{token} — validate before rendering the set-password form. Returns {email, display_name, purpose, expires_at}; unknown, used, and expired tokens all return 404.
POST /auth/invites/redeem — {"token": "...", "password": "..."} (≥8 chars). Sets the password (single-use, row-locked), then logs the user in — the response mirrors /auth/token including Set-Cookie. A second redeem of the same token returns 404.
GET /auth/users
List the tenant’s users — the directory behind the Platform Settings page’s user picker. Admin-only: the JWT (Bearer header or aegis_token cookie) is decoded in-service, and non-admin callers get 403.
Request:
curl http://localhost:8000/api/v1/auth/users \
-H "Authorization: Bearer $TOKEN"Response (200):
[
{
"id": "9b2f1c4e-...",
"email": "admin@aegis.local",
"display_name": "Bootstrap Admin",
"roles": ["admin", "operator", "reviewer"],
"is_active": true
}
]