Skip to content

Wiring a Cognito User Pool into the Platform

services/user-management uses Cognito for platform-user identity: inviting new users, disabling / re-enabling them, and triggering password resets. By default the service ships with COGNITO_PROVIDER=mock (see services/user-management/.env.example) so local dev doesn't need an AWS account — but the mock skips the real AdminCreateUser call, which means no invitation email is sent when you promote a clinical patient or create a user.

This runbook walks through setting up a real Cognito user pool and flipping the service over to use it. Roughly a 15-minute job for a fresh dev pool.

Prerequisites

  • An AWS account you have admin rights in (sandbox for dev; the shared platform account for shared envs).
  • IAM permissions to create a user pool, an IAM user, and an IAM policy.
  • A region picked up front. The code defaults to eu-west-2; match whatever the rest of your stack uses.

1. Create the Cognito user pool

Console path: Amazon Cognito → User pools → Create user pool.

  1. Sign-in experience: tick Email. Leave the other options unticked — we don't use username / phone sign-in.
  2. Password policy: Cognito defaults are fine for dev. Tighten for prod (12+ characters, all character classes).
  3. MFA: leave No MFA. The platform handles MFA itself in admin-api; stacking Cognito's MFA on top would double-prompt admins and confuse the flows.
  4. User account recovery: keep the default ("Email only").
  5. Required attributes: tick email (required). Add name as required — CognitoService.createUser sends a name attribute with the user's display name.
  6. Self-registration: disable it. The platform is invitation- only.
  7. Email verification: tick Send email message, verify email address. For dev, leave Send email with Cognito selected (gives ~50 free emails per day from no-reply@verificationemail.com). For prod, switch to Send email with Amazon SES with a verified domain — appears as an option on this step once you've moved SES into production access.
  8. Message customization: leave the default invitation template ("Your username is {username} and temporary password is {####}.") for dev. Override the wording later if you want.
  9. App integration → App client:
  10. App client name: clinical-platform (or similar)
  11. App type: Confidential client
  12. Authentication flows: tick ALLOW_ADMIN_USER_PASSWORD_AUTH and ALLOW_REFRESH_TOKEN_AUTH. Don't tick the SRP variant unless a downstream consumer needs it.

The App Client is optional if you only need invitations + sign-in; it becomes load-bearing when you wire services/auth's token- exchange grant against Cognito. Create it now — easier than retrofitting.

  1. Click Create user pool.

Record three values from the post-create overview:

Value Where Looks like
User Pool ID Pool overview, top eu-west-2_AbCdEf123
App client ID Pool → App integration → App clients and analytics 1abc23def…
App client secret Same page, Show client secret long base64-ish string

2. Create the IAM user for user-management's AWS SDK calls

CognitoService calls these actions against the pool:

  • AdminCreateUser
  • AdminDisableUser
  • AdminEnableUser
  • AdminDeleteUser
  • AdminResetUserPassword
  • AdminGetUser (defensive — used by some lookups)

It needs AWS credentials with permission for these on the specific pool ARN.

Console path: IAM → Users → Create user.

  1. Username: clinical-platform-user-mgmt-dev (keep dev / prod creds separate).
  2. Permissions: Attach policies directly → Create policy. Paste this, substituting <REGION>, <ACCOUNT_ID> (12 digits, top-right of the console), and <USER_POOL_ID>:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cognito-idp:AdminCreateUser",
        "cognito-idp:AdminDisableUser",
        "cognito-idp:AdminEnableUser",
        "cognito-idp:AdminDeleteUser",
        "cognito-idp:AdminResetUserPassword",
        "cognito-idp:AdminGetUser"
      ],
      "Resource": "arn:aws:cognito-idp:<REGION>:<ACCOUNT_ID>:userpool/<USER_POOL_ID>"
    }
  ]
}

Save it as clinical-platform-user-mgmt.

  1. Attach the policy to the new user.
  2. Once the user exists, open it → Security credentials → Create access key → Application running outside AWS. Copy the Access key ID + Secret access key. The secret is shown once only — stash it somewhere safe (pass, 1Password, etc.).

Prod note: skip the IAM user. Attach the policy to the ECS / EC2 / EKS execution role instead. The AWS SDK's default credential chain picks up the role automatically — no env vars required.

3. Wire the values into .env files

services/user-management/.env

COGNITO_PROVIDER=cognito
COGNITO_USER_POOL_ID=eu-west-2_AbCdEf123
COGNITO_REGION=eu-west-2

# AWS SDK creds — picked up by the default AWS credential chain.
AWS_ACCESS_KEY_ID=AKIA…
AWS_SECRET_ACCESS_KEY=# AWS_SESSION_TOKEN=…   # only when using STS / SSO temporary creds

services/auth/.env

COGNITO_USER_POOL_ID=eu-west-2_AbCdEf123
COGNITO_REGION=eu-west-2
COGNITO_APP_CLIENT_ID=<app-client-id-from-step-1.9>
# Optional override; otherwise derived from pool id + region:
# COGNITO_ISSUER=https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_AbCdEf123

The auth service only needs these if you're using the token-exchange grant. For "promote a user → they get an invite email", the auth service settings are optional — services/user-management does the work directly.

4. Restart and verify

scripts/dev/start-all.sh

(or just restart user-management if the rest of the stack is up already).

Watch the user-management log on the next promote. The mock log line:

[MOCK] Created user mock-<uuid> for <email>

should be replaced with the real-Cognito variant:

[CognitoService] Created Cognito user <sub-uuid> for <email>

The recipient's inbox should receive a Cognito invitation email within ~30 seconds. Check spam — the default no-reply@verificationemail.com sender lands in junk for some providers.

End-to-end smoke test:

  1. Promote a clinical patient from /users/duplicates or via the Promote button on /users.
  2. The new row appears in the platform users list with type=Patient and a real Cognito-shaped external_id (a UUID, no mock- prefix).
  3. Recipient clicks the email link → first sign-in forces a password reset → they're in.

Things that bite

Symptom Cause Fix
Promote succeeds but no email arrives Cognito default sender quota (~50/day) exhausted, or the recipient's provider blocked verificationemail.com Configure Amazon SES with a verified domain; resend via User pool → Users → user → Actions → Resend invitation.
Email arrives, link returns "User not found" Cognito user was hard-deleted; usernames are reserved ~14 days after delete Pick a different email or wait out the cool-off.
Promote 500s with AccessDeniedException IAM policy's Resource ARN doesn't match the actual pool Re-check arn:aws:cognito-idp:<REGION>:<ACCOUNT_ID>:userpool/<USER_POOL_ID>. Region + account id + pool id must all match.
Promote 500s with NotAuthorizedException AWS access key / secret missing or wrong Confirm AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY are set in services/user-management/.env and the service restarted.
User created in Cognito but later promote fails with 409 A user-management User row already exists for the email (perhaps from an earlier mock-mode promote) Delete the orphaned User row, or pick a different email.
Auth tokens minted by Cognito rejected by services/auth COGNITO_APP_CLIENT_ID in services/auth/.env doesn't match the App Client id, so the aud check fails Copy the App Client id from Cognito → User pool → App integration → App clients into the env var; restart auth.

Where to learn more

  • docs/audiences/tech/services/user-management.md — service overview including CognitoModule wiring + the mock/real provider switch.
  • services/user-management/src/cognito/CognitoService (real), CognitoMockService (dev default), and the factory in cognito.module.ts.
  • services/auth/src/config/app-config.tscognitoIssuerFromEnv shows the issuer-derivation logic if COGNITO_ISSUER is left blank.