Your login screen works locally. You point it at the staging user pool and every sign-in comes back as an error before it ever reaches a password check.

The code your AI assistant wrote calls InitiateAuth with AuthFlow: 'USER_PASSWORD_AUTH'. Reasonable guess: it is the flow in most Cognito tutorials. But the app client in staging was created by a Terraform module that set explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"], and that client also has a secret. So the call fails twice over: the flow is not enabled for this client, and the request is missing SECRET_HASH.

None of that is visible in your source files. The assistant read your repo, found no answer, and produced the most statistically common Cognito snippet on the internet.

The app client is the config, and it is not in your repo

Cognito's failure modes are almost all per app client settings, not per user pool settings. Four of them break generated code immediately:

Allowed auth flows. ExplicitAuthFlows is a whitelist. If ALLOW_USER_PASSWORD_AUTH is not on it, USER_PASSWORD_AUTH is rejected regardless of whether the username and password are correct. Client A in dev may allow it while client B in prod does not, and the same handler code then works in one environment and not the other.

Client secret. A client with GenerateSecret: true requires every auth call to carry SECRET_HASH, a base64 HMAC-SHA256 of username + clientId keyed with the client secret. An assistant that does not know a secret exists will never emit that field, and the call fails on secret verification rather than on credentials.

MFA. With MFA set to ON, InitiateAuth typically returns a ChallengeName and a session instead of tokens. Code written against the happy path reads response.AuthenticationResult.IdToken, gets undefined, and throws somewhere three functions away from the actual cause.

Token validity units. AccessTokenValidity: 60 means nothing on its own. With TokenValidityUnits.AccessToken = 'minutes' it is one hour. With 'days' it is two months. Refresh logic built on the wrong unit either hammers the token endpoint or lets sessions die.

OAuth settings. If you are using the hosted UI instead of direct auth, the client carries its own AllowedOAuthFlows, AllowedOAuthScopes, and CallbackURLs. Build a redirect against a URL that is not in the callback list, or request a scope the client does not allow, and Cognito refuses the request at the authorize endpoint before your app sees anything. An assistant writing the redirect has no way to know the staging client only registered https://staging.example.com/callback while your local dev URL was never added.

Every one of those lives in AWS, not in your repository. Even when the pool is defined in Terraform, the assistant would have to find the right module, resolve the variables, and know which client the running service actually uses. In practice it does not, so it guesses.

Reading the real client config

Infrawise extracts this and hands it to your assistant over MCP. The Cognito extractor in src/adapters/aws/services.ts walks four read-only calls: ListUserPools for every pool, then per pool DescribeUserPool, then ListUserPoolClients, then DescribeUserPoolClient for each client. Both listings are paginated with NextToken, so a pool with 80 app clients does not get silently truncated at the first page.

For each client it keeps exactly the fields that change how you write the call:

clientName, clientId
authFlows          <- ExplicitAuthFlows
oauthFlows         <- AllowedOAuthFlows
oauthScopes        <- AllowedOAuthScopes
callbackUrls       <- CallbackURLs
generatesSecret    <- !!ClientSecret
accessTokenValidity, idTokenValidity, refreshTokenValidity
tokenValidityUnits <- { accessToken, idToken, refreshToken }

Enter fullscreen mode Exit fullscreen mode

Note generatesSecret. DescribeUserPoolClient does return the secret value, and infrawise converts it to a boolean at the point of extraction. The value is never stored in the graph, never cached, and never returned by any tool. Your assistant learns that a secret exists and that SECRET_HASH is mandatory, without ever seeing the secret. Same for users: infrawise never calls any user API. Sign-in code is what it helps you write, not a directory it reads.

The get_cognito_overview MCP tool returns the whole thing:

{
  "total": 1,
  "note": "Client secret values and user data are never included.",
  "userPools": [
    {
      "name": "app-users-staging",
      "id": "ap-south-1_XXXXXXXXX",
      "mfaConfiguration": "OPTIONAL",
      "clients": [
        {
          "clientName": "web-spa",
          "clientId": "4h1...",
          "authFlows": ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"],
          "oauthFlows": ["code"],
          "oauthScopes": ["openid", "email"],
          "callbackUrls": ["https://staging.example.com/callback"],
          "generatesSecret": true,
          "accessTokenValidity": 60,
          "tokenValidityUnits": { "accessToken": "minutes" }
        }
      ]
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

That is the difference between an assistant guessing USER_PASSWORD_AUTH and an assistant writing SRP with a SECRET_HASH, because the whitelist and the secret flag are sitting right there in its context.

The tool description registered in src/server/index.ts tells the model when to reach for it and when not to: call it before writing any sign-in, sign-up, or token-refresh code; do not call it to look up users or tokens. That last clause matters more than it looks. Tool descriptions are the only thing steering which tool an agent picks, and a tool that sounds like a user directory will get called for the wrong reasons.

Turning it on

Cognito is off by default. infrawise start writes an infrawise.yaml with cognito: { enabled: false }, because most repos have no Cognito and there is no reason to make an API call for them. Auth work means flipping one key:

cognito:
  enabled: true

Enter fullscreen mode Exit fullscreen mode

The IAM policy is four read actions, and nothing else:

cognito-idp:ListUserPools
cognito-idp:DescribeUserPool
cognito-idp:ListUserPoolClients
cognito-idp:DescribeUserPoolClient

Enter fullscreen mode Exit fullscreen mode

Then:

infrawise start --claude

Enter fullscreen mode Exit fullscreen mode

That probes your environment, runs the analysis, writes .mcp.json so your editor reconnects on every future launch, and opens Claude Code with all 21 tools available. From then on you just run claude. Results are cached for 24 hours, and get_infra_overview reports a freshness object with the analysis age and a stale flag so the assistant can tell when it is looking at yesterday's picture.

Ask "write me a sign-in handler for the staging pool" and the flow is no longer a guess. The assistant calls get_cognito_overview, sees ALLOW_USER_SRP_AUTH and generatesSecret: true, and writes SRP with a secret hash the first time.

What this actually buys you

The bug class here is boring, which is why it eats so much time. Nothing crashes at build time. Types are fine. Tests that mock the Cognito client pass. The failure only shows up against a real user pool, as an exception whose message is about a flow name rather than about the app client that disallowed it, and the fix is a config value you have to go read in a console tab.

Cognito is one instance of the general pattern. The information needed to write correct code is split between your repo and your cloud account, and the assistant only has half. Infrawise closes that gap deterministically: no LLM in the extraction path, just SDK calls, AST parsing, and rule-based analyzers producing a graph that MCP tools read from.

Try it: GitHub or npm.

Key takeaways

  • Cognito failures are per app client, not per user pool. The same code succeeds against one client and fails against another in the same pool.
  • Check callbackUrls and oauthScopes before building a hosted UI redirect. A URL that is not registered is rejected at the authorize endpoint.
  • If generatesSecret is true, every auth call needs SECRET_HASH. An assistant that does not know a secret exists will never emit it.
  • AccessTokenValidity is meaningless without TokenValidityUnits. 60 is an hour or two months depending on the unit.
  • Set cognito: enabled: true in infrawise.yaml (it defaults to false) and grant the four cognito-idp read actions.
  • Call get_cognito_overview before writing sign-in, sign-up, or refresh code. It never returns client secret values or user data.