Skip to main content
CID222 Docs

Authentication

Issue a gateway API key or obtain a user JWT, and know which CID222 surface accepts which of the two.

  • Version: 0.4
  • Role: admin_user, normal_user
  • Type: task

CID222 accepts two credentials on the Authorization header, and they are not interchangeable. A gateway API key identifies a tenant for server-to-server traffic. A user JWT identifies a signed-in person and is the only credential that reaches sessions, image analysis and document analysis.

What do I need?

Licence
Any
Role
admin_usernormal_user

Prerequisites

  • The appliance has completed the first-boot setup wizard.
  • Issuing or revoking a gateway API key requires the admin_user role; obtaining a JWT requires only a tenant account.
  • Your client can reach the gateway on TCP 443, or on TCP 3000 if you are calling the container directly.

Which credential reaches which surface

Both credentials are presented the same way, as Authorization: Bearer <credential>. CID222 tells them apart by the cid_key_ prefix. What differs is where each one is accepted.

SurfaceGateway API keyUser JWT
POST /chat/completionsYesYes
POST /chat/harden-promptYesYes
GET /chat/providersYesYes
GET /modelsYesYes
POST /api/v1/guardrails/detectYesYes
/sessions and /sessions/:id/…NoYes
/image-analysis/…NoYes
/document-analysis/…NoYes
/admin/…, including /admin/api-keysNoYes, with admin_user

Warning

A gateway API key on /sessions, /image-analysis or /document-analysis returns 401. Those controllers are guarded by JWT validation alone; the key is never examined. Documentation that says otherwise is describing an intent, not the code.

Issue and use a gateway API key

Open the API key list

Select Administration → API Keys.

The page lists the tenant's existing keys with their names, scopes and expiry dates. The secret itself is not listed — CID222 stores only a SHA-256 hash of it.

Create the key

Select Create API key, name the key after the caller that will use it, and set an expiry date if the key should stop working on its own. Leaving the expiry empty means the key never expires.

The new key is displayed once, in the form cid_key_ followed by 64 hexadecimal characters — 32 random bytes rendered as hex.

Copy the key out before you close the dialog

Select Copy API Key and store the value in your secret manager or an environment variable.

Closing the dialog discards the plaintext. If you lose it, select Regenerate API Key on the key's row, which mints a new secret and immediately invalidates the old one.

Call the gateway with the key

Send the key as a bearer token.

curl -N -sS -X POST "https://<appliance-fqdn>/chat/completions" \
  -H "Authorization: Bearer cid_key_0123456789abcdef…" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","provider":"openai","messages":[{"role":"user","content":"Hello"}]}'

The response is an SSE stream. A rejected key returns 401 with the message Invalid API key.

Danger

A key scoped to a tenant group is attributed to the group's earliest-added member. Removing that user transfers the key's identity to another member instead of revoking the key. Revoke the key itself when the person who owned it leaves.

Obtain and use a user JWT

Exchange credentials for a token

Post the username and password to POST /auth/login.

curl -sS -X POST "https://<appliance-fqdn>/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"jdoe","password":"<password>"}'

The endpoint returns 200 with the token and the caller's identity.

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "tenant_id": "b2c3d4e5-6f70-4a81-9b2c-3d4e5f607182",
  "username": "jdoe",
  "contact_email": "jdoe@example.corp",
  "role": "normal_user"
}

Send the token on every subsequent request

Put access_token on the Authorization header.

curl -sS "https://<appliance-fqdn>/sessions" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"

The request succeeds for the tenant encoded in the token. A token for a deleted or deactivated tenant returns 401 even before it expires.

Re-authenticate when the token expires

Tokens are HS256 and live for the value of JWT_EXPIRES_IN, which defaults to 24 hours. CID222 issues no refresh token, so an expired token is replaced by calling POST /auth/login again.

An expired token returns 401. Treat 401 on any authenticated call as the signal to log in again and retry once.

What the token carries

{
  "sub": "b2c3d4e5-6f70-4a81-9b2c-3d4e5f607182",
  "username": "jdoe",
  "role": "normal_user",
  "iat": 1789200000,
  "exp": 1789286400
}
ClaimMeaning
subThe tenant id the token authenticates
usernameThe account name
roleThe role at the moment the token was minted
iatIssue time, seconds since the Unix epoch
expExpiry time, seconds since the Unix epoch

The role claim is informational. CID222 re-reads the role from the database on every request, so a token minted before a demotion does not keep the old role.

Roles

RoleWhat it can do
superadminEverything, across all tenants
admin_userTenant, credential, filter and key administration, plus chat and sessions
normal_userChat, sessions, and the analysis endpoints for its own tenant
viewerRead-only. Any mutating request is refused with READ_ONLY_ROLE
auditorReviews the estate. Chat and session writes are refused with ROLE_NOT_FOR_CHAT

A request authenticated with a gateway API key is given the synthetic role api_key. It is not a role you can assign to a tenant.

Rate limiting on sign-in

POST /auth/login accepts 10 requests per minute per client IP address. POST /auth/forgot-password and POST /auth/reset-password accept 5 per minute. Exceeding a limit returns 429.

No other endpoint is rate limited, and CID222 sends no X-RateLimit-* headers on any response.

Error shapes

The error body is not uniform, so match on the HTTP status first and on code only where it is present.

A framework-level rejection, such as a bad password or a missing bearer token, carries no code:

{
  "statusCode": 401,
  "message": "Invalid credentials",
  "error": "Unauthorized"
}

A CID222 policy rejection carries a machine-readable code, and sometimes extra fields:

{
  "statusCode": 403,
  "code": "ROLE_NOT_FOR_CHAT",
  "message": "This role does not use the chat: an auditor reviews the estate rather than adding traffic to it. Use the dashboard instead."
}

Verify

  1. Call GET /models with the gateway API key. It returns 200 and a JSON array.
  2. Call GET /sessions with the same gateway API key. It returns 401 — this is the expected refusal, and it confirms the key is scoped as documented.
  3. Call GET /sessions with a JWT from POST /auth/login. It returns 200 and an array of the tenant's sessions.
  4. Select Administration → API Keys. The key's row shows a last-used timestamp reflecting your test call.

Next steps

  • API overview — every endpoint, its base path and its guard.
  • Chat API — the request body and the SSE contract on /chat/completions.
  • Sessions API — the JWT-only surface that keeps conversation state.

Last updated on

On this page

Download PDF