Authentication
The two credential types, and which surface each one opens.
AOCore uses two distinct credential types depending on what surface you’re hitting:
| Surface | Credential | Where it comes from |
|---|---|---|
/v1/* LLM gateway (chat, embeddings, images, audio, rerank, messages, …) | API key (sk-…) | Minted in the developer portal — see the Quickstart |
/developer/v1/* management endpoints (mint keys, view spend, etc.) | Developer session cookie (aocore_dev_session=…) | Set by POST /developer/auth/login after registration |
Both surfaces are documented end-to-end in the API reference. This page focuses on the wire-level details: what headers you send, where you store your credentials, and what the gateway accepts.
API-key authentication (/v1/*)
API keys are scoped to a single developer, can carry a model allowlist + budget + RPM/TPM limits, and are revocable from the portal. The gateway accepts the key in any of the following header or query forms — pick whichever your SDK supports.
Recommended: Authorization: Bearer
curl https://gateway.core.aocyber.ai/v1/chat/completions \
-H "Authorization: Bearer sk-…" \
-H "Content-Type: application/json" \
-d '{ "model": "gpt-4o-mini", "messages": [{"role":"user","content":"Hi"}] }'
This is the standard OpenAI / Anthropic / Google AI wire format — every SDK uses it out of the box, and it’s the form that shows up cleanly in proxy / load-balancer logs without exposing the key in URL access logs. Use it unless you have a reason not to.
Alternative header forms
The gateway also accepts the key in any of these headers, checked in order — the first one present wins:
Authorization: Bearer sk-… ← recommended
x-aosentry-api-key: sk-… ← AOCore-specific
x-api-key: sk-… ← Anthropic-style; OpenAI Python SDK accepts via api_key
x-goog-api-key: sk-… ← Google AI SDK uses this header natively
api-key: sk-… ← Azure OpenAI SDK uses this header natively
Multiple forms exist to keep the gateway drop-in compatible with every major upstream SDK without per-SDK config gymnastics.
Query-parameter form (not recommended in production)
GET /v1/chat/completions?api-key=sk-…
GET /v1/chat/completions?key=sk-…
Both are accepted but log to URL access logs. Use only for local testing, ad-hoc shell scripts, or signed pre-flight checks — never in long-lived production traffic. If you ship one of these, treat the access log as a credential disclosure and rotate the key.
What happens if you send no key
401 Unauthorized with the body:
{
"error": {
"message": "No API key provided. Pass it via Authorization: Bearer <token> or x-api-key header.",
"type": "authentication_error",
"code": "missing_credentials"
}
}
What happens if you send a key for a different surface
If you send your developer session cookie to a /v1/* endpoint, the gateway looks
for an API key first and fails closed with 401. If you send an API key to a
/developer/v1/* endpoint, the management middleware rejects it because the
session middleware does not accept Bearer tokens. The two credential paths are
deliberately non-overlapping — see the errors guide
for the exact response shapes.
How keys are stored
You receive the raw key (sk-…) once at mint time. Internally:
- The gateway computes a SHA256 of the raw token and stores only the hash in
api_keys.token(column typetext, primary lookup index). - The raw value is never persisted anywhere — not in the DB, not in audit logs, not in metrics. If you lose it, revoke and re-mint; there is no recovery.
This is why the portal shows the raw key exactly once at mint time, gated by an
explicit “I’ve copied this” confirmation. Audit logs record the key’s UUID
(api_keys.id) and SHA256 digest fingerprint — never the raw value.
Developer session authentication (/developer/v1/*)
The portal authenticates via a server-set cookie:
Cookie: aocore_dev_session=<opaque-32-byte-hex>
It’s set by POST /developer/auth/login and cleared by POST /developer/auth/logout.
Properties:
- HttpOnly — JavaScript cannot read it. CSRF protection comes from the
portal’s
SameSite=Lax+ same-origin enforcement. - Secure in production (TLS-only).
- Role-filtered — even with a valid cookie, the middleware re-checks
users.user_role = 'developer'per request, so an admin cookie cannot reach/developer/v1/*and vice versa. - No master-key bypass. Unlike admin endpoints,
/developer/v1/*does NOT acceptAuthorization: Bearer <master-key>. The cookie is the only path.
Session lifecycle
| Event | Effect |
|---|---|
POST /developer/auth/register | Creates user + writes developer_register audit row; does NOT set a session cookie |
POST /developer/auth/login | Sets aocore_dev_session cookie (or returns mfa_required: true + mfa_token for MFA-enrolled accounts) |
POST /developer/auth/verify_mfa | Exchanges an mfa_token for the session cookie |
POST /developer/auth/logout | Revokes the user_sessions row + clears the cookie + writes developer_logout audit row |
| Session expiry | Default 30 days; configurable per deployment. Expired = 401 with the same body as no-cookie |
All four endpoints are documented in the API reference.
Error-body parity
All four 401 failure modes (no cookie, expired, revoked, wrong role) return the
byte-identical response body. This is the role-enumeration prevention property
locked by TestDeveloperSessionMiddleware_ErrorBodyParity — an attacker cannot
distinguish “this email doesn’t exist” from “this email is an admin, not a
developer” by comparing 401 bodies.
{
"error": {
"message": "Invalid or expired developer session",
"type": "authentication_error",
"code": "invalid_session"
}
}
Where to put credentials in client code
| Pattern | What to do |
|---|---|
| Backend service | Pull from environment variable (AOSENTRY_KEY or your own naming) — same as OPENAI_API_KEY. See the examples folder. |
| CLI tool | Read from a config file in $XDG_CONFIG_HOME/aocore/credentials (or equivalent) — never commit. |
| Browser app | Do not. API keys must never reach a browser. Proxy the call through your backend, which holds the key. |
| CI / CD | Inject as a masked secret. Most CI providers redact sk- prefixed values automatically; don’t rely on that — set the secret level explicitly. |
A common anti-pattern is hard-coding the key in source. Don’t — git history is
forever, and rotating the key requires every consumer to update. The
examples folder always uses os.environ["AOSENTRY_KEY"] or shell
${AOSENTRY_KEY} substitution.
Related
- the Quickstart — invitation → register → mint key → first call
- the rate-limits guide — what the response headers tell you
- the models guide — model allowlists per key
- the errors guide — full
APIErrorshape + auth error codes - API reference — every endpoint, every status code