Files
cairnobs/docs/local-login.md
jcoffey-dev a24860ac2d Make local login reachable, and write down how it works
Local login is implemented, wired through api, alerting and web, and
undiscoverable. No compose file turns it on, the Helm chart sets none
of its variables, and no markdown in the repository mentions
-seed-admin, LOCAL_AUTH_ENABLED or local login at all. The only way to
find it is to read cmd/api/main.go's authorizer switch.

Enabling it in docker-compose.yml is not the answer: a plain
`docker compose up` has no authentication, and every Phase 0-3 runbook
verifies the pipeline with bare curl against /query. Turning login on
by default would break the project's own documented verification.
So it's an opt-in overlay instead.

Four settings have to agree, and only one of them is obviously about
login. Each fails differently and none of the failures name the cause:
the route 404s, or the browser refuses the request before sending it,
or login returns 200 and every later request is anonymous because the
cookie was never stored, or the same symptom again from the opposite
end because the bundle never attaches it. That is what the new document
is mostly for.

The Helm chart still has no local-login support. Recorded in the
document as a gap rather than papered over.

Signed-off-by: John Coffey <[email protected]>
2026-09-04 17:48:20 -07:00

4.0 KiB

Local login

Username and password authentication, served by api itself against the control-plane Postgres. It is the alternative to Phase 4's SSO (enterprise-auth, OIDC/SAML) for a deployment that wants a login screen without an identity provider behind it.

The two are mutually exclusive by construction. cmd/api/main.go picks one authorizer: ENTERPRISE_AUTH_URL wins if it is set, and only otherwise does LOCAL_AUTH_ENABLED take effect. With SSO configured, /auth/* is never registered and local login does not exist.

Off by default, and why

A plain docker compose up has no authentication at all. That is deliberate rather than an oversight: every Phase 0-3 runbook verifies the pipeline with bare curl against /query, and those steps only work against an unauthenticated API. Turning login on globally would break the project's own documented verification procedure.

So it is an opt-in overlay:

docker compose -f docker-compose.yml -f docker-compose.local-auth.yml up -d --build

--build is not optional. One of the four settings is a Vite build arg baked into the static bundle, so a restart alone leaves the frontend disagreeing with the backend.

Creating the first account

There is no sign-up. The first account is an operator action:

docker compose -f docker-compose.yml -f docker-compose.local-auth.yml \
  run --rm api -seed-admin
created default admin user:
  username: admin
  password: <generated>
this password will not be shown again -- save it now.

It creates admin as an owner with a random password, prints it once, and stores only a bcrypt hash. Losing it means resetting it, not recovering it. The command is idempotent: if the admin account already exists it says so and does nothing.

Then sign in at http://localhost:3000/login. Change the password from the account page.

The four settings, and why they must agree

Setting Service What it does
LOCAL_AUTH_ENABLED api, alerting Registers /auth/*, and swaps WithCORS for WithCredentialedCORS
CORS_ALLOWED_ORIGIN api, alerting Must be a literal origin — the default * is refused by browsers for credentialed requests
LOCAL_AUTH_COOKIE_SECURE api Defaults to true; the cookie is not stored over plain http://localhost unless this is false
VITE_LOCAL_AUTH_ENABLED web (build arg) Without it the bundle never sends credentials: 'include'

Only the first is obviously about login, and every one of them fails differently:

  • LOCAL_AUTH_ENABLED unset — the login page posts to /auth/login and gets a 404. The route is not registered at all, deliberately, so a deployment with the feature off answers as though it does not exist rather than advertising a disabled feature.
  • CORS_ALLOWED_ORIGIN left at * — the browser refuses the request before it is sent, and the page reports a network failure rather than an HTTP status.
  • LOCAL_AUTH_COOKIE_SECURE left at true over HTTP — login returns 200 and appears to work, then every subsequent request is unauthenticated, because the cookie was never stored.
  • VITE_LOCAL_AUTH_ENABLED unset — same symptom as the previous one, and from a different cause: the cookie exists but is never attached.

Set LOCAL_AUTH_COOKIE_SECURE back to true for anything served over HTTPS, and set CORS_ALLOWED_ORIGIN to the real web origin. The values in the overlay assume http://localhost:3000.

Roles

owner, admin, editor, viewer, from api/authz. Owners manage everyone; admins may create and delete viewer/editor accounts only, and cannot reset an owner's password. Two guards prevent a deployment from becoming unadministrable: the last owner can be neither deleted nor demoted, and no account can delete itself while signed in.

Not covered by the Helm chart

deploy/helm/cairnobs has no local-login support — it sets none of these variables. A Kubernetes deployment authenticates via enterprise-auth SSO or not at all. This is a gap, not a decision.