Skip to content

Runbook: Cognito Dev Client Abuse

Alarm: tradai-dev-client-auth-failures-<env> (CloudWatch, namespace AWS/Cognito, metric SignInThrottles)

Scope: Non-prod only. The dev client (tradai-dev-client-<env>) uses USER_PASSWORD_AUTH and is the natural target for credential stuffing / password spray. The prod stack does not create this client.

⚠️ Pulumi drift warning. The dev client is owned by the tradai-persistent Pulumi stack. Any AWS-console or aws cognito-idp mutation against the client itself (auth flows, token validity, the client's existence) will be reverted on the next pulumi up. Prefer the user-level mitigations below — they don't touch the client resource. If you genuinely need to disable the client, land a code change instead of mutating live infra.

When it fires

Cognito's adaptive throttling started rejecting InitiateAuth calls against the dev client for a sustained period (≥ 5 throttles per 5-minute window, two consecutive windows). That typically means:

  • Credential-stuffing attempt against known dev usernames
  • Password spray (one password across many usernames)
  • Misconfigured client retrying with stale credentials in a loop

Response (preferred — no Pulumi drift)

User-level actions don't touch the Pulumi-managed client and so do not drift:

  1. Identify the targeted accounts. CloudTrail → filter eventSource = cognito-idp.amazonaws.com, eventName = InitiateAuth, errorCode = NotAuthorizedException for the affected pool/client. Note source IPs and target usernames.
  2. Lock the targeted users (immediate):
    aws cognito-idp admin-disable-user \
        --user-pool-id <user_pool_id> \
        --username <email>
    
  3. Rotate credentials for any user that appears in the failed-auth list (permanent, no email loop):
    aws cognito-idp admin-set-user-password \
        --user-pool-id <user_pool_id> \
        --username <email> \
        --password "<new-strong-password>" \
        --permanent
    
  4. Block the source at WAF if it's an external IP. Edge stack owns WAF; add a temporary IP-set entry for 24h. File a follow-up to evaluate making the block permanent.
  5. Re-enable users after credential rotation:
    aws cognito-idp admin-enable-user \
        --user-pool-id <user_pool_id> \
        --username <email>
    

Response (last resort — kill the dev client)

If the abuse continues and user-level mitigations are not enough, the only safe way to remove the USER_PASSWORD_AUTH surface is via a code PR, not a live AWS-console change:

  1. Open a PR that wraps _create_dev_client() in infra/persistent/modules/cognito.py behind a feature flag, or removes the client outright for the affected env.
  2. Get expedited review + merge.
  3. Run just infra-up-persistent <env> to deploy.

Doing this via aws cognito-idp update-user-pool-client or the AWS console would silently revert on the next pulumi up from CI — re-opening the abuse window mid-incident.

Postman / curl auth flow (reference)

The dev client exists so Postman / curl can authenticate without SRP. The single canonical example lives here so the docstring, runbook, and PR description don't drift apart:

POST https://cognito-idp.<region>.amazonaws.com/
x-amz-target: AWSCognitoIdentityProviderService.InitiateAuth
Content-Type: application/x-amz-json-1.1

{
  "AuthFlow": "USER_PASSWORD_AUTH",
  "ClientId": "<cognito_dev_client_id from pulumi stack output>",
  "AuthParameters": {
    "USERNAME": "user@example.com",
    "PASSWORD": "your-password"
  }
}

Get the client ID and user pool ID

cd infra/persistent
pulumi stack output cognito_dev_client_id --stack <env>
pulumi stack output cognito_user_pool_id --stack <env>

Why this client exists

Frontend / API testing convenience — Postman and curl can authenticate with a plain InitiateAuth POST instead of running the SRP handshake. It is intentionally excluded from prod because USER_PASSWORD_AUTH puts the password into the request payload, raising credential-stuffing exposure. Prod callers must use the public PKCE client (tradai-api-client-prod).