Skip to main content

Authentication

Single-user local auth for the Chaos Cypher deployment. Every /api/ request passes through an nginx auth_request subrequest before reaching Cortex.

Base path: /api/v1/auth

Related pages

How Auth Works

Browser / API client


nginx (port 80 / 443)
│ auth_request → GET /api/v1/auth/verify
│ ← 200 + X-Auth-User header (authenticated)
│ ← 401 (unauthenticated → nginx returns 401 to client)

Cortex (internal only)
reads X-Auth-User from the verified header

Single-user model. There is one local operator account. No admin/user tiers, no multi-user, no registration endpoint.

Two credential mechanisms are accepted:

MechanismHow to use
Session cookieSet at login/setup; sent automatically by browsers. Name: cc_session (configurable).
API keyAuthorization: Bearer cc_live_<key>. Useful for scripts and CI.

Cookie takes priority when both are present. Every request goes through /verify; both mechanisms are checked there.

Server-side invalidation. The session cookie is stateless HMAC-SHA256 — no session store. Invalidation works by incrementing session_epoch in the credentials file. Any cookie that carries a stale epoch is rejected. Epoch is bumped on: logout, password change, username change.


Endpoint Reference

Check Status

GET /api/v1/auth/status

Returns setup state and whether the current caller is authenticated. No auth required. Safe to poll on page load.

Response — 200 OK

{"setup_needed": false, "authenticated": true, "username": "alice"}
FieldTypeDescription
setup_neededbooleantrue when no credentials file exists (first-run state).
authenticatedbooleantrue when the request carries a valid session cookie.
usernamestring | nullPresent only when authenticated is true.

curl example

curl -s http://localhost/api/v1/auth/status
{"setup_needed": true, "authenticated": false}

First-Run Setup

POST /api/v1/auth/setup

Creates the single admin account. Only available when no credentials file exists (i.e., setup_needed: true). Returns a session cookie on success.

Request body

FieldTypeRequiredConstraints
usernamestringYes3–64 chars
passwordstringYes8–128 chars
{"username": "alice", "password": "a-good-passphrase"}

Response — 201 Created (+ Set-Cookie: cc_session=...)

{"username": "alice"}

Errors

StatusCondition
409 ConflictAlready initialized — setup has been completed.
422 Unprocessable EntityValidation failed (username/password constraints).

curl example

curl -s -X POST http://localhost/api/v1/auth/setup \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "a-good-passphrase"}'
{"username": "alice"}

# Already initialized:
curl -s -X POST http://localhost/api/v1/auth/setup \
-d '{"username": "alice", "password": "a-good-passphrase"}' \
-H "Content-Type: application/json"
{"error": "CONFLICT", "message": "already initialized", "details": null}

Login

POST /api/v1/auth/login

Validate username + password and receive a session cookie.

Request body

FieldTypeRequired
usernamestringYes
passwordstringYes
{"username": "alice", "password": "a-good-passphrase"}

Response — 200 OK (+ Set-Cookie: cc_session=...)

{"username": "alice"}

Errors

StatusCondition
401 UnauthorizedInvalid username or password.
409 ConflictSetup has not been run yet (setup_needed: true).

curl example

curl -s -c cookies.txt -X POST http://localhost/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "a-good-passphrase"}'
{"username": "alice"}

# Subsequent authenticated request using the saved cookie jar:
curl -s -b cookies.txt http://localhost/api/v1/auth/me
{"username": "alice"}

Logout

POST /api/v1/auth/logout

Clears the session cookie and bumps session_epoch, which immediately invalidates every outstanding session for the account — including sessions on other browsers or API clients using the old cookie.

Response — 204 No Content

No body. The Set-Cookie header in the response clears the cookie.

curl example

curl -s -o /dev/null -w "%{http_code}" -b cookies.txt \
-X POST http://localhost/api/v1/auth/logout
204

Verify (internal nginx target)

GET /api/v1/auth/verify
Internal endpoint

This endpoint is the auth_request subrequest target called by nginx before forwarding requests to Cortex. Do not call it directly in application code — it is not rate-limited separately and the response is only meaningful in the nginx subrequest context.

Checks the session cookie or Authorization: Bearer header. On success, returns 200 and sets the X-Auth-User response header to the authenticated username. nginx extracts this header and forwards it as X-Auth-User on the proxied request to Cortex.

Response — 200 OK (authenticated) X-Auth-User: alice

Response — 401 Unauthorized (not authenticated or invalid credentials)


Get current user

GET /api/v1/auth/me

Returns the authenticated caller's username. Useful for confirming which account a credential belongs to.

Auth required: session cookie or Authorization: Bearer cc_live_...

Response — 200 OK

{"username": "alice"}

Errors

StatusCondition
401 UnauthorizedNo valid session cookie or API key.

curl example

# With session cookie:
curl -s -b cookies.txt http://localhost/api/v1/auth/me
{"username": "alice"}

# With API key:
curl -s http://localhost/api/v1/auth/me \
-H "Authorization: Bearer cc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"username": "alice"}

Change password

POST /api/v1/auth/password

Rotates the admin password. Bumps session_epoch and clears the caller's session cookie, requiring re-login. All other outstanding sessions are also invalidated.

Auth required: session cookie or API key.

Request body

FieldTypeRequiredConstraints
old_passwordstringYesCurrent password
new_passwordstringYes8–128 chars
{"old_password": "a-good-passphrase", "new_password": "an-even-better-one"}

Response — 204 No Content

The Set-Cookie header in the response clears the caller's session cookie. Re-login with POST /login.

Errors

StatusCondition
401 UnauthorizedNo valid session.
403 Forbiddenold_password does not match.

curl example

curl -s -o /dev/null -w "%{http_code}" -b cookies.txt \
-X POST http://localhost/api/v1/auth/password \
-H "Content-Type: application/json" \
-d '{"old_password": "a-good-passphrase", "new_password": "an-even-better-one"}'
204

Change username

POST /api/v1/auth/username

Renames the admin account. Requires the current password. Issues a fresh session cookie bound to the new username. Bumps session_epoch, invalidating any other outstanding sessions.

Auth required: session cookie or API key.

Request body

FieldTypeRequiredConstraints
passwordstringYesCurrent password (for confirmation)
new_usernamestringYes3–64 chars
{"password": "an-even-better-one", "new_username": "bob"}

Response — 200 OK (+ new Set-Cookie: cc_session=... bound to new_username)

{"username": "bob"}

Errors

StatusCondition
401 UnauthorizedNo valid session.
403 ForbiddenPassword confirmation failed.

API Keys

API keys allow token-based access without a browser session. Useful for scripts, CI/CD pipelines, and any non-interactive client.

Format: cc_live_<32 url-safe base64 chars>

Storage: bcrypt-hashed in the credentials file. The plaintext key is shown exactly once, at creation time.

Usage: Authorization: Bearer cc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Verification: The prefix cc_live_ is checked first (cheap constant-time string comparison). bcrypt verification against stored hashes runs only for keys that pass the prefix check.


Create API key

POST /api/v1/auth/keys

Mints a new API key. The plaintext key is returned once — store it immediately.

Auth required: session cookie or API key.

Request body

FieldTypeRequiredConstraints
namestringYes1–64 chars. Human-readable label.
{"name": "CI Pipeline"}

Response — 201 Created

{
"id": "key_a1b2c3d4",
"name": "CI Pipeline",
"key": "cc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"created_at": "2026-04-26T14:30:00.000000"
}
Save the key now

The key field is returned only at creation time. It cannot be retrieved later — the stored value is a bcrypt hash.

Errors

StatusCondition
401 UnauthorizedNo valid session.
422 Unprocessable EntityValidation failed (e.g., empty name).

curl example

curl -s -X POST http://localhost/api/v1/auth/keys \
-b cookies.txt \
-H "Content-Type: application/json" \
-d '{"name": "CI Pipeline"}'
{
"id": "key_a1b2c3d4",
"name": "CI Pipeline",
"key": "cc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"created_at": "2026-04-26T14:30:00.000000"
}

List API keys

GET /api/v1/auth/keys

Returns all keys for the account. No secret material is included.

Auth required: session cookie or API key.

Response — 200 OK

[
{
"id": "key_a1b2c3d4",
"name": "CI Pipeline",
"created_at": "2026-04-26T14:30:00.000000",
"last_used_at": "2026-04-26T09:22:00.000000"
}
]
FieldTypeDescription
idstringStable key identifier. Use for revocation.
namestringHuman-readable label set at creation.
created_atdatetimeISO-8601 UTC creation timestamp.
last_used_atdatetime | nullnull until the key is first used. Updated on each successful verification.

curl example

curl -s -b cookies.txt http://localhost/api/v1/auth/keys
[{"id": "key_a1b2c3d4", "name": "CI Pipeline", "created_at": "...", "last_used_at": null}]

Revoke API key

DELETE /api/v1/auth/keys/{key_id}

Permanently revokes the API key. The key becomes unusable immediately.

Auth required: session cookie or API key.

Path parameters

ParameterTypeDescription
key_idstringKey ID returned by the create or list endpoint.

Response — 204 No Content

Errors

StatusCondition
401 UnauthorizedNo valid session.
404 Not FoundNo key with that ID exists.

curl example

curl -s -o /dev/null -w "%{http_code}" \
-b cookies.txt \
-X DELETE http://localhost/api/v1/auth/keys/key_a1b2c3d4
204

AttributeValue
Namecc_session (default; configurable via local_auth.cookie_name)
SigningHMAC-SHA256 (stateless; no session store)
TTL30 days (configurable via local_auth.cookie_ttl_seconds)
HttpOnlyYes — not accessible from JavaScript
SameSiteStrict
SecureSet when TLS is active (local_auth.cookie_secure)
Path/

Invalidation: bumping session_epoch in the credentials file immediately invalidates all outstanding cookies without any network round-trip. Epoch is bumped on logout, password change, and username change.


Error Envelope

All error responses use the unified envelope:

{"error": "ERROR_CODE", "message": "Human-readable message.", "details": {...}}

Common codes for auth endpoints:

HTTP StatuserrorWhen
401AUTH_REQUIREDNo session cookie or API key (via nginx; Cortex returns not authenticated)
403(varies)Wrong password on /password or /username
409CONFLICTAlready initialized (on setup) or setup required (on login before setup)
422VALIDATION_FAILEDRequest body failed field validation

Validation error structure:

{
"error": "VALIDATION_FAILED",
"message": "Request body failed validation",
"details": {"errors": [{"loc": ["body", "password"], "msg": "...", "type": "..."}]}
}

What Is NOT in the Auth Model

The following do not exist in Chaos Cypher and should not appear in any configuration:

  • JWT tokens — there is no jwt_secret_key, jwt_algorithm, or jwt_expiration_minutes. The session cookie is HMAC-SHA256, not JWT.
  • Refresh tokens — the session cookie is long-lived and reissued on login. There is no /refresh endpoint.
  • Multi-user accounts — there is one operator account. No registration, no user list, no admin vs. user roles.
  • auth.enabled setting — auth is always active. It cannot be disabled.