Authentication
Alexandria uses short-lived access JWTs (15 min) and 30-day rotating refresh tokens. Three token types flow through the system:
| Type | Who holds it | Used for |
|---|---|---|
access | Human users | All /v1 and /admin endpoints |
agent | Agents | /mcp, /v1/agent/* |
tool | Running tools | /v1/ingest/* |
All tokens are sent as Authorization: Bearer <token>.
POST /auth/login
Rate limited per-IP. Records failures and locks accounts after repeated mismatches.
Request
{ "username": "alice", "password": "s3cret!" }
Response 200
{
"access_token": "eyJ...",
"refresh_token": "a1b2c3...",
"token_type": "Bearer",
"expires_in": 900
}
Errors
401— invalid credentials or account disabled429— rate limited or account temporarily locked
POST /auth/refresh
Rotates the refresh token (old token revoked immediately) and returns a new access token.
Request
{ "refresh_token": "a1b2c3..." }
Response 200 — same shape as /auth/login.
Errors
401— invalid, revoked, or expired refresh token
POST /auth/logout
Revokes the provided refresh token. Always returns 200 regardless of whether the token existed.
Request
{ "refresh_token": "a1b2c3..." }
GET /auth/setup
Returns whether initial admin setup is required.
{ "needs_setup": true }
POST /auth/setup
Bootstraps the first admin account. Returns 403 once any admin account exists.
Security note: if all admin accounts are disabled, this endpoint re-opens to unauthenticated callers. This is intentional for demo deployments but is a configuration risk in production.
Request
{ "username": "admin", "password": "Str0ng!Pass" }
Response 200 — access + refresh token pair for the created admin.
POST /v1/agent-token
Exchanges a human access token for an agent token. Computes the effective tool list via permission intersection:
effective = agent_allowed_tools ∩ user_allowed_tools ∩ server_ceiling ∩ tenant_ceiling
super_admin bypasses intersection.
Request
{ "agent": "researcher", "session_id": "my-session" }
Response 200
{
"token": "eyJ...",
"agent": "researcher",
"effective_tools": ["web_search", "calculator"],
"expires_in": 3600
}
Errors
403— user not inallowed_agentslist for this agent404— agent not found
WebAuthn / FIDO2 Passkeys
Passkey flows are rate-limited to 20 req/min per IP.
POST /auth/webauthn/authenticate/begin
Initiates passkey authentication. Requires username in the request body.
Request
{ "username": "alice" }
Response 200 — WebAuthn PublicKeyCredentialRequestOptions object (pass directly to navigator.credentials.get()).
POST /auth/webauthn/authenticate/finish
Complete authentication. Pass ?username=alice as query param; body is the raw AuthenticatorAssertionResponse.
Response 200 — token pair on success (same shape as /auth/login).
POST /auth/webauthn/register/begin
Requires human access token. Agents cannot register passkeys.
Response 200 — PublicKeyCredentialCreationOptions object.
POST /auth/webauthn/register/finish
Body is the AuthenticatorAttestationResponse from the browser.
Response 200
{ "ok": true }
curl examples
# Login
curl -s -X POST http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"changeme"}' | jq .
# Refresh
ACCESS=$(curl -s ... | jq -r .access_token)
curl -s -X POST http://localhost:8080/auth/refresh \
-H 'Content-Type: application/json' \
-d "{\"refresh_token\":\"$REFRESH\"}" | jq .
# Get agent token
curl -s -X POST http://localhost:8080/v1/agent-token \
-H "Authorization: Bearer $ACCESS" \
-H 'Content-Type: application/json' \
-d '{"agent":"researcher"}' | jq .