Skip to Content
API ReferenceAuthentication

Authentication

The AEGIS API uses a two-step authentication flow: exchange email/password credentials for a JWT token, then use the token on subsequent requests. The Go API gateway enforces authentication on every request except health checks and the token endpoint itself.

Authentication Flow

1. Client sends email + password to POST /api/v1/auth/token 2. Auth service verifies credentials (bcrypt), returns JWT 3. Client includes JWT in Authorization header (or aegis_token cookie) on all subsequent requests 4. Gateway validates JWT on each request before proxying to backend

Obtaining a JWT Token

Exchange email and password credentials for a JWT token by calling the token endpoint. This endpoint does not require prior authentication.

curl -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

FieldTypeRequiredDescription
emailstringYesThe user’s email address (matched case-insensitively)
passwordstringYesThe user’s password

Response

Since R42b the JWT rides only in the Set-Cookie header — the body carries identity fields for immediate UI hydration, no access_token:

Set-Cookie: aegis_token=eyJhbGciOiJIUzI1NiIs...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
{ "expires_in": 86400, "user_id": "52c8f756-...", "roles": ["admin", "operator", "reviewer"], "email": "admin@aegis.local", "display_name": "Bootstrap Admin" }
FieldTypeDescription
expires_inintegerToken lifetime in seconds (86400 = 24 hours)
user_idstringThe authenticated user’s ID
rolesstring[]Roles granted to this user
emailstringThe authenticated user’s email
display_namestring | nullHuman-readable name for the UI header

Programmatic clients capture the cookie value and may send it back either as the aegis_token cookie or as Authorization: Bearer <value> — the gateway accepts both.

If the email or password is incorrect (or the user is inactive), the endpoint returns 401 Unauthorized with {"detail": "Invalid email or password"}.

Using the JWT Token

Include the JWT in the Authorization header with the Bearer scheme on all subsequent requests:

curl http://localhost:8000/api/v1/conversations \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

For browser clients (SSE/EventSource), the gateway also accepts the JWT from the aegis_token cookie set at login, so no Authorization header is required.

# Step 1: Get a token (from the Set-Cookie header — the body no longer carries it) TOKEN=$(curl -si -X POST http://localhost:8000/api/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"email": "admin@aegis.local", "password": "aegis-dev-admin"}' \ | grep -i '^set-cookie: aegis_token=' | sed 's/^[Ss]et-[Cc]ookie: aegis_token=//; s/;.*//') # Step 2: Use the token curl http://localhost:8000/api/v1/conversations \ -H "Authorization: Bearer $TOKEN"

Token Details

JWT Claims

The JWT payload contains the following claims:

ClaimDescription
user_idThe authenticated user’s identifier
emailThe authenticated user’s email address
rolesArray of role keys (e.g., ["admin", "operator", "reviewer"]), sourced from user_roles at mint time
tenant_idThe user’s tenant, forwarded to backends as X-Tenant-Id
tvToken version (R42a) — checked against users.token_version at resolve time; logout-all / deactivation / password reset bump it and revoke all outstanding tokens. Missing (pre-R42a token) = 0
issIssuer — "aegis-auth" for tokens from the auth service
iatIssued-at timestamp (Unix epoch)
expExpiration timestamp (Unix epoch)

The JWT carries identity; authorization state (roles, permissions, active status) is re-resolved from the database at the perimeter via POST /auth/resolve (R42a, ~5s cache) — so role changes and deactivation bind on live traffic within seconds, not at next login. Sessions can be refreshed (POST /auth/refresh), ended (POST /auth/logout), or revoked everywhere (POST /auth/logout-all) — see the auth endpoints page.

Token Expiry

Tokens expire 24 hours after issuance. When a token expires, the gateway returns 401 Unauthorized. Request a new token by calling POST /api/v1/auth/token again.

Signing Algorithm

Tokens are signed with HS256 (HMAC-SHA256) using a shared secret configured via the JWT_SECRET environment variable.

Token Validation

The gateway first validates tokens locally using the shared JWT secret (fast reject), then re-resolves authorization state from the database via auth-service’s internal POST /auth/resolve (~5s cache, stale-while-revalidate — see the auth flow page). The auth service also exposes a legacy claims-only validation endpoint:

# The token is passed as the `authorization` parameter, not a JSON body curl -X POST "http://localhost:8000/api/v1/auth/validate?authorization=Bearer%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Response:

{ "valid": true, "user_id": "dev-user", "roles": ["admin", "operator", "reviewer"] }

Role-Based Access

Some endpoints require specific roles. The gateway checks roles from the DB-fresh resolve result before proxying the request:

Endpoint PatternRequired Role
/api/v1/admin/*admin
/api/v1/detection-rules/*power_user or admin
POST /api/v1/approvals/{id}/decidereviewer or admin (R42c)
Config-type writes (entity/event types, guard rules)power_user or admin (R42c)
All other endpointsAny authenticated user

If the user’s roles do not include the required role, the gateway returns 403 Forbidden:

{ "error": "admin role required" }

Since R42d, backend services additionally enforce fine-grained permissions (e.g. hitl.approve on approval decisions, filings.submit on checklist submission, skills.author on skill edits) from the gateway-stamped X-Permissions header, per the R42 Appendix C matrix — see Service-Side Enforcement. A failed permission check returns 403 with {"detail": "Insufficient permissions"} (while AEGIS_AUTH_MODE=log, the failure is warn-logged and allowed through during the rollout soak).

Public Endpoints

The following endpoints do not require authentication:

EndpointDescription
GET /healthGateway health check
POST /api/v1/auth/tokenToken exchange
GET /api/v1/auth/invites/{token}, POST /api/v1/auth/invites/redeemInvite validation/redemption (public since R42b)
GET /api/v1/entity-types*Public entity type listings
GET /api/v1/relationship-rules*Public relationship rule listings
GET /api/v1/relationship-types*Public relationship type listings

User Provisioning

Accounts are admin-provisioned — there is no self-serve signup. Users live in the users table with role grants in user_roles (see the auth-service page). Since R42e, user identity is a uuid end-to-end: every identity column across the platform references users(id) with a database-level foreign key.

The primary path is the Users & Roles admin page at /admin/users (R42d — see the user guide), backed by the admin API: POST /api/v1/auth/admin/users creates an invited account and returns a one-time invite URL (7-day expiry); the invitee redeems it to choose a password and is logged in. A CLI fallback remains:

cd services/auth-service poetry run python -m auth_service.create_user \ --email alice@example.com \ --roles admin,operator,reviewer

The command prompts for a password (or reads it from AEGIS_NEW_USER_PASSWORD). Re-running it for an existing email rotates that user’s password and revokes their live sessions. Accounts are deactivate-only (--deactivate / --reactivate, or PATCH /auth/admin/users/{id}) — there is no hard delete.

User Directory

Admins can list the tenant’s users via GET /api/v1/auth/users — it returns each user’s id, email, display_name, roles, and is_active. The auth service decodes the JWT in-service (Bearer header or aegis_token cookie) rather than trusting forwarded headers, and returns 403 for non-admin callers. This directory feeds the Platform Settings page’s per-user budget-override picker. See the auth endpoints page for an example.

System Users (R42e)

Four non-loginable system accounts exist for FK integrity and attribution: system@aegis.local (the nil UUID 00000000-0000-0000-0000-000000000000), legacy@aegis.local (backfill target for unmappable pre-R42 identity strings), eval-runner@aegis.local, and demo-seed@aegis.local. Each has an empty password hash, which fails closed at login — they can never authenticate — while is_active stays true so they satisfy foreign keys and appear in attribution joins. The pre-R42 admin-author sentinel UUID is retired; rows it owned were remapped to the real admin.

Bootstrap Admin

On startup the auth service seeds an initial admin from the BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_ADMIN_PASSWORD environment variables, if a user with that email does not already exist. For local development these default to admin@aegis.local / aegis-dev-admin.

EmailPasswordRoles
admin@aegis.localaegis-dev-adminadmin, operator, reviewer

The default bootstrap admin credentials must never be used in production. Set strong BOOTSTRAP_ADMIN_EMAIL / BOOTSTRAP_ADMIN_PASSWORD values and a real JWT_SECRET via environment variables before deploying.

Security Notes

  • Passwords are hashed with bcrypt before storage; plaintext passwords are never persisted.
  • Email matching is case-insensitive (enforced by a LOWER(email) unique index).
  • The JWT_SECRET must be set via an environment variable in production.
  • CORS is configured to allow all origins (*). Restrict this in production deployments.
Last updated on