Authentication
ChatWalaʻau has two complementary auth mechanisms: a unified API key (Bearer token) and an optional web sign-in (username/password) for cloud deployments.
Unified API key
API_KEY is a single Bearer token that protects the external OpenAI API, every
write REST endpoint, and the AG-UI chat stream when reached from a non-loopback
(LAN) client. Same-machine clients (127.0.0.1, ::1, localhost) bypass auth,
so localhost development stays zero-configuration even when APP_HOST=0.0.0.0.
API_KEY=sk-chatwalaau-your-secret-key-here
# APP_REQUIRE_AUTH_ON_LAN=true # default: fail-closed on LAN without a key
Decision matrix for write endpoints and the AG-UI chat stream (POST /ag-ui/):
| Client address | APP_REQUIRE_AUTH_ON_LAN | API_KEY | Outcome |
|---|---|---|---|
| loopback | any | any | allow |
| LAN | false | any | allow (operator opt-out) |
| LAN | true | empty | 503 |
| LAN | true | set | Bearer required |
/v1/responses always requires a matching Bearer key regardless of client address.
If APP_HOST is non-loopback and API_KEY is unset, the AG-UI stream now returns
the same 503 / 401 as every other write endpoint. Add API_KEY=..., or accept LAN
exposure explicitly with APP_REQUIRE_AUTH_ON_LAN=false.
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_KEY (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
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:
| Action | Effect |
|---|---|
Delete AUTH_SESSION_STORE_PATH | All sessions end at the next restart |
Rotate AUTH_PASSWORD_HASH | All sessions end immediately at the next restart |
AUTH_SESSION_PERSIST=false | Restores 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.