Skip to content

[Backend] Admin-invite user provisioning via Cognito AdminCreateUser #243

Description

@nourshoreibah

Decision (locked)

Adopt admin-invite provisioning via Cognito AdminCreateUser. There is no open self-signup — only an admin creates accounts. This keeps Cognito as the single identity source and eliminates the orphaned-row state described below.

Background / the bug this fixes

authenticateRequest (shared/lambda-auth/src/authenticate.ts:50) looks users up in branch.users by cognito_sub and returns isAuthenticated:false when there is no match.

  • Self POST /register (auth lambda) creates a Cognito user and a branch.users row with cognito_sub. Works — but is open signup, which we don't want.
  • Admin POST /users (users/handler.ts:221) inserts a branch.users row with no Cognito account and null cognito_sub → that user can never authenticate.

Key fact that makes this clean: AdminCreateUser returns the new user's sub in User.Attributes. So we can store cognito_sub at creation time — no post-confirmation trigger and no email-linking step required.


Implementation

1. Backend — users lambda: POST /users (admin only)

  • Keep the existing ADMIN authorization gate.
  • Validate body (email, name, isAdmin).
  • Call Cognito AdminCreateUser:
    • DesiredDeliveryMediums: ['EMAIL'] (sends the invite email + temporary password).
    • UserAttributes: email, email_verified: true, name.
    • Let Cognito generate the temp password (omit TemporaryPassword).
  • Read sub from the AdminCreateUser response User.Attributes.
  • Insert the branch.users row with cognito_sub = that sub, plus email, name, is_admin.
  • Rollback (mirror the auth register flow): if the DB insert fails, call AdminDeleteUser to keep Cognito and the DB in sync.
  • Handle duplicate email: if AdminCreateUser throws UsernameExistsException, return 409.

2. Backend — auth lambda

  • Remove / disable open POST /register (no public signup).
  • POST /login must handle the NEW_PASSWORD_REQUIRED challenge — invited users are created in FORCE_CHANGE_PASSWORD state and must set a password on first login. Implement the RespondToAuthChallenge (NEW_PASSWORD_REQUIRED) exchange, returning the challenge to the client and accepting the new password.

3. Infra — infrastructure/aws/cognito.tf

  • Add an admin_create_user_config block with allow_admin_create_user_only = true (disables public sign-up) and an invite email template (invite_message_template).
  • IAM: the users lambda execution role needs cognito-idp:AdminCreateUser, AdminDeleteUser, AdminGetUser on the pool. (Add to infrastructure/aws/lambda.tf; the auth lambda already has its Cognito perms — mirror them.)
  • Add COGNITO_USER_POOL_ID (+ client id) to the users lambda environment.

4. Frontend


Acceptance criteria

  • Admin creates a user → invite email is sent, a branch.users row exists with a non-null cognito_sub.
  • Invited user sets a password on first login and can immediately call other lambdas (authenticated).
  • Public self-registration is no longer possible.
  • DB insert failure rolls back the Cognito user (no orphans either direction).
  • Duplicate email returns 409, not a 500.

Edge cases / notes

  • Seed/legacy users (Ashley, Renee, Nour in db_setup.sql) have null cognito_sub. They need either re-invite via the new flow or a one-time backfill — call out in the migration step.
  • Decide invite email copy / branding for the Cognito invite template.

Out of scope

  • Bulk invite / CSV import.
  • Role management UI beyond isAdmin.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions