Skip to content

feat(auth): hosted sign-in with CHT credentials - #34

Draft
witash wants to merge 1 commit into
masterfrom
33-cht-login
Draft

witash wants to merge 1 commit into
masterfrom
33-cht-login

Conversation

@witash

@witash witash commented Oct 15, 2026

Copy link
Copy Markdown

Closes #33. Builds on the hosted mode from #11 / #13.

What: in hosted mode, people sign in with their CHT username and password against one of a configured list of CHT instances, instead of creating an email + password account in the builder. The server makes the same checks cht-user-management makes, and lets in admins and users whose roles hold can_configure (configurable).

Why this was necessary: hosted sign-up was open to anyone who could reach the URL, with no email verification, no password reset and no invites. docs/deployment-requirements.md had to tell whoever deploys it to put separate authentication in front. The people who should use the builder already have an identity that says whether they may configure an app: their CHT user.

What it solves: access is granted and revoked in CHT, as it is for cht-user-management. Nobody manages a second set of passwords, and the builder stores no password at all.


What changed

Sign-in against CHT (server/src/chtAuth.ts). The checks follow cht-user-management's src/lib/cht-session.ts:

  1. POST <instance>/_session checks the password.
  2. GET <instance>/medic/org.couchdb.user:<name> reads the user's roles. A CouchDB server admin has no user-settings doc, so their _session roles are used.
  3. GET <instance>/api/v1/settings reads which roles hold which permissions. This is skipped for admins.

The CHT session cookie is used only for those reads and is closed straight after (DELETE /_session, awaited). Errors say what went wrong: wrong password is 401; a missing permission is 403 and names the permission; an unreachable instance is 502; a timeout after 15 s is 504.

Instance allow-list (server/src/config.ts).

  • CHT_INSTANCES is a comma-separated list of base URLs, validated at boot: http(s) only, no embedded credentials, de-duplicated, trailing slash dropped. Hosted mode refuses to start without it.
  • Only listed instances are ever contacted, so the login form cannot be used to relay passwords to an arbitrary host.
  • CHT_REQUIRED_PERMISSIONS defaults to can_configure.

Accounts and sessions (server/src/auth.ts).

  • POST /api/auth/login {instance, username, password} replaces sign-up and email login.
  • GET /api/auth/instances is unauthenticated and feeds the form.
  • An account is just (instance, username). It is created on first sign-in, with an id derived from that pair, so the same person always lands on the same projects. Usernames are lower-cased.
  • users.json holds no password hashes. Bearer tokens and 30-day sessions are unchanged.
  • /api/auth/me returns the CHT username and the instance host.

Sign-in screen (client/src/ui/SignIn.tsx). Instance picker (read-only when only one instance is configured), username and password. "Create an account" is gone. The server's message is shown as-is, e.g. Your CHT user on … is missing: can_configure.

Test double (scripts/fake-cht.mjs). Just enough of a CHT for the four endpoints above. It has a program officer who holds can_configure, a CHW who does not, and a server admin, so no real instance is needed in CI.

Docs. README.md, docs/hosted-deploy.md, docs/deployment-requirements.md (new env vars, the deploy-check curl, revised security properties), docs/plans/hosted-authoring.md, the Dockerfile usage comment and the CI step names.

Before deploying ⚠️

  • Set CHT_INSTANCES on every hosted deployment (including the Create a hostable package to test CHT UI builder #11 test hosting) before rolling this out. Without it the server will not start.
  • Existing email + password accounts stop working. Their projects stay on disk under DATA_ROOT/users/<old id>/ but are not moved to the new (instance, username) account. Tell current test users to download anything they want to keep.
  • Revoking access is not instant. Sessions last 30 days and are not re-checked against CHT, so revoking someone's CHT access takes effect at their next sign-in. Deleting their row in DATA_ROOT/auth/sessions.json signs them out immediately.

Tests

  • server/src/chtAuth.test.ts (new, against fake-cht.mjs): permitted user let in with roles; wrong password 401; missing permission 403 and named; server admin without a user doc let in; CHT session closed after the checks; unreachable instance 502; missingPermissions and parseInstances edge cases.
  • scripts/hosted-acceptance.mjs: sign-in replaces sign-up. Adds wrong password, missing permission, an instance outside CHT_INSTANCES refused (no relay), /api/auth/signup gone, admin sign-in, same account on re-login in any letter case, no password in any response, and /api/auth/me naming the user and instance. The isolation checks are unchanged.
  • client/tests/hosted-authoring.spec.ts: every journey now signs in with CHT credentials. New test: a CHW is refused and told can_configure is missing. playwright.hosted.config.ts starts fake-cht.mjs alongside the server.

Validation

Check Result
pnpm build ok
pnpm typecheck clean
pnpm --filter @cht-ui/server test 92 pass, 0 fail
node scripts/hosted-acceptance.mjs all checks pass (hosted and desktop)
playwright test -c playwright.hosted.config.ts 5 pass, 0 fail
pnpm lint fails on master already (164 problems, identical count on this branch); the 13 touched source files lint clean
pnpm --filter @cht-ui/shared test 6 failures, unrelated: this PR touches nothing in shared/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Log in with CHT credentials

1 participant