# Cognito setup
The service verifies OIDC JWTs from your existing Cognito user pool(s). One-time setup per pool.
You will need these values later:
| Value | Where it comes from | Used for |
|---|---|---|
AUTH_ISSUER |
Pool id + region | Worker config |
| Cognito domain | App integration → Domain | Token URL host |
| Client ID | App client detail page | $CLIENT_ID in the token curl |
| Client secret | Shown once when the app client is created (or “View” / rotate later) | $CLIENT_SECRET in the token curl |
# Publisher settings at a glance (PUBLISHER_SCOPES vs PUBLISHER_GROUPS)
These two Worker env vars control who is allowed to call POST /v1/notifications (publish to any user). They are not required for reading an inbox or for POST /v1/notifications/self.
| Setting | What it is | Do I need it? | What you create in Cognito |
|---|---|---|---|
PUBLISHER_SCOPES=notifications/publish |
Allowlist of OAuth scopes on the access token | Yes — this is the normal path for backends / jobs (M2M) | A resource server + custom scope, then an app client allowed to request that scope. No Cognito group. |
PUBLISHER_GROUPS=notification-publishers |
Allowlist of Cognito group names on a user token (cognito:groups) |
Optional. Only if humans/admins should publish with their interactive login | A Cognito user group with that exact name, and users added to it |
Rules of thumb:
- Most setups only need
PUBLISHER_SCOPES. Machine publishers (Assess, cron jobs, etc.) use client-credentials tokens that carry scopes, not groups — so groups would never help them. - You do not need to create the
notification-publishersgroup unless you deliberately want human publishers. If you are not using groups, omitPUBLISHER_GROUPSentirely (or leave it unset). - Regular end users need neither. Any valid user token can list / mark-read / delete their own inbox, and can self-notify via
/self. They must not be given the publish scope just to “make the SPA work.” - Publishing is allowed if the token matches either list (scope or group). At least one of the two env vars must be set and match the token, or every cross-user publish returns
403.
# Typical production / local .dev.vars — scopes only:
PUBLISHER_SCOPES=notifications/publish
# Only add this if you also created the Cognito group and put admin users in it:
# PUBLISHER_GROUPS=notification-publishers
The strings must match exactly what Cognito puts on the token (notifications/publish in scope, or notification-publishers in cognito:groups). You can comma-separate multiple values if needed.
# 1. Point the service at the pool
In AWS Console → Cognito → User pools → (your pool):
- Region is in the URL / top-right (e.g.
eu-west-2) - User pool ID is on the pool overview (e.g.
eu-west-2_AbCdEf123)
AUTH_ISSUER=https://cognito-idp.<region>.amazonaws.com/<userPoolId>
# example:
# AUTH_ISSUER=https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_AbCdEf123
Multiple pools hitting one deployment: comma-separate the issuers. JWKS URLs are derived automatically; keys are cached and rotation is handled (refetch on unknown kid).
Optionally pin accepted clients: AUTH_AUDIENCE=<appClientId1>,<appClientId2> (matches aud on id tokens and client_id on access tokens).
# 2. Users (inbox access)
Nothing to configure. Any valid token from an accepted issuer can read/manage its own inbox — the sub claim is the inbox key. Producers must address notifications to that same sub.
# 3. Server-to-server publishers (the normal case)
Cognito client-credentials tokens carry scopes, not groups, so publishers are authorized by scope.
# 3.1 Create a resource server + scope
- Open the user pool → App integration → Resource servers → Create resource server.
- Set:
- Resource server identifier:
notifications(exact string) - Resource server name: anything readable, e.g.
Notifications
- Resource server identifier:
- Under Custom scopes, add:
- Scope name:
publish - Description: e.g.
Publish notifications
- Scope name:
- Save.
The full scope string apps request is:
notifications/publish
(identifier + / + scope name)
# 3.2 Create an app client (this is where Client ID + Client secret come from)
Do this once per producer app (e.g. one client for Assess, one for another service).
- Same user pool → App integration → App clients → Create app client.
- App type: choose Confidential client (machine-to-machine).
Public / SPA clients do not get a client secret and cannot use client-credentials. - App client name: e.g.
notifications-totum-assess. - Client secret: ensure Generate a client secret is enabled (default for confidential clients).
- Authentication flows: enable Client credentials only (uncheck authorization-code / implicit if you do not need them for this client).
- Under Allowed OAuth scopes / custom scopes, select
notifications/publish(the resource-server scope from 3.1). - Create the client.
On the next screen (or App client → your client):
| Field | Where |
|---|---|
| Client ID | Shown as Client ID / Client id on the app client detail page. Copy it → this is $CLIENT_ID. |
| Client secret | Shown as Client secret. Cognito often shows it only once at creation — copy it immediately into a password manager / secrets store. That is $CLIENT_SECRET. If you lost it: open the app client → Actions → regenerate / rotate client secret (invalidates the old one). |
The Client ID also becomes the notification source on every publish from this client (visible to consumers; used in dedupe). One app client per producer keeps sources meaningful.
# 3.3 Find your Cognito domain (for the token URL)
Still under App integration → Domain:
- Either a Cognito domain prefix:
https://<prefix>.auth.<region>.amazoncognito.com - Or a custom domain you configured
That host is what goes in the token curl (not the issuer URL).
Example token endpoint:
https://mycompany-dev.auth.eu-west-2.amazoncognito.com/oauth2/token
# 3.4 Fetch an M2M access token
Service config must include:
PUBLISHER_SCOPES=notifications/publish
PowerShell (Windows) — use curl.exe so PowerShell does not alias curl to Invoke-WebRequest:
$ClientId = "xxxxxxxxxxxxxxxxxxxxxxxxxx" # from app client detail
$ClientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # from app client secret
$CognitoDomain = "https://<prefix>.auth.<region>.amazoncognito.com"
curl.exe -X POST "$CognitoDomain/oauth2/token" `
-H "Content-Type: application/x-www-form-urlencoded" `
-u "${ClientId}:${ClientSecret}" `
-d "grant_type=client_credentials&scope=notifications/publish"
macOS / Linux (bash):
export CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxx"
export CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export COGNITO_DOMAIN="https://<prefix>.auth.<region>.amazoncognito.com"
curl -X POST "$COGNITO_DOMAIN/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials&scope=notifications/publish"
-u "id:secret" is HTTP Basic auth (curl base64-encodes it for you).
Response includes access_token and expires_in. Cache the token until it expires; do not fetch one per notification.
Decode the access token at jwt.io and confirm:
client_idequals your Client IDscopecontainsnotifications/publishissequals yourAUTH_ISSUER
# 4. Human/admin publishers (optional — Cognito groups)
Skip this entire section unless a person (not a backend) needs to publish notifications while signed in with a normal user token.
M2M app clients cannot use groups — their tokens have scopes only. For backends, §3 + PUBLISHER_SCOPES is enough.
If you do want human publishers:
- Cognito → user pool → Groups → Create group.
- Group name: e.g.
notification-publishers(remember the exact spelling). - Add the admin users to that group.
- On the Worker set:
PUBLISHER_GROUPS=notification-publishers
(Keep PUBLISHER_SCOPES=notifications/publish as well if you still have M2M publishers.)
Their user tokens (which carry cognito:groups) can then publish; source falls back to their sub.
Regular end users need no group — they only read their own inbox (and may use /self).
# Non-Cognito IdPs
The defaults are Cognito-shaped but overridable: AUTH_SUB_CLAIM (default sub), AUTH_GROUPS_CLAIM (default cognito:groups), AUTH_JWKS_URL (default {issuer}/.well-known/jwks.json). Any OIDC issuer that signs RS256/ES256 JWTs works.