# Your Cognito Login Code Fails Because the App Client Never Allowed That Auth Flow

> Source: <https://dev.to/siddharth_pandey_27/your-cognito-login-code-fails-because-the-app-client-never-allowed-that-auth-flow-1hmf>
> Published: 2026-08-02 20:39:22+00:00

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.

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.

[Infrawise](https://github.com/Sidd27/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 }
```

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" }
        }
      ]
    }
  ]
}
```

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.

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
```

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

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

Then:

```
infrawise start --claude
```

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.

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.

`callbackUrls`

and `oauthScopes`

before building a hosted UI redirect. A URL that is not registered is rejected at the authorize endpoint.`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.`cognito: enabled: true`

in `infrawise.yaml`

(it defaults to false) and grant the four `cognito-idp`

read actions.`get_cognito_overview`

before writing sign-in, sign-up, or refresh code. It never returns client secret values or user data.
