Authentication

The two credential types, and which surface each one opens.

AOCore uses two distinct credential types depending on what surface you’re hitting:

SurfaceCredentialWhere 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.

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.

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 type text, 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 accept Authorization: Bearer <master-key>. The cookie is the only path.

Session lifecycle

EventEffect
POST /developer/auth/registerCreates user + writes developer_register audit row; does NOT set a session cookie
POST /developer/auth/loginSets aocore_dev_session cookie (or returns mfa_required: true + mfa_token for MFA-enrolled accounts)
POST /developer/auth/verify_mfaExchanges an mfa_token for the session cookie
POST /developer/auth/logoutRevokes the user_sessions row + clears the cookie + writes developer_logout audit row
Session expiryDefault 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

PatternWhat to do
Backend servicePull from environment variable (AOSENTRY_KEY or your own naming) — same as OPENAI_API_KEY. See the examples folder.
CLI toolRead from a config file in $XDG_CONFIG_HOME/aocore/credentials (or equivalent) — never commit.
Browser appDo not. API keys must never reach a browser. Proxy the call through your backend, which holds the key.
CI / CDInject 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.