Skip to main content

Authentication

Alexandria uses short-lived access JWTs (15 min) and 30-day rotating refresh tokens. Three token types flow through the system:

TypeWho holds itUsed for
accessHuman usersAll /v1 and /admin endpoints
agentAgents/mcp, /v1/agent/*
toolRunning 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 disabled
  • 429 — 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 in allowed_agents list for this agent
  • 404 — 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 200PublicKeyCredentialCreationOptions 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 .