# Amazon Cognito Machine-to-Machine Authentication for Notifications
Document purpose: Explain what the proposed Cognito changes are, why they are required, how they differ from the existing user-login setup, and how to introduce them safely in the production user pool.
Applies to: Notifications publishing in production
Last verified against AWS documentation: 22 July 2026
Related docs in this repo:
| Doc | Role |
|---|---|
| Cognito explained | Short plain-language “why” (resource servers, scopes, secrets) |
| Cognito setup | Click-by-click console how-to |
| Local development | Laptop dry run (API keys / optional M2M) |
| Setup guide | Cloudflare deploy including Worker env vars |
| Publishing | App-facing publish paths |
# Executive summary
The proposal is not to create another Cognito user pool and it does not create, copy, move, modify, or delete any users.
The proposal is to add two small pieces of OAuth configuration to the existing production user pool:
- A resource server definition called, for example,
notifications, containing a permission calledpublish. - A dedicated machine-to-machine app client, such as
notifications-publisher-production, which is allowed to request the permissionnotifications/publish.
The existing app client, auth-service-production-user-pool-client, is intended for human users signing in. It has no client secret because it is a public client used by browser or client-side code. It should remain unchanged.
The new app client is for a trusted backend process rather than a person. It has a client ID and client secret, exchanges those credentials for a short-lived access token, and presents that token when publishing a notification.
Creating a separate app client and resource server is an established Cognito pattern. It is an additive change and, when performed correctly, does not invalidate existing sessions or change existing users, groups, passwords, multi-factor authentication (MFA) settings, triggers, or app clients.
However, it would be inaccurate to describe any production change as completely risk-free. The safe conclusion is:
Adding a dedicated machine-to-machine app client and a narrowly scoped resource server is a low-risk, isolated production change, provided the existing user-login client and pool-wide settings are not edited, the secret is stored securely, and the notifications API verifies both the token and the required scope.
The most important point is that a Cognito scope is only a permission label in a token. The notifications API must still validate the token correctly and explicitly require notifications/publish.
In this repository that enforcement is PUBLISHER_SCOPES=notifications/publish on the Worker (see Cognito setup).
# Terminology correction: it is not an “app pool”
There are two similarly named Cognito concepts:
- User pool: The user directory and identity provider. It contains users and pool-wide authentication settings.
- App client: A registration inside a user pool representing one application or machine that uses Cognito.
The requested change is a new app client inside the existing user pool, not a new “app pool” and not a new user pool.
A single user pool can contain several app clients, for example:
Production Cognito user pool
│
├── auth-service-production-user-pool-client
│ └── Used by people signing in
│
├── notifications-publisher-production
│ └── Used by a backend service publishing notifications
│
└── another-service-production
└── Potential future machine or application integration
Each app client has its own identity and configuration. Adding one does not automatically modify the others.
# The system we already have
# The production user pool
The Cognito user pool is the production identity directory. It is responsible for things such as:
- User accounts
- Password authentication
- User groups
- MFA
- Account recovery
- User-related Lambda triggers
- Issuing tokens after successful authentication
From the application’s perspective, Cognito acts as an identity provider.
# The existing user-login app client
auth-service-production-user-pool-client appears to be a public client intended for user authentication.
A public client is appropriate for a browser single-page application (SPA), mobile application, desktop client, or other application where users can inspect the deployed code and network traffic. It normally has:
- A client ID
- No client secret
- User authentication flows such as Secure Remote Password (SRP) or authorization code with Proof Key for Code Exchange (PKCE)
- Tokens representing a signed-in user
The absence of a client secret is intentional. A secret embedded in a browser bundle, Unity client, Electron app, or mobile app would not remain secret.
This existing client answers the question:
“Which person has signed in, and what is that user allowed to do?”
It does not provide a secure identity for a background service that has no human user.
# What is changing
The notifications publisher needs to authenticate when no person is signing in.
Examples include:
- A backend publishing a notification after completing a job
- A scheduled process generating notifications
- A webhook handler creating notifications
- A Cloudflare Worker publishing an event
- A deployment or maintenance script publishing a test notification
- A service notifying another service about a completed operation
These are machine-to-machine, or M2M, operations.
The new setup answers a different question:
“Which trusted application is making this request, and has that application been granted permission to publish notifications?”
That requires a dedicated machine identity.
# Human login and machine login are different jobs
| Concern | Existing user-login client | New notifications M2M client |
|---|---|---|
| Represents | A human-facing application | A trusted backend process |
| Authentication proof | User credentials, SRP, or browser authorization | Client ID and client secret |
| Client type | Public | Confidential |
| Secret | None | Required |
| OAuth grant | User-oriented flow | Client credentials |
| User interaction | Required | None |
| Token represents | A signed-in user/session | An application |
| ID token | Usually available in user login | Not issued |
| Refresh token | Often available in user login | Not issued |
| Access token | Yes | Yes |
| Permissions | User claims/groups and allowed scopes | Custom scopes only |
| Safe location | Browser/client application | Backend or secret store only |
Trying to make one app client perform both roles weakens separation and increases the chance of accidentally exposing a secret or broadening permissions.
# What is a confidential app client?
A confidential app client is an application registration that has a secret.
The word “confidential” does not mean that Cognito hides the existence of the application. It means the application runs somewhere capable of keeping credentials confidential, such as:
- A server
- A Cloudflare Worker secret
- AWS Lambda with Secrets Manager
- A protected CI/CD environment
- A local developer machine using a secure environment variable for controlled testing
It must not be used directly from:
- A browser SPA
- Shipped JavaScript
- A Unity or Electron client distributed to users
- A mobile application
- A public repository
- A Markdown documentation file
- A checked-in
.envfile
The client ID identifies the application. The client secret proves that the caller is permitted to act as that application.
# What is the client credentials grant?
The client credentials grant is an OAuth 2.0 flow for non-human applications.
The process is:
- The publisher sends its client ID and client secret to Cognito’s token endpoint.
- It requests a specific permission, such as
notifications/publish. - Cognito verifies that:
- The app client exists.
- The secret is correct.
- The app client is permitted to use the client credentials flow.
- The requested scope is assigned to that app client.
- Cognito returns a short-lived access token.
- The publisher sends that access token to the notifications API.
- The notifications API validates the token and checks that it contains
notifications/publish. - The API publishes the notification only when all checks pass.
sequenceDiagram
participant Publisher as Backend publisher
participant Cognito as Amazon Cognito
participant API as Notifications API
Publisher->>Cognito: Client ID + client secret<br/>grant_type=client_credentials<br/>scope=notifications/publish
Cognito->>Cognito: Validate client, secret and allowed scope
Cognito-->>Publisher: Short-lived access token
Publisher->>API: POST notification<br/>Authorization: Bearer access-token
API->>API: Verify signature, issuer, expiry,<br/>token_use, client_id and scope
API-->>Publisher: Notification accepted or rejectedThere is no user login page and no user account involved.
# What Cognito returns
A successful client credentials request returns an access token.
It does not return:
- An ID token, because there is no human identity to describe
- A refresh token, because the machine can use its client credentials to request another access token
The publisher should cache and reuse the access token until shortly before it expires rather than request a new token for every notification.
# What is a resource server?
“Resource server” is one of the most confusing terms in Cognito.
Creating a Cognito resource server does not create:
- An EC2 server
- An API Gateway
- A Lambda function
- A Worker
- A database
- A notification service
- Any new running infrastructure
A Cognito resource server is a registration describing an API and the permission names that Cognito is allowed to place in access tokens.
In this case:
Resource server friendly name: Notifications API
Resource server identifier: notifications
Custom scope: publish
Full scope in the token: notifications/publish
The actual notifications API is the real protected resource. The Cognito “resource server” entry is the OAuth permission catalogue for that API.
A useful mental model is:
Resource server = permission namespace
Scope = permission within that namespace
For example:
notifications/publish
notifications/read
notifications/admin
Only notifications/publish should be created if publishing is the only machine permission currently required. Additional speculative scopes should not be added.
# What is a scope?
A scope is a named permission carried inside an OAuth access token.
For example:
notifications/publish
This means:
“Cognito issued this token to a client that is allowed to request the notifications publishing permission.”
A scope does not itself execute anything and does not automatically protect an API. It is evidence that the API can inspect when deciding whether to allow a request.
The notifications API must enforce the scope. It must reject a token that is:
- Missing
- Expired
- Signed by an untrusted issuer
- Intended for a different Cognito user pool
- An ID token instead of an access token
- Issued to an unexpected app client, where client allowlisting is required
- Missing
notifications/publish
A common implementation error is to verify only that a JSON Web Token (JWT) has a valid signature. That proves Cognito issued it, but does not prove that the caller has the required publishing permission.
# Why a resource server is required for client credentials
Cognito’s client credentials flow can issue only custom scopes for M2M access.
Standard user-oriented scopes such as these are not appropriate:
openid
profile
email
phone
They describe access associated with a human identity and user profile.
To issue a machine access token with a permission such as notifications/publish, Cognito therefore needs:
- A resource server identifier:
notifications - A custom scope within it:
publish - An app client allowed to request that custom scope
Without the resource server and custom scope, Cognito has no application-specific permission to put in the M2M token.
# Why we have not needed this before
There are several common reasons a system can operate for years without a Cognito M2M client.
# Previous operations happened in a user session
If a notification was created only when a signed-in person performed an action, the application could pass that user’s access token to the API.
That token represented the user, so no separate machine identity was necessary.
This does not work well when publishing happens:
- After the user request has finished
- In a queue consumer
- In a scheduled job
- In a webhook
- In an independent service
- Without any user being signed in
# V1 may have used an anonymous shared API key
A previous notifications implementation may have accepted a static API key duplicated across projects, with little or no named identity.
That V1 pattern is simple, but it lacks many properties of OAuth access tokens:
- Short expiration
- Standard issuer validation
- Explicit scopes
- Clear client identity
- Easy separation between different publishers
- Controlled revocation per app client
- Standard JWT validation libraries
V2 still supports named publisher API keys on the Worker (PUBLISHER_API_KEYS — see api-keys.md): one secret per producer name, publish-only, source = name. That is a deliberate easy alternative to Cognito M2M — not the same as V1’s anonymous shared key.
A Cognito client secret is still a long-lived credential, but it is not sent to the notifications API on every publish request. It is exchanged with Cognito for a short-lived token.
# Publishing may previously have happened inside one trusted service
If the notification code ran inside the same process as the API, no service-to-service boundary existed. Splitting notifications into a separate package, service, Worker, or API creates an authentication boundary that did not previously exist.
# The system may have relied on infrastructure-level trust
Some architectures use AWS Identity and Access Management (IAM) and Signature Version 4 (SigV4) for service authentication. That can work well for AWS-to-AWS communication but can be more difficult when callers include Cloudflare Workers, external services, local scripts, or non-AWS environments.
The dedicated Cognito M2M client provides a platform-neutral OAuth approach.
# Why the need appears now
The likely architectural change is not that Cognito has suddenly started requiring something that was always missing.
The likely change is:
Notifications V2 now permits an application or backend process to publish independently of a signed-in user, so it needs its own authenticated machine identity.
That is a new security boundary, and the M2M client is the identity for that boundary.
# Why the existing login app client must not be reused
The existing login app client is the wrong client for M2M publishing for several reasons.
# It has no secret
Client credentials authentication requires the caller to prove possession of a client secret. A public client intentionally has no secret.
# Adding a secret would be inappropriate for the SPA
Browser code cannot keep a secret. Adding a secret to a client that is used by client-side code would either break the existing integration or encourage insecure distribution of the secret.
# The flows have different purposes
User login and client credentials are different OAuth grants. AWS requires a client credentials app client to use client_credentials as its OAuth flow rather than combining it with authorization-code or implicit grants.
# Separation limits the blast radius
A dedicated client can be:
- Granted only
notifications/publish - Revoked without affecting user login
- Rotated independently
- Audited as a specific publisher
- Given a short token lifetime
- Stored only in the publishing environment
# It avoids confusing users and machines
User tokens and machine tokens should not be treated as interchangeable. Separate clients make this distinction explicit in configuration and code.
# Production safety assessment
# What creating the resource server does not change
Creating a resource server and the publish scope does not, by itself:
- Modify any user
- Modify any password
- Change group membership
- Change MFA
- Change account recovery
- Change existing user tokens
- Revoke existing sessions
- Change an existing app client
- Grant the new scope to every app client
- Cause an application to begin accepting the new scope automatically
- Deploy or modify the notifications API
It registers a new custom permission in the user pool.
# What creating the app client does not change
Creating a new app client does not, by itself:
- Edit
auth-service-production-user-pool-client - Change how the SPA signs users in
- Invalidate existing refresh tokens
- Log users out
- Change callback URLs on another client
- Change existing client IDs or secrets
- Change the user pool’s password or MFA policy
- Move users to a different pool
The new client is independent.
# The realistic risks
The risk is not that Cognito will spontaneously alter the existing user directory. The realistic risks are configuration and implementation mistakes.
# 1. Editing the existing client instead of creating a new one
Changing the production user-login client’s OAuth flows, callback URLs, secret configuration, identity providers, or token settings could affect login.
Control: Create a new client. Do not edit the existing one.
# 2. Changing pool-wide settings during the same change
Changes to the pool domain, Lambda triggers, password policy, MFA, email configuration, or sign-in settings have a wider blast radius.
Control: Keep the change limited to the resource server and new app client. Treat any required domain change as a separate reviewed item.
# 3. Exposing the client secret
Anyone with the client ID and secret can request tokens with the scopes assigned to that client.
Control: Store the secret in a backend secret store. Never place it in frontend configuration, logs, screenshots, tickets, source control, or documentation.
# 4. Granting excessive scopes
A client with broad permissions has a larger blast radius if compromised.
Control: Grant only notifications/publish. Do not add read, admin, user, or unrelated scopes.
# 5. Failing to enforce the scope in the API
A valid Cognito token without notifications/publish must not be enough.
Control: The API must verify the JWT and require the exact scope. In this Worker: set PUBLISHER_SCOPES=notifications/publish.
# 6. Accepting tokens from any app client when that is not intended
The scope is normally the primary authorization control. For additional isolation, the API can also allowlist the expected M2M client_id.
Control: Decide whether only one publisher client should be accepted. If so, validate both the scope and client_id. (This Worker today authorizes publish via scope/group allowlists; client_id is recorded as notification source.)
# 7. Requesting a new Cognito token for every notification
M2M token requests affect Cognito request quotas and AWS cost.
Control: Cache each token and reuse it until shortly before expiry.
# 8. Infrastructure-as-code drift
Creating production configuration manually can leave Terraform, CloudFormation, CDK, Pulumi, or another deployment definition unaware of it.
Control: Prefer defining the resource server and app client in the project’s normal infrastructure-as-code system. If an emergency console change is made, import or reproduce it in code promptly.
# 9. Accidental update calls resetting omitted values
AWS warns that some UpdateUserPool and UpdateUserPoolClient API operations can reset omitted properties to defaults.
Control: Prefer create operations for the new resources. Avoid updating the existing client. When updating through an API or CLI, preserve the complete current configuration.
# 10. Pre-token-generation Lambda behaviour
If the user pool has a pre-token-generation Lambda trigger, inspect it before enabling M2M. Cognito invokes that trigger for client credentials only when the pool is configured for the V3 event format.
Control: Confirm whether a trigger exists, which event version it uses, and whether its logic safely handles machine tokens.
# User-pool domain requirement
The client credentials flow uses Cognito’s OAuth token endpoint:
https://<cognito-domain>/oauth2/token
The user pool therefore requires a Cognito domain.
The pool may already have one if it uses:
- Cognito managed login or the hosted UI
- Authorization-code OAuth
- Federation through the Cognito authorization server
- Existing OAuth token endpoint integrations
If the pool has no domain, adding one is required for M2M. This is still generally additive, but it is a separate pool-level configuration item and deserves explicit review.
Adding a domain does not normally change applications that authenticate directly with Cognito SDK/API operations such as SRP. However, a custom domain introduces DNS, certificate, and naming considerations. An AWS-managed Cognito prefix domain is simpler if no custom public login branding is required.
Do not alter or replace an existing production domain merely to add the M2M client.
# Recommended production configuration
# Resource server
Friendly name: Notifications API
Identifier: notifications
Scope name: publish
Description: Publish notifications
Full scope: notifications/publish
Use a stable identifier. Changing it later changes the full scope string and therefore requires changes in publishers and API authorization rules.
# M2M app client
Name: notifications-publisher-production
Application type: Machine-to-machine application
Client type: Confidential
Client secret: Generated
OAuth flow: Client credentials only
Allowed custom scope: notifications/publish
User login flows: Disabled
Callback URLs: None
Sign-out URLs: None
Identity providers: None required
User attributes: No read/write permissions where configurable
Access-token lifetime: 5–15 minutes, subject to operational requirements
Cognito supports access-token lifetimes from five minutes to one day. A short lifetime reduces the useful lifetime of a stolen access token. The publisher should cache the token, so a short lifetime does not imply requesting one for every notification.
Ten or fifteen minutes is usually a practical starting point. Five minutes is suitable when the publisher can reliably cache and renew tokens and clock synchronisation is dependable.
# Secret storage
Use the secret mechanism appropriate to the publishing runtime:
- AWS Secrets Manager
- AWS Systems Manager Parameter Store with encryption
- Cloudflare Worker secret
- GitHub Actions encrypted secret
- Another approved production secret vault
The secret should be available only to the component that requests tokens.
# Required API validation
The notifications API must validate access tokens before accepting a publish request.
At minimum it must check:
-
Signature
The JWT signature matches a currently valid Cognito signing key from the correct user pool. -
Issuer (
iss)
The token was issued by the expected production Cognito user pool. -
Expiry (
exp)
The token has not expired. -
Not-before and issued-at values where applicable
The token is currently valid, allowing only a small clock-skew tolerance. -
Token use (
token_use)
The value isaccess, notid. -
Scope (
scope)
The token contains the exact scopenotifications/publish. -
Client ID (
client_id)
The token was issued to an expected app client where client allowlisting is part of the design. -
Algorithm and key ID
The API accepts the expected Cognito signing algorithm and selects a key from the user pool’s trusted JSON Web Key Set (JWKS) document. It must not accept an arbitrary algorithm supplied by the token.
The API should deny by default. A malformed token or failed validation must return an authentication or authorization failure rather than falling back to anonymous publishing.
# Authentication versus authorization
These are separate checks:
Authentication:
“Is this a genuine, valid token issued by our production Cognito pool?”
Authorization:
“Does this valid token contain notifications/publish, and is this client allowed here?”
A valid token can still be unauthorized.
# Example token request
The following is an illustrative local or backend test. Real credentials must be supplied through protected environment variables.
curl --request POST \
--user "${COGNITO_CLIENT_ID}:${COGNITO_CLIENT_SECRET}" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=notifications/publish" \
"https://<production-cognito-domain>/oauth2/token"
A successful response resembles:
{
"access_token": "<signed-jwt>",
"expires_in": 600,
"token_type": "Bearer"
}
It will not contain an ID token or refresh token.
The access token is then sent to the notifications API (POST /v2/notifications):
curl --request POST \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--data '{"userGuid":"<recipient-cognito-sub>","title":"Test notification","body":"M2M publishing test","dedupeKey":"m2m-test:1"}' \
"https://notifications.totumcloud.com/v2/notifications"
Never paste the real client secret into a shared terminal transcript, support conversation, documentation page, or source file.
PowerShell / Windows examples: local-development.md §2 (API keys), §3 (M2M), and cognito-setup.md.
# Token caching
The publisher should not request a token for every publish call.
A sensible strategy is:
- Request a token when no cached token exists.
- Store the token and its expiry in memory.
- Reuse it for publish operations.
- Renew it shortly before expiry, for example 30–60 seconds early.
- If the API rejects it because it has expired, obtain one new token and retry once.
- Do not enter an unlimited retry loop.
This reduces:
- Cognito token endpoint traffic
- AWS cost
- Risk of reaching request-rate quotas
- Latency when publishing notifications
A distributed cache is not necessary unless several independent instances need to share the same token. Each instance can usually maintain its own in-memory cache.
# Secret rotation and revocation
# If the secret is suspected to be compromised
- Add or generate a replacement secret for the app client where supported.
- Deploy the replacement secret to the publisher.
- Confirm the publisher can obtain and use new tokens.
- Delete the old secret.
- Review logs for unexpected token requests or publishing activity.
AWS now supports up to two active secrets per app client, allowing a zero-downtime rotation window.
# If the publisher must be blocked immediately
- Configure the notifications API to reject the affected
client_id. - Delete the dedicated app client, or remove its allowed publishing scope, to prevent new tokens being issued.
- Remove the compromised secret from the publisher and secret store.
- Investigate recent token requests and publishing activity.
This affects the M2M publisher, not the existing user-login client.
Deleting the app client or removing its scope prevents future token issuance, but an access token that was already issued can remain cryptographically valid until it expires. Immediate API-side rejection of the affected client_id closes that window. This is another reason to use a short access-token lifetime.
# Rollback
The rollback is isolated:
- Stop the publisher from requesting or using M2M tokens.
- Delete
notifications-publisher-production, or remove its allowed publishing scope if the client must temporarily remain. - Remove the
notifications/publishscope from the resource server if no other app client uses it. - Delete the
notificationsresource server if it is completely unused. - Remove the secret from the production secret store.
- Revert the notifications API configuration that accepts the new client/scope.
The existing user-login app client does not need to be changed during rollback.
Do not delete the resource server while another app client or API still depends on its scopes.
# Pre-change production checklist
Record the current state before changing anything.
- [ ] Confirm the AWS account and Region are production.
- [ ] Record the production user pool ID.
- [ ] Record the existing Cognito domain, if present.
- [ ] Export or screenshot the existing app-client list.
- [ ] Export the configuration of
auth-service-production-user-pool-client. - [ ] Confirm that the existing app client will not be edited.
- [ ] List existing resource servers and custom scopes.
- [ ] Check app-client and resource-server quotas.
- [ ] Check whether a pre-token-generation Lambda trigger exists.
- [ ] Identify the trigger event version if one exists.
- [ ] Confirm whether Cognito is managed by Terraform, CloudFormation, CDK, Pulumi, or another deployment system.
- [ ] Confirm where the new client secret will be stored.
- [ ] Confirm the notifications API validates JWT signatures and scopes.
- [ ] Confirm the intended token lifetime.
- [ ] Confirm token caching is implemented.
- [ ] Confirm logging will not capture the client secret or full bearer token.
- [ ] Confirm there is a rollback owner and procedure.
# Change procedure
# Phase 1: Register the permission
- Open the existing production Cognito user pool.
- Navigate to the resource-server configuration.
- Create the
notificationsresource server. - Add only the
publishscope. - Save the configuration.
- Verify that no existing app client was changed.
At this point, nothing can use the scope unless an app client is explicitly granted it.
# Phase 2: Create the machine client
- Create a new app client.
- Choose the machine-to-machine application type.
- Name it
notifications-publisher-production. - Generate a client secret.
- Enable client credentials as the only OAuth flow.
- Assign only
notifications/publish. - Disable user authentication flows.
- Do not configure callback or sign-out URLs.
- Do not configure social or enterprise identity providers.
- Set the required access-token lifetime.
- Save the client ID and secret directly into the approved secret store.
- Do not copy the secret into project documentation.
# Phase 3: Test token issuance
- Request a token from a controlled backend or local administrative environment.
- Confirm the request succeeds only with the correct secret.
- Confirm requesting an unassigned scope fails.
- Decode the access token for inspection without treating decoding as validation.
- Confirm:
token_useisaccessclient_idis the new clientscopeincludesnotifications/publishissidentifies the production user poolexpmatches the intended lifetime
# Phase 4: Test API enforcement
Test all of these cases:
| Test | Expected result |
|---|---|
No Authorization header |
Rejected |
| Random bearer token | Rejected |
| Expired Cognito token | Rejected |
| Cognito ID token | Rejected |
| Access token from wrong user pool | Rejected |
Valid access token without notifications/publish |
Rejected |
| Valid access token from disallowed client, if allowlisting | Rejected |
| Correct M2M access token and valid request | Accepted |
| Correct token but invalid notification payload | Rejected by input validation |
# Phase 5: Monitor
After deployment, monitor:
- Token endpoint failures
- Notification publish failures
- Unexpected client IDs
- Missing-scope failures
- Unusual publishing volume
- Cognito throttling
- Token-request volume and cost
- Logs accidentally containing bearer tokens or secrets
# Recommended separation between user and machine access
The notifications API may eventually accept both:
- User access tokens for inbox or user-facing operations
- M2M access tokens for publishing
These must have separate authorization policies.
For example:
GET /v2/notifications
Required identity: signed-in user
Required checks: valid user access token (inbox is user-scoped)
POST /v2/notifications
Required identity: trusted machine publisher
Required checks: valid access token + notifications/publish
The existence of a valid user token must not automatically grant publishing permission unless that behaviour is explicitly designed and separately authorized.
Likewise, the M2M token should not automatically grant access to user inbox data merely because it comes from the same user pool.
# Alternatives considered
# Continue using an anonymous shared API key (V1-style)
Advantages
- Simple to implement
- No token exchange
Disadvantages
- Long-lived credential is sent to the API on every request
- No standard expiry
- Usually no scope model
- Harder to identify individual publishers
- Rotation can be disruptive
- Often leads to one broadly shared secret with no name
Not recommended. If you want “just a key,” use V2’s named PUBLISHER_API_KEYS instead (api-keys.md): Worker-gated, publish-only, stable source per producer. Prefer Cognito M2M when you need short-lived tokens and Cognito-side rotation.
# Use a signed-in user’s token
Advantages
- No new machine credential
- Works when every action genuinely occurs for a current user
Disadvantages
- Background operations may have no user
- Encourages impersonating or selecting a service user
- Ties service availability to user sessions and refresh tokens
- Blurs user actions and system actions in audit logs
Not appropriate for autonomous publishers.
# Use AWS IAM and SigV4
Advantages
- Strong AWS-native service identity
- Excellent for AWS-to-AWS calls
- Integrates with IAM policies and roles
Disadvantages
- More complex for non-AWS publishers
- Cloudflare, external services, and local tools need AWS credentials and request signing
- The receiving API must support the IAM model
A valid alternative when all publishers and APIs are AWS-native.
# Use a dedicated Cognito M2M client
Advantages
- Standards-based OAuth 2.0
- Short-lived access tokens
- Explicit permission scopes
- Independent client identity
- Isolated revocation and rotation
- Works across AWS and non-AWS runtimes
- Familiar bearer-token API integration
Disadvantages
- Requires a Cognito domain and token endpoint
- Adds M2M Cognito cost and quotas
- Requires secure client-secret storage
- Requires correct JWT and scope validation
This is a good fit when multiple backend environments need a common OAuth mechanism.
# Frequently asked questions
# Does the new app client get a copy of all users?
No. App clients are configurations within a user pool. The new M2M client does not create a second directory or copy any user records.
# Can it change user passwords or groups?
Not merely because it exists or because it has notifications/publish.
The custom scope is intended for the external notifications API. The client should have no user login flows or user-attribute permissions. Administrative Cognito operations would additionally require AWS IAM credentials and permissions; the OAuth client secret is not an AWS administrator credential.
# Will it log existing users out?
Creating a separate app client does not revoke tokens or sessions belonging to another app client.
# Will it change the SPA login?
Not if the existing app client and pool-wide login settings are left unchanged.
# Why can the existing client ID not be used without a secret?
A client ID identifies a client but is not proof of identity. Client IDs are commonly visible and must not be treated as passwords.
# Why is the secret not sent directly to the notifications API?
Exchanging it for a short-lived token reduces exposure of the long-lived credential, enables scopes, and allows the API to use standard JWT verification.
# Is notifications/publish an AWS permission?
It is an application-defined OAuth permission. Cognito places it in an access token. The notifications API decides what it means and enforces it.
It is not an IAM policy and does not directly grant access to AWS resources.
# Does creating a resource server start billing for another server?
No server is deployed. However, client-credentials token requests are chargeable under Cognito M2M pricing, so token caching and current pricing review are required.
# Can any app client request the new scope?
No. The custom scope must be explicitly assigned to an app client. A request for a scope that the client is not allowed to use should fail.
# Should development and production share a client?
No. Use separate clients and secrets for each environment. Prefer separate user pools where that is already the project’s environment model.
Suggested names:
notifications-publisher-development
notifications-publisher-staging
notifications-publisher-production
# Can several production publishers share one client?
They can, but separate clients provide better isolation, auditing, revocation, and secret rotation.
Start with one client only if all publishers are genuinely one trust boundary. Create another client when a distinct service, organisation, customer, or operational owner requires independent control.
# Is the client secret recoverable later?
Secret handling capabilities have evolved. AWS supports app-client secret management and rotation, including up to two active secrets. Regardless of console visibility, the operational rule should be to capture a newly created secret directly into the approved secret store and avoid relying on later retrieval.
# What happens if the secret leaks?
An attacker could request access tokens carrying the scopes assigned to that app client until the secret is revoked or replaced. Limit the damage through one narrow scope, short access-token lifetime, secret rotation, client-specific logging, and API-side client allowlisting where appropriate.
# Approval recommendation
The change is appropriate to approve when all of the following are true:
- A backend or non-interactive process genuinely needs to publish notifications.
- The existing user-login client will remain unchanged.
- The new client uses client credentials only.
- The new client is granted only
notifications/publish. - The secret is stored only in an approved backend secret store.
- The notifications API verifies the JWT signature, issuer, expiry, token type, scope, and any required client allowlist.
- Tokens are cached rather than requested for every publish.
- Any existing Cognito domain and pre-token-generation trigger have been reviewed.
- The configuration is represented in infrastructure as code or formally recorded.
- A rollback procedure and owner are identified.
Under those controls, the change has a small and well-contained blast radius. A failure in the new M2M configuration should affect notification publishing, not end-user authentication.
# Suggested pull-request summary
Add a dedicated Cognito machine-to-machine identity for Notifications V2 publishing. This introduces a
notificationsresource server with the custom scopenotifications/publish, plus a confidential production app client restricted to the client credentials grant and that scope. The existing end-user login app client is unchanged. The client secret is stored only in the production secret store, and the notifications API validates the Cognito access token and required publishing scope.
# Official AWS references
- Amazon Cognito user pools
- Application-specific settings with app clients
- Scopes, M2M, and resource servers
- OAuth 2.0 grants
- Cognito token endpoint
- Understanding the access token
- Verifying Cognito JSON Web Tokens
- Security best practices for Cognito user pools
- Managing user-pool token expiration and caching
- Monitoring and managing Cognito costs
- Updating user-pool and app-client configuration
- Pre-token-generation Lambda trigger