Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lambda-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ jobs:
fail=1
fi
if printf '%s\n' "$sql" | grep -inE '^[[:space:]]*INSERT[[:space:]]+INTO[[:space:]]+(branch\.)?users\b'; then
echo "::warning file=$f::inserting users in a migration puts rows in PRODUCTION. Seeded users with a NULL cognito_sub are claimable by POST /auth/register, so this can hand someone an account. Dev seed rows belong in apps/backend/db/seed.sql."
echo "::warning file=$f::inserting users in a migration puts rows in PRODUCTION. Dev seed rows belong in apps/backend/db/seed.sql."
fi
done
exit $fail
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ any environment is bootstrapped in SQL:
cd apps/backend && make grant-admin EMAIL=you@example.com
```

Accounts are invitation-only: a `branch.users` row with `cognito_sub IS NULL` is a
pending invitation, and `POST /auth/register` claims it. A Cognito user created
out of band has no matching row and will be rejected.
Accounts are admin-created: `POST /users` calls Cognito `AdminCreateUser` and writes
the `branch.users` row with its `cognito_sub`. There is no self-serve signup. A
Cognito user created out of band has no matching row and will be rejected.

## Documentation

Expand Down
8 changes: 3 additions & 5 deletions apps/backend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Defaults work without `.env` (DB: branch_dev/password@postgres:5432/branch_db).

**Shared dev-server (single service iteration)** — from a lambda dir (`npm run dev`). All lambdas register on **port 3000**; first one started owns the server, others register via `POST /_register`. Routes dispatch by first path segment:
```
http://localhost:3000/auth/register
http://localhost:3000/auth/login
http://localhost:3000/donors # GET /
http://localhost:3000/<service>/swagger # Swagger UI from openapi.yaml
http://localhost:3000/<service>/health
Expand Down Expand Up @@ -78,11 +78,9 @@ Automatic on push to `main` touching `apps/backend/lambdas/**` or `shared/types/

**`branch.users.is_admin` is the single source of truth for admin.** There is no promotion from a Cognito group, and no pre-token-generation trigger, so `is_admin` is not a JWT claim — `GET /auth/me` is the only way a client can learn it.

**Account provisioning is invitation-only.** A `branch.users` row with `cognito_sub IS NULL` is a pending invitation, created by `db/seed.sql` or by admin `POST /users` (ADMIN-gated). `POST /auth/register` is public, so it deliberately **cannot create a row** — it only claims an existing invitation, setting `cognito_sub` and never touching `is_admin`. An email with no pending invitation gets 403 `INVITATION_REQUIRED`; an already-claimed one gets 409.
**Account provisioning is admin-only.** There is no self-serve signup: the pool sets `allow_admin_create_user_only`, so Cognito `SignUp` is refused, and `/auth/register`, `/auth/verify-email` and `/auth/resend-code` no longer exist. Admin `POST /users` (ADMIN-gated) calls `AdminCreateUser`, which mails a temporary password, and inserts the `branch.users` row with `cognito_sub` already set.

The 403 is intentionally identical whether or not the address exists, so registration cannot be used to enumerate staff emails.

This is the real control, not the Cognito pool config: `authenticateRequest` rejects any Cognito identity whose `sub` has no `branch.users` row, so a Cognito user created out of band is inert. Full flow: admin `POST /users` → invitee `POST /auth/register` with that email → `POST /auth/verify-email` with the emailed code → `POST /auth/login`.
`authenticateRequest` rejects any Cognito identity whose `sub` has no `branch.users` row, so a Cognito user created out of band is inert — and a `branch.users` row with a NULL `cognito_sub` can never sign in. Full flow: admin `POST /users` → invitee `POST /auth/login` with the temporary password → `POST /auth/respond-challenge` to satisfy `NEW_PASSWORD_REQUIRED`.

**Bootstrapping the first admin** is a manual SQL statement in every environment, because `is_admin` can only be set by an existing admin: `make grant-admin EMAIL=…` locally, or the equivalent `UPDATE` against RDS in production.

Expand Down
3 changes: 0 additions & 3 deletions apps/backend/lambdas/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ Lambda for auth handler.
| Method | Path | Description |
|--------|------|-------------|
| GET | /auth/health | Health check |
| POST | /auth/register | |
| POST | /auth/login | |
| POST | /auth/respond-challenge | |
| POST | /auth/refresh | |
| GET | /auth/me | |
| POST | /auth/verify-email | |
| POST | /auth/resend-code | |
| POST | /auth/logout | |
| POST | /auth/forgot-password | |
| POST | /auth/reset-password | |
Expand Down
288 changes: 0 additions & 288 deletions apps/backend/lambdas/auth/controllers/register.ts

This file was deleted.

Loading
Loading