深度剖析 AWS Cognito 最常见的认证失败原因——ExplicitAuthFlows 和 SECRET_HASH 配置错误。提醒 AI 生成代码的隐患。
你的登录页面在本地运行正常。可当你把它指向 staging 环境的 user pool 后,每次登录都会报错,甚至还没走到密码校验那一步。
你的 AI 助手编写的代码调用了 InitiateAuth,并将 AuthFlow 设置为 'USER_PASSWORD_AUTH'。这个猜测很合理:大多数 Cognito 教程使用的都是这种 flow。但 staging 环境中的 app client 是由一个 Terraform module 创建的,其中设置了 explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"],而且这个 client 还配置了 secret。因此,这次调用存在两个问题:一是该 client 没有启用这种 flow,二是请求中缺少 SECRET_HASH。
这些信息在你的源代码文件中完全看不到。助手读遍了 repo,没有找到答案,于是生成了互联网上统计意义上最常见的 Cognito 代码片段。
Cognito 的失败场景几乎都取决于每个 app client 的设置,而不是 user pool 的设置。其中有五类设置会直接导致生成的代码失效:
允许的 auth flow。ExplicitAuthFlows 是一个白名单。如果其中没有 ALLOW_USER_PASSWORD_AUTH,那么无论用户名和密码是否正确,USER_PASSWORD_AUTH 都会被拒绝。dev 环境中的 client A 可能允许这种 flow,而 prod 环境中的 client B 却不允许,于是同一份 handler 代码在一个环境中可以运行,在另一个环境中却会失败。
Client secret。设置了 GenerateSecret: true 的 client 要求每次 auth 调用都携带 SECRET_HASH。它是一个 Base64 编码的 HMAC-SHA256 值:以 client secret 作为密钥,对 username + clientId 进行计算。如果助手不知道存在 secret,就永远不会生成这个字段,调用也会在 secret 验证阶段失败,而不是在凭据验证阶段失败。
MFA。当 MFA 设置为 ON 时,InitiateAuth 通常会返回 ChallengeName 和一个 session,而不是 token。只针对理想路径编写的代码会尝试读取 response.AuthenticationResult.IdToken,得到 undefined,然后在距离真正原因足足隔了三个函数的地方抛出异常。
Token 有效期单位。单看 AccessTokenValidity: 60 没有任何意义。如果 TokenValidityUnits.AccessToken = 'minutes',它表示一小时;如果单位是 'days',它就表示两个月。基于错误单位构建的 refresh 逻辑,要么会疯狂请求 token endpoint,要么会放任 session 失效。
OAuth 设置。如果你使用的是 hosted UI,而不是 direct auth,那么 client 还会包含自己的 AllowedOAuthFlows、AllowedOAuthScopes 和 CallbackURLs。如果构造 redirect 时使用的 URL 不在 callback 列表中,或者请求了该 client 不允许的 scope,Cognito 就会直接在 authorize endpoint 拒绝请求,你的应用根本看不到它。负责生成 redirect 的助手不可能知道 staging client 只注册了 https://staging.example.com/callback,而你的本地 dev URL 从未被添加进去。
以上每项配置都存在于 AWS 中,而不是你的 repository 里。即使这个 pool 是用 Terraform 定义的,助手也必须找到正确的 module、解析其中的变量,还得知道正在运行的服务实际使用的是哪个 client。现实中它通常做不到这些,所以只能猜。
Infrawise 会提取这些信息,并通过 MCP 交给你的助手。src/adapters/aws/services.ts 中的 Cognito extractor 会依次执行四类只读调用:先通过 ListUserPools 获取所有 pool,再对每个 pool 调用 DescribeUserPool,然后调用 ListUserPoolClients,最后对每个 client 调用 DescribeUserPoolClient。两个列表接口都使用 NextToken 处理分页,因此,即使一个 pool 中有 80 个 app client,也不会在第一页之后被悄悄截断。
对于每个 client,它只保留那些会改变调用代码写法的字段:
clientName, clientId
authFlows <- ExplicitAuthFlows
oauthFlows <- AllowedOAuthFlows
oauthScopes <- AllowedOAuthScopes
callbackUrls <- CallbackURLs
generatesSecret <- !!ClientSecret
accessTokenValidity, idTokenValidity, refreshTokenValidity
tokenValidityUnits <- { accessToken, idToken, refreshToken }
请注意 generatesSecret。DescribeUserPoolClient 确实会返回 secret 的值,而 Infrawise 会在提取时立即把它转换成 boolean。secret 的具体值绝不会存入 graph,不会被缓存,也不会由任何工具返回。你的助手可以知道 secret 存在,也知道必须提供 SECRET_HASH,但永远看不到 secret 本身。用户数据也是如此:Infrawise 从不调用任何用户相关的 API。它帮助你编写的是登录代码,而不是读取用户目录。
get_cognito_overview MCP 工具会返回完整的信息:
{
"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" }
}
]
}
]
}
这就是两种助手之间的区别:一种只能猜测使用 USER_PASSWORD_AUTH,另一种则能编写带有 SECRET_HASH 的 SRP 代码,因为白名单和 secret 标志就摆在它的上下文中。
在 src/server/index.ts 中注册的工具描述会告诉模型,什么时候应该调用这个工具,什么时候不该调用:在编写任何登录、注册或 token refresh 代码之前调用它;不要用它查询用户或 token。最后这句话远比看起来重要。工具描述是决定 Agent 选择哪个工具的唯一引导,如果一个工具听起来像用户目录,Agent 就会出于错误的目的调用它。
Cognito 默认处于关闭状态。infrawise start 会生成一个包含 cognito: { enabled: false } 的 infrawise.yaml,因为大多数 repo 都没有使用 Cognito,没必要为它们发起 API 调用。要处理 auth,只需切换一个配置项:
cognito:
enabled: true
IAM policy 只需要四项读取权限,不需要其他权限:
cognito-idp:ListUserPools
cognito-idp:DescribeUserPool
cognito-idp:ListUserPoolClients
cognito-idp:DescribeUserPoolClient
infrawise start --claude
这条命令会探测你的环境、运行分析、写入 .mcp.json,让编辑器在以后每次启动时都能重新连接,并打开 Claude Code,同时提供全部 21 个工具。从那以后,你只需运行 claude。结果会缓存 24 小时,而 get_infra_overview 会返回一个 freshness 对象,其中包含分析结果的时间以及 stale 标志,让助手能够判断自己看到的是不是昨天的快照。
当你提出“为 staging pool 编写一个登录 handler”时,auth flow 就不再靠猜了。助手会调用 get_cognito_overview,看到 ALLOW_USER_SRP_AUTH 和 generatesSecret: true,第一次就写出带 secret hash 的 SRP 实现。
这里涉及的 bug 类型很无聊,而这正是它如此浪费时间的原因。构建时不会发生任何崩溃,类型检查也没有问题,mock Cognito client 的测试同样可以通过。只有连接真实 user pool 时才会失败,抛出的异常信息说的是 flow 名称,而不是禁止该 flow 的 app client;真正的修复方式,则是打开控制台页面,读取其中的一个配置值。
Cognito 只是这种通用模式的一个实例。编写正确代码所需的信息,一部分位于 repo 中,另一部分位于 cloud account 中,而助手只能看到其中一半。Infrawise 以确定性的方式弥合了这道鸿沟:提取路径中没有 LLM,只有 SDK 调用、AST 解析和基于规则的 analyzer,它们会生成供 MCP 工具读取的 graph。
试试看:GitHub 或 npm。
Cognito 的失败取决于各个 app client,而不是 user pool。同一份代码连接同一 pool 中的某个 client 时可以成功,连接另一个 client 时却可能失败。
在构建 hosted UI redirect 之前,请检查 callbackUrls 和 oauthScopes。未注册的 URL 会在 authorize endpoint 被拒绝。
如果 generatesSecret 为 true,每次 auth 调用都需要提供 SECRET_HASH。不知道存在 secret 的助手永远不会生成它。
没有 TokenValidityUnits,AccessTokenValidity 就毫无意义。数值 60 可能表示一小时,也可能表示两个月,具体取决于单位。
在 infrawise.yaml 中设置 cognito: enabled: true(默认值为 false),并授予四项 cognito-idp 读取权限。
在编写登录、注册或 refresh 代码之前,调用 get_cognito_overview。它绝不会返回 client secret 的具体值或用户数据。
对于后续操作,你可以考虑屏蔽此人和/或举报滥用行为。