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.
- Sign-in experience: tick Email. Leave the other options unticked — we don't use username / phone sign-in.
- Password policy: Cognito defaults are fine for dev. Tighten for prod (12+ characters, all character classes).
- 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. - User account recovery: keep the default ("Email only").
- Required attributes: tick email (required). Add name
as required —
CognitoService.createUsersends anameattribute with the user's display name. - Self-registration: disable it. The platform is invitation- only.
- 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. - Message customization: leave the default invitation template ("Your username is {username} and temporary password is {####}.") for dev. Override the wording later if you want.
- App integration → App client:
- App client name:
clinical-platform(or similar) - App type: Confidential client
- 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.
- 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:
AdminCreateUserAdminDisableUserAdminEnableUserAdminDeleteUserAdminResetUserPasswordAdminGetUser(defensive — used by some lookups)
It needs AWS credentials with permission for these on the specific pool ARN.
Console path: IAM → Users → Create user.
- Username:
clinical-platform-user-mgmt-dev(keep dev / prod creds separate). - 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.
- Attach the policy to the new user.
- 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:
- Promote a clinical patient from
/users/duplicatesor via the Promote button on/users. - The new row appears in the platform users list with type=Patient
and a real Cognito-shaped
external_id(a UUID, nomock-prefix). - 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 includingCognitoModulewiring + the mock/real provider switch.services/user-management/src/cognito/—CognitoService(real),CognitoMockService(dev default), and the factory incognito.module.ts.services/auth/src/config/app-config.ts—cognitoIssuerFromEnvshows the issuer-derivation logic ifCOGNITO_ISSUERis left blank.