Amazon Cognito 用户池与身份池:从登录接入到临时 AWS 凭据治理
移动端已经能在浏览器里完成登录,调用业务 API 却始终得到 401。开发者从 JWT 中看到了邮箱和用户名,便认定 Cognito 正常,最终发现 API 在另一个 Region 拼接 issuer,而且把 app client ID 当成所有 token 都相同的 audience 规则。用户池签发令牌只是第一步;资源端还要按 token 类型校验 issuer、签名、有效期、client/audience 和 scope,任何 Region、pool 或 app client 错配都应该被明确拒绝。
另一个项目为了让前端上传 S3,引入 identity pool 并把 authenticated role 放宽到整个 bucket。后来团队又把 identity pool 当成“客户组织”,试图用 identity ID 做业务租户主键。结果一个会变化的 AWS 临时凭据映射承担了永久业务身份,权限也从单用户前缀扩大到共享资源。Cognito user pool 负责用户目录和 OIDC token,identity pool 负责把可信身份换成临时 AWS 凭据;两者解决不同问题,更不能替代业务租户与授权模型。
先分清 User Pool 与 Identity Pool
Amazon Cognito 是 AWS 托管的 Region 服务,不需要在开发机安装服务端。User pool 是用户目录和 OAuth 2.0/OIDC 身份提供方,保存本地或联邦用户 profile,并签发 ID、access 和 refresh token。App client 依附于 user pool,控制认证 flow、OAuth flow、scope、callback/logout URL、token 生命周期、支持的 IdP,以及是否创建 client secret。一个 pool 可以服务多个 app client,但每个客户端应按应用形态拥有独立配置和撤销边界。
User pool federation 把社交、OIDC 或 SAML 上游身份映射为 Cognito 用户 profile,再由 user pool 向应用签发统一令牌。Identity pool 属于另一条链:它接受 user pool、OIDC、SAML、社交或自定义开发者身份,建立 identity,并通过 AWS STS 取得临时 AWS credentials。只需要用 JWT 调业务 API 时,user pool 已足够;只有浏览器或移动端必须直接访问 S3、AppSync 等 AWS 资源时,才评估 identity pool。
Identity pool 不是用户目录,不保存可登录密码,也不是 B2B Organization。Cognito group 可参与 token claim 或 IAM role 选择,但不会自动提供邀请、组织专属 Connection、成员管理和业务租户隔离。共享 pool 的多租户模型可组合 group、不可由用户自行修改的 custom attribute 与业务数据库 membership;若要求租户级管理隔离、独立策略或故障域,则评估 pool-per-tenant。对象职责和当前能力从 Cognito 产品总览与 User pool 说明核对。
在沙箱账号创建最小 User Pool
所有创建动作放在沙箱 AWS account 和专用测试 Region。操作者先通过 AWS IAM Identity Center、角色扮演或其他短期身份取得控制面权限,不在本地保存长期 access key。创建角色只允许所需 Cognito 动作及必要的标签操作;若还要配置域名、日志、Lambda、KMS、SNS 或 SES,应分别增加对应最小权限,并接受 SCP、permission boundary 和服务配额的共同约束。
先确认 profile、调用主体与 Region,再创建带标签的测试 pool。aws cognito-idp 管 user pool,aws cognito-identity 管 identity pool;名称相近不代表对象或 API 可以混用。CLI 参数和默认行为会演进,执行前从 create-user-pool CLI 参考核对目标版本。
set -eu
export AWS_PROFILE='<profile>'
export AWS_REGION='<region>'
aws sts get-caller-identity --profile "$AWS_PROFILE"
aws configure get region --profile "$AWS_PROFILE"
USER_POOL_ID=$(aws cognito-idp create-user-pool \
--pool-name identity-lab \
--username-attributes email \
--auto-verified-attributes email \
--user-pool-tags purpose=identity-lab,owner=platform-team \
--profile "$AWS_PROFILE" --region "$AWS_REGION" \
--query 'UserPool.Id' --output text)
printf 'USER_POOL_ID=%s\n' "$USER_POOL_ID"随后创建不带 secret 的 public app client。浏览器和原生应用不能可靠保守 secret,所以不要添加 --generate-secret。示例只启用适合测试的 flow;真实应用应根据 SDK、managed login 和风险要求选择 flow,并从 App client 设置确认每个开关的影响。
APP_CLIENT_ID=$(aws cognito-idp create-user-pool-client \
--user-pool-id "$USER_POOL_ID" \
--client-name identity-lab-public \
--explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH \
--prevent-user-existence-errors ENABLED \
--profile "$AWS_PROFILE" --region "$AWS_REGION" \
--query 'UserPoolClient.ClientId' --output text)
aws cognito-idp describe-user-pool-client \
--user-pool-id "$USER_POOL_ID" \
--client-id "$APP_CLIENT_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION"创建 pool 会带来目录与后续消息、域名、日志等潜在成本;它不会自动证明邮件或短信已可送达。验证通知前应检查 SNS/SES sandbox、发送身份、运营商注册、Region 和服务角色,不要把真实手机号或邮箱写进脚本。若团队用 CloudFormation、CDK 或 Terraform 管理资源,首次 CLI 实验结束后应转成 IaC,让 schema、删除保护、标签、日志和角色关系进入评审。
用账号、Region 与 Discovery 做第一组正反实验
正例同时证明三层事实:当前 AWS principal 属于预期账号,user pool 位于预期 Region,OIDC discovery 的 issuer 与项目配置完全一致。只看控制台里的同名 pool 不够,因为资源名可重复,真正契约是 Region 与 pool ID 组成的 issuer。
aws sts get-caller-identity \
--profile "$AWS_PROFILE"
aws cognito-idp describe-user-pool \
--user-pool-id "$USER_POOL_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION"
curl --fail --silent --show-error \
"https://cognito-idp.$AWS_REGION.amazonaws.com/$USER_POOL_ID/.well-known/openid-configuration" \
| jq '{issuer, authorization_endpoint, token_endpoint, jwks_uri}'反例故意把同一个 pool ID 发往错误 Region。预期是 ResourceNotFoundException 或非零退出;部署启动检查应立即终止,不能扫描其他 Region 猜测资源,更不能在找不到 JWKS 时跳过验签。
set +e
aws cognito-idp describe-user-pool \
--user-pool-id "$USER_POOL_ID" \
--profile "$AWS_PROFILE" --region "$WRONG_REGION" \
> cognito-wrong-region.json 2>&1
status=$?
set -e
test "$status" -ne 0
rg 'ResourceNotFoundException|not found|Unable to locate' cognito-wrong-region.json
rm -f cognito-wrong-region.json若反例报的是凭据、代理或 DNS 错误,说明实验尚未到达 Region/pool 判定层;先修复调用身份和网络,再重跑,不要把“任何失败”都算作正确拒绝。实验记录保留账号别名、角色 ARN 的脱敏标识、Region、pool ID 后缀和命令退出码即可,不保存 session token 或完整用户资料。
用 Domain 与 App Client 开启浏览器联邦
SDK/API 可以在没有 user pool domain 的情况下认证本地用户,但 managed login 和第三方 IdP 浏览器联邦需要 domain 承载 /oauth2/authorize、/oauth2/token 等端点。可选择 Cognito 前缀域名或自定义域名;自定义域名还涉及 DNS、ACM 证书、CloudFront 和所有权验证。启用前从 User pool domain 文档确认目标 Region 与域名约束。
App client 要登记精确 callback/logout URL、OAuth flow、scope 和 Supported Identity Providers。Authorization Code + PKCE 适合 SPA 与原生应用;confidential 服务端 client 才能持有 secret。Federated 用户必须通过浏览器 authorize/login 链进入,不能拿上游密码调用 InitiateAuth。配置 OIDC/SAML IdP 时,上游侧也要登记 Cognito redirect URI,随后验证 issuer/metadata、client credential 或证书、attribute mapping 与 app client 启用关系,细节从 外部 IdP 配置核对。
下面是第二组实验的正例入口:先确认 app client 已启用预期 IdP,并人工走一次授权码登录。回调端只记录 state 校验结果、错误码和 correlation ID,不记录 authorization code 或 token。
aws cognito-idp describe-user-pool-client \
--user-pool-id "$USER_POOL_ID" \
--client-id "$APP_CLIENT_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION" \
--query 'UserPoolClient.{Providers:SupportedIdentityProviders,Callbacks:CallbackURLs,Flows:AllowedOAuthFlows}'反例使用未注册的 redirect_uri 发起 authorize 请求。平台应拒绝该请求,绝不能把 code 或 token 带到 invalid.example。这里不把某个固定错误页面文案当断言,因为 managed login 界面会变化;断言目标是响应 Location 不指向攻击者地址且没有敏感参数。
curl --silent --show-error --dump-header cognito-headers.txt \
--output /dev/null \
"https://$COGNITO_DOMAIN/oauth2/authorize?response_type=code&client_id=$APP_CLIENT_ID&redirect_uri=https%3A%2F%2Finvalid.example%2Fcallback&scope=openid"
! rg -i '^location: https://invalid\.example/.*(code|access_token|id_token)=' \
cognito-headers.txt
rm -f cognito-headers.txt如果反例真的跳向未登记地址,应立即禁用 app client、撤销可能暴露的 client credential、保存响应头和 CloudTrail 事件并检查配置写入者。不要为了让测试通过而添加通配 callback;精确 redirect URI 是授权码不被转交给攻击者的关键边界。
在业务 API 中严格验证三类 Token
ID token 面向客户端,包含用户身份 claims;access token 面向 user pool 资源服务器和 scope;refresh token 只用于换取新 token,不交给业务 API。后端验签从 discovery 获取 JWKS,固定 issuer,并区分 token use。Cognito ID token 通常以 app client ID 作为 audience;access token 则需要检查 client_id 与 token_use=access,不能用一条模糊规则接受两类 token。
import { createRemoteJWKSet, jwtVerify } from "jose";
const region = process.env.AWS_REGION;
const poolId = process.env.COGNITO_USER_POOL_ID;
const appClientId = process.env.COGNITO_APP_CLIENT_ID;
const issuer = `https://cognito-idp.${region}.amazonaws.com/${poolId}`;
const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
export async function verifyAccessToken(token, requiredScope) {
const { payload } = await jwtVerify(token, jwks, { issuer });
if (payload.token_use !== "access" || payload.client_id !== appClientId) {
throw new Error("wrong_token_context");
}
const scopes = new Set(String(payload.scope ?? "").split(" ").filter(Boolean));
if (!scopes.has(requiredScope)) throw new Error("insufficient_scope");
return { subject: payload.sub, scopes };
}未知 kid 可能表示密钥轮换,也可能是伪造 token。JWKS 客户端可受控刷新并缓存,但刷新次数、超时与失败语义必须有限;不能从 token 自带的任意 URL 获取密钥。API 再根据稳定的业务 user key、租户 membership 和资源归属授权,不能仅凭 email、group 名或可变 custom attribute 放行。完整 JWT、refresh token、session cookie、设备 key 和用户 PII 不进入日志。
前端使用 Amplify 或 AWS SDK 时,同样要由项目锁文件固定依赖,并把 Region、pool ID、app client ID 与 domain 作为环境公开配置。public client 不应有 secret。可以在 CI 使用可撤销测试 secret 指纹扫描构建产物:正例是 app client 配置不依赖 secret,反例是在 dist/ 或 source map 中发现指纹后立即失败。
aws cognito-idp describe-user-pool-client \
--user-pool-id "$USER_POOL_ID" --client-id "$PUBLIC_APP_CLIENT_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION" \
--query 'UserPoolClient.ClientSecret'
! rg --fixed-strings "$KNOWN_TEST_CLIENT_SECRET" dist/ build/扫描参数只能是已撤销的测试 secret 指纹,不能把生产 secret 展开到 shell history 或 CI log。若 public client 已生成 secret,不能假装前端会保密;创建新的无 secret app client,切换 client ID,等待旧会话按策略失效,然后删除旧 client。
只在需要 AWS 临时凭据时引入 Identity Pool
浏览器或移动端确实要直接访问 AWS 资源时,identity pool 把可信 provider token 映射为 identity,再通过 STS 获取短期 access key、secret key 和 session token。authenticated 与 unauthenticated role 的 trust policy 必须限制目标 identity pool、认证状态和可接受 provider;角色权限再限制资源 ARN、操作和条件。不要在前端放长期 IAM user key,也不要把 authenticated role 等同于“已获所有业务权限”。
典型 S3 上传应把用户限制在自己的前缀,例如根据 Cognito identity ID 构造资源条件,并由后端维护 identity ID 与业务主体的映射。identity ID 不是 user pool sub,切换 provider、合并身份或迁移 pool 后不能假设它永久不变。客户端取得凭据后执行一次最小 PutObject/GetObject 正例,再尝试访问另一个主体前缀作为反例;反例必须得到 AccessDenied,CloudTrail 或数据事件能关联 assumed role 与资源。
Identity pool 控制面使用独立 namespace:
aws cognito-identity list-identity-pools \
--max-results 10 \
--profile "$AWS_PROFILE" --region "$AWS_REGION"
aws cognito-idp list-user-pools \
--max-results 10 \
--profile "$AWS_PROFILE" --region "$AWS_REGION"两份列表不同是正确结果。把 user pool token 交给 identity pool 时,还要验证 provider key、app client、server-side token check 和 role mapping。若业务 API 本身可以代理上传、执行细粒度授权并生成预签名 URL,identity pool 可能只增加 IAM、STS、缓存和撤销复杂度;选型应比较直连 AWS 的延迟与离线能力,和后端代理的策略集中、审计与数据边界。
用 Lambda Trigger 扩展而不泄露认证数据
User pool Lambda triggers 覆盖注册、认证 challenge、token 生成、消息、自定义 sender、用户迁移和 inbound federation 等阶段。它们不是一组同构 webhook:每个 trigger 有自己的 event schema、同步/异步行为、可修改字段和失败影响。绑定错误版本、返回缺失字段或 Lambda 超时,都可能让用户卡在注册或登录关键路径。
Pre Token Generation 适合加入体积受控、来源可信的 claim。下面骨架只把已验证的 tenant key 映射到 access token,且不记录完整 event。实际可用 event version、claimsAndScopeOverrideDetails 和 feature plan 应从 Lambda triggers 文档核对,不能假设所有 pool 都支持同一事件结构。
export const handler = async (event) => {
const tenantKey = event.request.userAttributes["custom:tenant_key"];
if (tenantKey) {
event.response.claimsAndScopeOverrideDetails = {
accessTokenGeneration: {
claimsToAddOrOverride: { tenant_key: tenantKey },
},
};
}
console.log(JSON.stringify({
triggerSource: event.triggerSource,
userPoolId: event.userPoolId,
userNameHash: event.userName ? "present" : "absent",
}));
return event;
};外部调用放入认证路径前要设置短超时、有限重试、幂等键和明确的 fail-open/fail-close 决策。密码迁移 trigger 必须访问仍能验证旧密码的服务;旧库超时或属性冲突时应安全失败并留下 correlation ID,绝不能打印 password、authorization code、token、完整 assertion、ClientMetadata 或旧库响应。Trigger 的执行角色只允许写自身日志和访问必要依赖,secret 放在 Secrets Manager 并由 KMS 与资源策略限制。
用 CloudTrail、Metrics 与日志导出还原因果链
CloudTrail 回答“哪个 AWS/IAM principal 调用了哪个 API”,适合追控制面创建、更新、删除与部分受支持认证事件;CloudWatch Metrics 回答登录成功/失败、延迟和 quota 使用趋势;Lambda 自身日志以及配置后的用户活动或通知错误日志提供更细粒度证据。三者不能互相替代。CloudTrail 会遮蔽部分常见密码/token 字段,但不会自动识别调用者塞进普通字段的全部 PII,因此 ClientMetadata、custom attributes 和日志消息仍要主动去敏。
排障先分层。ResourceNotFoundException 优先核对 account、Region 与 pool ID;NotAuthorizedException 区分用户凭据、app client secret hash、用户状态与 flow;浏览器回调失败核对 domain、app client、redirect、IdP metadata 和 attribute mapping;API 401 核对 issuer、JWKS、kid、token_use、client/audience、expiry 与时钟;API 403 再看 scope、group、tenant membership 和 IAM policy。每层保存 request ID、pool/app client、Region、trigger source、错误码和配置 revision,不保存原始 token。
长期告警应同时观察失败率、延迟、配额使用、Lambda errors/throttles、日志投递失败和 STS 拒绝。细粒度 user activity 与威胁保护日志可能依赖额外配置和商业能力,不能假设每个 pool 默认存在;从 Cognito 监控入口、CloudTrail 说明和账户内实际配置确认可用证据。
按 Account 与 Region 规划配额、成本和故障域
User pool API quota 常按同一 AWS account 与 Region 聚合到类别,单个 pool 的压测可能影响同 Region 其他 pool。Identity pool 则按自己的 operation quota 计算,不能沿用 user pool 的模型。登录、token 刷新、managed login、Lambda trigger、外部 IdP、短信邮件、日志导出和 STS 都可能形成独立瓶颈;容量模型要从一次用户动作展开调用倍数和重试,不只统计页面 PV。
上线前在 Service Quotas 查询实际账户值,并从 Cognito Quotas检查哪些资源和速率可调整。收到 throttling 时采用带抖动的指数退避、总 deadline 和并发上限;不要让所有客户端同时刷新 token。callback/logout URL、IdP、app client、resource server、custom attribute 与 token claim 也有数量或长度约束,多租户设计不能靠无限增加 group 和 claim 延续。
成本评审按 MAU 或产品当前计量、消息发送、Lambda、CloudWatch、CloudTrail、Firehose/S3、KMS、WAF、域名与数据传输逐项核算,并直接打开 AWS Cognito Pricing和 Cost Explorer 查看目标账户口径。价格、免费额度、feature plan 与 Region 支持会变化,架构决策记录保存查询入口、预算假设和告警 owner,不复制静态数字。
多 Region 不是复制一个 pool ID 就完成。普通 profile 导出/import 不包含密码、MFA secret、风险历史、联邦 profile、域名、Lambda、IAM 和全部属性语义。平台提供的多 Region 能力也有资格、基础设施、KMS、一致性和写入限制,应从 Multi-Region 说明核对后再设计。应用仍要处理 issuer、DNS、会话、联邦首次登录和 secondary 限制,不能把托管复制当成透明跨区切换。
清理双池资源并恢复权限边界
开发清理先停止客户端登录和临时 AWS 访问,再撤销 identity pool 角色信任与 app client secret,解绑 Lambda triggers、日志目标和外部 IdP,删除测试用户、group、domain 与 app client,最后删除 identity pool 和 user pool。S3 测试对象、CloudWatch log group、Firehose、KMS grant、Lambda、IAM role、SNS/SES 配置和 DNS 不会因为删 pool 自动全部消失,必须按标签和 IaC state 逐项核销。
# 仅在确认变量指向沙箱资源后执行。
aws cognito-idp delete-user-pool-client \
--user-pool-id "$USER_POOL_ID" --client-id "$APP_CLIENT_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION"
aws cognito-idp delete-user-pool \
--user-pool-id "$USER_POOL_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION"
aws cognito-idp describe-user-pool \
--user-pool-id "$USER_POOL_ID" \
--profile "$AWS_PROFILE" --region "$AWS_REGION" && exit 1 || true生产回滚先停止配置写入和客户端推广,恢复上一版 app client/domain/IdP/trigger 配置,撤销新角色与凭据,再以真实授权码、API 验签、错误 redirect、跨前缀 AccessDenied 和日志关联验证。删除 pool 通常不是可逆动作;回滚材料应是可重建的 IaC、外部身份源配置、用户迁移状态和业务映射,不是对“完整备份”的错误承诺。
迁移退出时接受密码、MFA 与主体 ID 会变化
迁入 Cognito 有两条常见路线。CSV import 能导入 profile,但不接受密码明文或 hash,用户需要重置密码;User Migration Lambda 可在首次登录时调用旧身份库验证旧密码并创建用户,使活跃用户懒迁移。后者要求旧验证服务在迁移窗口持续可用,并处理超时、重复 email、alias 冲突、verified attribute 与幂等;已迁移用户再次登录不得继续访问旧库。具体行为从 用户导入总览和 CSV 导入说明核对。
迁出 Cognito 时,profile 导出不提供可复用密码。sub、federated username、group、custom attribute、verified flags、identity pool identity ID 与业务 user key 都要显式映射;MFA、设备、风险历史、active session 和 refresh token 默认按不可移植处理。若客户端仍需 AWS 临时凭据,可以暂时保留 identity pool,把新 IdP 的 OIDC token 接入并重新验证 trust、claims 与 IAM role,而不是强行把它映射成目标平台的组织对象。
切流前建立 shadow/canary 用例:本地用户首次迁移与重复登录、forgot-password、社交/OIDC/SAML 首次和重复登录、上游属性变化、重复邮箱、错误 issuer/audience、未知 kid、Lambda 超时、429、日志延迟、MFA 重绑、旧 session 失效,以及另一个租户身份访问当前资源的拒绝。旧、新系统用 migration correlation ID 关联,业务数据库保存独立主体主键和迁移状态。
只有当旧 issuer 的最长 token 生命周期结束、旧 app client 与 IdP callback 已撤销、identity pool role trust 被收回、未迁移用户已有密码重置或支持路径、日志与账单资源核销完成后,才删除旧 pool。安全退出的判据不是控制台里少了一个资源,而是旧身份链不能再签发 token、换取 AWS credentials 或影响业务授权,且团队仍能用保留证据解释每个主体去了哪里。
