Skip to main content

Authentication

ChatWalaʻau has two complementary auth mechanisms: API keys (Bearer tokens you issue per caller) and an optional web sign-in (username/password) for cloud deployments.

Breaking change in v0.215.0

API_KEY and RESPONSES_API_KEY in .env are no longer read. A value still set is named in a startup warning and ignored. Callers that used them get 401 until you give them a managed key (below). Old values are not imported.

API keys​

Every caller gets its own key. A key has a name, one scope, an optional expiry and an enabled switch.

ScopeMay callGive it to
responsesPOST /v1/responses onlya company site's server, an external app
apievery API except key management (/api/*, /ag-ui/, WebSockets, /v1/responses)the CLI, automation
admineverything, including key managementonly where keys are managed

Create a key​

  • API Keys screen: the key icon in the sidebar footer (wide screens). It appears when you are signed in through the browser or use ChatWalaʻau on the same machine. Issue, edit, disable / enable, reissue and delete keys there; each action shows a spinner until it is done.
  • CLI on the server (also how you make the first key when there is no web sign-in):
chatwalaau keys create --name company-site-bff --scope responses --expires-days 90
chatwalaau keys list
chatwalaau keys disable <id> # keep it, but refuse it
chatwalaau keys enable <id>
chatwalaau keys reissue <id> # a new secret; the old one stops working
chatwalaau keys delete <id>

The secret (cwk_<id>_...) is shown once, when it is issued or reissued. Only its SHA-256 hash is stored, in the file named by API_KEYS_FILE, with an audit log of every change beside it:

API_KEYS_FILE=api_keys.jsonc # default; on Azure App Service use a path under /home
# APP_REQUIRE_AUTH_ON_LAN=true # default: fail-closed on LAN without a key

Changes apply on the next request, without a restart. api_keys.jsonc and its audit log are ignored by Git, Docker builds and the public repository sync.

What is accepted where​

Same-machine clients (127.0.0.1, ::1, localhost) pass the write endpoints and the AG-UI stream without a key, so localhost development stays zero-configuration even when APP_HOST=0.0.0.0.

Client addressAPP_REQUIRE_AUTH_ON_LANActive key or web sign-inOutcome on /api/* and POST /ag-ui/
loopbackanyanyallow
LANfalseanyallow (operator opt-out)
LANtrueneither503
LANtrueyesan api / admin key (or the sign-in cookie) required
  • /v1/responses always requires a key, of any scope, regardless of client address (503 when no key is active).
  • An unknown, expired or disabled key gets 401; a key whose scope is too narrow gets 403.
  • Key management accepts only web sign-in, the same machine or an admin key -- never the APP_REQUIRE_AUTH_ON_LAN=false open mode -- and a caller cannot grant a scope above its own.
  • APP_REQUIRE_AUTH_ON_LAN stays in .env; the API Keys screen shows it read-only.

Per-key limits​

Since v0.216.0 a key can carry its own /v1/responses limits, set in the API Keys screen under Limits for /v1/responses:

FieldMeaning
requests_per_minute / requests_per_day / requests_per_monthRequests this key may make
total_tokens_per_day / total_tokens_per_monthInput plus output tokens this key may spend

Empty or 0 means no limit for this key. The global limits in App Settings apply as well (usage limits). The screen shows each key's usage today and this month. A key over its limit gets 429 with a key_ code. The other keys keep working, so one leaked key cannot use up the whole budget. Over HTTP, send "limits": {...} with POST or PATCH /api/api-keys; null clears them.

Agent and conversations per key​

Since v0.218.0 a key can be bound to a custom agent and choose how its /v1/responses conversations are stored, in the API Keys screen under Agent and conversations for /v1/responses:

FieldMeaning
AgentNone (the request chooses the model), or a custom agent: the key then runs only that agent
ConversationsSaved (listed in the chat history), or Temporary (never listed, deleted after the retention period)

The list marks strict agents (documents only) and agents with problems (has problems); a key bound to an agent with problems gets 503 until it is fixed. Over HTTP, send "agent" (null unbinds) and "storage" with POST or PATCH /api/api-keys. From the server: chatwalaau keys set ID --agent hr-desk --storage temporary (--no-agent unbinds). See Document desk.

Upgrading to v0.215.0​

  1. Upgrade. Callers using API_KEY / RESPONSES_API_KEY now get 401.
  2. Optionally set API_KEYS_FILE (under /home on Azure App Service).
  3. Create one key per caller: responses for a BFF or SDK app, api for the CLI (CHATWALAAU_API_KEY) and automation, admin only where keys are managed.
  4. Remove API_KEY and RESPONSES_API_KEY from .env and your platform configuration.

A responses key replaces RESPONSES_API_KEY with the same guarantee: its holder can call the API but cannot change the settings that bound it (operator controls). Requests from a loopback peer still pass /api/* without a key, so run such a caller on another host.

Web SPA authentication (optional)​

For deploying ChatWalaʻau as a private cloud web app where a single operator signs in through the browser. It coexists with API keys (still used for CLI / SDK access) and is disabled by default -- without AUTH_USERNAME there is no behavior change.

AUTH_USERNAME=admin
AUTH_PASSWORD_HASH=scrypt$N=16384,r=8,p=1$<base64-salt>$<base64-hash>
# AUTH_SESSION_TTL_SECONDS=86400 # default 24h, sliding
# AUTH_COOKIE_SECURE=auto # auto / true / false
# AUTH_COOKIE_NAME=chatwalaau_session
# AUTH_SESSION_PERSIST=true # sessions survive a restart (default)
# AUTH_SESSION_STORE_PATH=.auth/session_tokens.json

Generate the hash with the bundled CLI:

chatwalaau hash-password # interactive (confirms twice)
echo "$PASSWORD" | chatwalaau hash-password --stdin --quiet # scripted

When AUTH_USERNAME is set, the SPA renders a /login page; the server validates credentials in constant time and issues an opaque token via an HttpOnly + SameSite=Strict cookie. The backend then accepts either a Bearer API key (scope api or admin) or a valid session cookie on every write endpoint and the AG-UI stream. The /v1/responses external-app path stays Bearer-only.

  • No new Python dependency (stdlib hashlib.scrypt + secrets)
  • Single-user, single-process model
  • HTTPS strongly recommended for non-loopback deployments
  • Loopback CLI calls keep their no-credential bypass

Sessions survive a restart​

Since v0.104.0, signing in survives a backend restart: a deploy, a crash, or a dev-server relaunch no longer sends every browser back to the login page. The server stores only the SHA-256 digest of each session token in AUTH_SESSION_STORE_PATH -- never the token itself -- so the file cannot be turned into a working cookie and needs no encryption. It is written atomically, is 0600 on POSIX, and is git-ignored.

To sign every session out, pick whichever fits your workflow:

ActionEffect
Delete AUTH_SESSION_STORE_PATHAll sessions end at the next restart
Rotate AUTH_PASSWORD_HASHAll sessions end immediately at the next restart
AUTH_SESSION_PERSIST=falseRestores the old behavior: sessions die with the process

Restarting the process alone no longer signs users out.

When a session does expire​

If a session expires (TTL, logout elsewhere, a rotated password), the app does not reload to /login. It opens a sign-in dialog over your intact chat, so the message you were typing, your attachments, and your model selection are all preserved. Signing in returns you exactly where you were. The /login page is still used at first load and after an explicit sign-out.

Likewise, if the server is unreachable when you press Send, your message is returned to the input box with a Retry button rather than being discarded.