Authentication¶
Two kinds of credentials, for two kinds of callers.
| Console users | Applications | |
|---|---|---|
| Credential | Bearer token obtained with email and password | API key ax_… created in the console |
| Used on | Management API (/v1/users, /v1/llm, /v1/proxy-apps, …) |
Runtime API (/v1/proxy-apps/<app-id>/openai, /gemini, /vertex) |
| Authorization | Role-based permissions | Scope: one proxy app |
| Lifetime | Until rotated; no expiry | Optional expires_at; can be deactivated |
Console users¶
Log in¶
curl -s http://localhost:3000/v1/users/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@axium.local","password":"admin123456"}'
{
"status": true,
"data": {
"id": "00000000-0000-0000-0000-000000000002",
"email": "admin@axium.local",
"name": "Super Admin",
"token": "9f1c…40 hex…",
"is_active": true,
"permissions_cache": ["core:read", "core:create", "…"],
"last_login_at": "…", "created_at": "…", "updated_at": "…"
}
}
Wrong credentials or an inactive user answer 401 Invalid email or password.
Use the token¶
The token is opaque (not a JWT) and validated against the database on every
request. It has no expiry. A new one is issued, invalidating the previous, on
every login, on POST /v1/users/logout, when the account is deactivated and
when an administrator resets the password.
Permissions¶
Each route declares the permissions that open it; the user needs at least one.
Without a valid token: 401 Unauthorized. With a token but without the
permission: 403 Forbidden. The list of permissions a user holds comes back
in permissions_cache at login and in GET /v1/users/me.
Permissions are module:action:
| Modules | Actions |
|---|---|
core (users, roles), llm (providers, models), rm (proxy apps, API keys, projects), observability (stats, traces), governance, chat, mcp, agents |
read, view, create, update, delete, manage, execute, test, export |
The seeded super-admin role holds all of them. Create narrower roles with
POST /v1/roles and PATCH /v1/roles/:id/permissions.
Change the initial password¶
curl -X PATCH http://localhost:3000/v1/users/password \
-H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
-d '{"current_password":"admin123456","new_password":"a-long-new-password"}'
Minimum 8 characters.
Applications: API keys¶
Create one¶
In the console, or:
curl -X POST http://localhost:3000/v1/rm/api-keys \
-H "Authorization: Bearer <console token>" -H 'Content-Type: application/json' \
-d '{"name":"billing-service","scope_type":"proxy_app","proxy_app_id":"<app-id>","expires_at":"2027-01-01T00:00:00Z"}'
The response contains key in clear once. Axium stores only a hash and
the last four characters; a lost key has to be replaced.
Send it¶
Either header works; Authorization wins if both are present:
Authorization: Bearer ax_8d2d9ff993e24eb6144e8d87e7890a51401eca99
x-goog-api-key: ax_8d2d9ff993e24eb6144e8d87e7890a51401eca99
The second exists so Google's SDKs work unchanged. A ?key= query parameter
is not accepted and is stripped before forwarding.
What the runtime checks¶
- The key exists, is active and has not expired. Any failure is the same
401, on purpose. - The key is scoped to a proxy app, and it is the app in the URL. Otherwise
403. - From there, the proxy app's own rules apply: status, budget, model allow-list, policies. See Proxy apps & gateways.
last_used_at is updated on a best-effort basis.
Rotate¶
Create the new key, deploy it, then deactivate the old one with
PATCH /v1/rm/api-keys/:id {"is_active": false}. Deactivating keeps the
key's history in statistics and traces; deleting does too but loses its name.