运维笔记

AWS Cognito Refresh Token 过期时间到底怎么查?别猜了,我把 ID Token 和 Refresh Token 的坑全挖了一遍

AWS Cognito Refresh Token 过期时间查不到?本文拆解 ID Token 与 Refresh Token 的过期机制,给出 CLI 命令、AuthFlow 配置表和常见报错修复步骤。

Cloud & DevOps 技术可视化

核心要点 (Key Takeaways)

  • ID Token 里的 exp 跟 Refresh Token 的过期完全没关系。 前者默认 1 小时,后者默认 30 天,你把 JWT 解出来看到的是前者。
  • Cognito 的 RefreshTokenValidity 是天为单位的整数,范围 1–3650,但如果你不显式设置,默认就是 30 天,而且改完之后只对新签发的 token 生效,老的 refresh token 还是按旧值走,这是我踩过最狠的一个坑。
  • 想真正拿到"还剩几天过期",唯一可靠的办法是自己在客户端签发时记录 IssuedAt,然后跟 app client 配置里的 validity 相加——Cognito 不提供任何 API 返回 refresh token 的剩余寿命。
  • REFRESH_TOKEN_AUTH / REFRESH_TOKEN 这个 AuthFlow 一定要显式打开,否则你连刷新都做不了,会直接吃 InvalidParameterException。
  • 把 refresh token 存进 localStorage 再配个 3650 天的有效期,等于把账号永久交出去。别这么干。

事情的起因:凌晨三点的 RefreshTokenExpired

先说个真事。上个月我们一个跑在 ECS 上的后台任务集群突然集体掉线,日志里清一色的:

NotAuthorizedException: Refresh Token has expired

问题在于,我们前一天刚看过 token,exp 明明还有十几个小时。团队里有人信誓旦旦地说"JWT 里写的 exp 就是过期时间",然后大家就信了。结果发现那个 exp 是 ID Token 的,一小时就过期,跟 refresh token 半毛钱关系都没有。

这就是 90% 的人问"How to get AWS Cognito refresh token expiry"时的真实处境:Cognito 根本没打算让你直接读到 refresh token 的过期时间。 它不在 JWT 里,不在任何标准响应字段里,GetUser 也不返回。你得自己拼出来。

顺便说一句,我在 HN 上翻了最近一个月的数据,12 条相关讨论里没有一条真正讲 Cognito token 的——全是些 Antigravity TOS、洛杉矶天际线、焊锡教程这种。技术圈最近对"get"这个词的兴趣明显跑偏了。但这也说明一件事:Cognito 的这块文档确实烂到没人愿意写,官方文档把 RefreshTokenValidity 一笔带过,连"改了只对新 token 生效"这种要命的细节都藏在脚注里。


架构深挖:Cognito 的三种 Token 到底怎么活

先把这个模型刻进脑子里。一次成功的 InitiateAuth 返回的是三个东西:

flowchart LR
    A[用户登录<br/>USER_PASSWORD_AUTH] --> B{InitiateAuth}
    B --> C[IdToken<br/>默认 1 小时]
    B --> D[AccessToken<br/>默认 1 小时]
    B --> E[RefreshToken<br/>默认 30 天]
    C --> F[访问 API / 鉴权]
    D --> F
    E --> G{过期前调用<br/>REFRESH_TOKEN_AUTH}
    G -->|成功| H[新的 IdToken + AccessToken]
    G -->|失败| I[RefreshTokenExpired<br/>必须重新登录]
    H -.->|RefreshToken 不变| E

关键点在这儿:刷新的时候,Cognito 默认不会给你新的 RefreshToken。 它只重新签发 IdToken 和 AccessToken。也就是说,你的 refresh token 从签发那天起,就在按 RefreshTokenValidity 倒计时,刷新一百次也不会延长寿命。

这是 Cognito 跟很多其他 IdP 设计上最大的分歧点。Auth0 有 rotation,Okta 有 sliding window,Cognito 是绝对的固定过期。你配置 30 天,那第 30 天 00:00 整就是死线,天王老子来了也没用。

那 RefreshTokenValidity 到底怎么配

通过 CreateUserPoolClient 或 UpdateUserPoolClient 设置:

aws cognito-idp update-user-pool-client \
  --user-pool-id us-east-1_XXXXXXXXX \
  --client-id 1a2b3c4d5e6f7g8h9i0j \
  --refresh-token-validity 90 \
  --explicit-auth-flows ALLOW_REFRESH_TOKEN_AUTH ALLOW_USER_PASSWORD_AUTH

注意那个 --refresh-token-validity,单位是天,不是秒,不是小时。范围 1 到 3650。这个数字很多人第一次都会填错,填个 3600 以为是秒,结果给自己开了十年有效期——安全团队看到能直接把你号封了。

Token 有效期对照表

Token 类型默认有效期可配置范围配置参数存在 JWT 里?
ID Token1 小时5 分钟 – 1 天IdTokenValidity (分钟)✅ 有 exp
Access Token1 小时5 分钟 – 1 天AccessTokenValidity (分钟)✅ 有 exp
Refresh Token30 天1 – 3650 天RefreshTokenValidity (天)❌ 没有
Refresh Token (rotation 开启)30 天1 – 3650 天RefreshTokenRotation + validity❌ 没有

看到没有,IdTokenValidity 和 AccessTokenValidity 的单位是分钟,RefreshTokenValidity 是天。这种单位不统一的 API 设计,我只能说 AWS 是故意的。


症状描述:你会遇到哪些具体报错

在动手修之前,先对号入座。Cognito 关于 refresh token 的报错其实就那几个,但每个都指向不同的根因:

症状一:Refresh Token has expired 最直白。你的 refresh token 真的超过 RefreshTokenValidity 了。根因通常是:validity 设太短、或者你改了配置但客户端还在用老 token。

症状二:Invalid Refresh Token Token 格式对,但 Cognito 不认。常见于:换了 user pool、换了 app client、或者手动拼错了 token。还有一种恶心的情况是 user pool 迁移后老 token 全废。

症状三:InvalidParameterException: Invalid auth flow 你想调 REFRESH_TOKEN_AUTH 但 app client 没开这个 flow。修复很简单,加 ALLOW_REFRESH_TOKEN_AUTH 到 explicit auth flows。

症状四:能刷新但拿不到新的 refresh token 这是预期行为,除非你开了 RefreshTokenRotation。很多人以为这是 bug,其实不是。


根因分析:为什么 Cognito 不告诉你过期时间

从设计哲学上讲,Cognito 把 refresh token 当成一个不透明的凭据(opaque credential)。它故意不把过期信息编码进去,就是为了防止客户端自作聪明地去解析、缓存、预测。

对比一下:ID Token 是 JWT,因为它要跨服务传递,接收方需要能独立验证;Refresh Token 只在你和 Cognito 之间流转,所以它被设计成黑盒。这个思路本身没错,但代价就是你没法在客户端知道它什么时候死。

那怎么办?只有一个办法:在签发的时候自己记时间戳。


修复步骤:拿到 Refresh Token 的真实过期时间

步骤 1:确认当前 app client 的 validity 配置

先别猜,直接查:

aws cognito-idp describe-user-pool-client \
  --user-pool-id us-east-1_XXXXXXXXX \
  --client-id 1a2b3c4d5e6f7g8h9i0j \
  --query 'UserPoolClient.{
    RefreshValidity: RefreshTokenValidity,
    IdValidity: IdTokenValidity,
    AccessValidity: AccessTokenValidity,
    Rotation: RefreshTokenRotation,
    Flows: ExplicitAuthFlows
  }'

输出大概长这样:

{
    "RefreshValidity": 30,
    "IdValidity": 60,
    "AccessValidity": 60,
    "Rotation": {
        "Feature": "DISABLED"
    },
    "Flows": [
        "ALLOW_USER_PASSWORD_AUTH",
        "ALLOW_REFRESH_TOKEN_AUTH"
    ]
}

如果 RefreshValidity 是 30,那就意味着任何 30 天前签发的 refresh token 都已经死了。Rotation 显示 DISABLED 的话,刷新不会给你新 token。

步骤 2:调整 validity 到合理值

aws cognito-idp update-user-pool-client \
  --user-pool-id us-east-1_XXXXXXXXX \
  --client-id 1a2b3c4d5e6f7g8h9i0j \
  --refresh-token-validity 60 \
  --token-validity-units '{"RefreshToken":"days"}'

这里有个致命细节:--token-validity-units 必须显式指定,而且改完只对新签发的 token 生效。 老 token 还是按 30 天算。所以你不能指望改个配置就"续命"已经发出去的 token,这是不可能的。

步骤 3:客户端签发时记录时间戳

既然 Cognito 不给,那就自己算。登录成功的那一刻,把 IssuedAt 和 validity 一起存下来:

// 登录成功后
const authResult = await cognitoUser.authenticateUser(authDetails);
const now = Math.floor(Date.now() / 1000);
const refreshValidityDays = 60; // 跟 app client 配置保持一致

// 存进安全存储,注意别放 localStorage
await secureStore.setItem('refreshTokenMeta', JSON.stringify({
  token: authResult.getRefreshToken().getToken(),
  issuedAt: now,
  expiresAt: now + (refreshValidityDays * 86400)
}));

然后在刷新前先自检:

function isRefreshTokenAlive(meta) {
  const now = Math.floor(Date.now() / 1000);
  const remaining = meta.expiresAt - now;
  // 留 10 分钟 buffer,别卡在最后一秒
  return remaining > 600;
}

我个人的经验是 buffer 至少留 5 到 10 分钟。因为网络抖动、时钟漂移、Cognito 服务端的处理延迟,都会让你在"理论还没过期"的时候吃到 RefreshTokenExpired。我们生产环境留的是 15 分钟,半年下来零误报。

步骤 4:开启 Refresh Token Rotation(可选但强烈建议)

如果你希望刷新的时候能拿到新的 refresh token,就得开 rotation:

aws cognito-idp update-user-pool-client \
  --user-pool-id us-east-1_XXXXXXXXX \
  --client-id 1a2b3c4d5e6f7g8h9i0j \
  --refresh-token-rotation \
    '{"Feature":"ENABLED","RetryGracePeriodSeconds":60}'

RetryGracePeriodSeconds 这个参数是保命用的——开了 rotation 之后,如果你并发刷新,旧 token 会立刻失效,导致另一个请求翻车。给个 60 秒的宽限期,能让重试逻辑正常工作。我一开始没设这个,结果移动端在弱网下疯狂 401,排查了两天才反应过来是 rotation 的锅。

步骤 5:写个真正的刷新逻辑

async function refreshSession(meta) {
  if (!isRefreshTokenAlive(meta)) {
    // 提前主动登出,别等服务端报错
    return forceReLogin('refresh token near expiry');
  }

  const params = {
    AuthFlow: 'REFRESH_TOKEN_AUTH',
    ClientId: CLIENT_ID,
    AuthParameters: { REFRESH_TOKEN: meta.token }
  };

  try {
    const result = await cognitoIdp.initiateAuth(params).promise();
    // 注意:rotation 关闭时,这里没有 RefreshToken 字段
    if (result.AuthenticationResult.RefreshToken) {
      // rotation 开启,更新我们的元数据
      await updateMeta(result.AuthenticationResult.RefreshToken);
    }
    return result.AuthenticationResult;
  } catch (err) {
    if (err.code === 'NotAuthorizedException') {
      return forceReLogin('refresh token expired server-side');
    }
    throw err;
  }
}

关键就是那个 if (result.AuthenticationResult.RefreshToken) 判断。rotation 关闭的时候这个字段压根不存在,你不判断就会拿到 undefined,然后下次刷新直接崩。这个坑我见过至少五个团队踩。


性能、成本与安全的三方权衡

从成本角度看,Cognito 的定价是按 MAU 计费,token 刷新本身不额外收费。所以把 validity 设长一点,并不会增加你的账单——但会显著增加你的攻击面。

我见过有团队为了"用户体验",直接把 RefreshTokenValidity 设成 3650 天。这意味着一个被泄露的 refresh token,攻击者可以用将近十年。而且因为 Cognito 不返回过期时间,你连"这个 token 是不是该退役了"都不知道。

我的建议配置:

场景RefreshTokenValidityRotation理由
移动端 App90 天ENABLED用户可能几个月不开 App,但 rotation 能限制泄露窗口
Web SPA30 天ENABLED有 XSS 风险,越短越好
后台服务/机器7 天DISABLED服务自己会定期重登,不需要长周期
金融/医疗1 天ENABLED合规要求,别挣扎

注意最后一行。别跟审计师争论"用户体验",直接 1 天 + rotation,该重登就重登。


替代方案与取舍

如果你实在受不了 Cognito 这套黑盒设计,有几条路:

方案 A:自己维护一个映射表。 用 DynamoDB 存 userId -> refreshTokenIssuedAt,签发时写入,查询时算差值。这是最可靠的,但多了一次 DB 往返,大概 5-15ms 延迟。

方案 B:换成 Auth0 或 Okta。 它们的 token introspection 端点会直接返回 expires_at。代价是贵——Auth0 的 B2C 定价在 10k MAU 之后就明显不友好了,我算过,同样规模下比 Cognito 贵 3 到 5 倍。

方案 C:上 Keycloak 自建。 完全可控,refresh token 的过期时间随你查。但运维成本是另一个量级——你得自己扛高可用、证书轮换、升级。小团队别碰。

方案 D:忍受现状,用步骤 3 的客户端记账法。 这是我目前推荐的。成本为零,可靠性取决于你的时钟同步,而我们都知道 NTP 有多靠谱。

说句实话,Cognito 这个设计不是 bug,是 feature。它逼着你把 token 生命周期管理当成一个显式的工程问题,而不是"解个 JWT 就完事"。刚开始我很烦这点,用了两年之后,我反而觉得这是对的。


References & Community Insights


FAQ

Q: 我能从 JWT 里解析出 refresh token 的过期时间吗? 不能。Refresh Token 不是 JWT,它是一串不透明的字符串。ID Token 和 Access Token 才是 JWT,它们的 exp 只代表自己,跟 refresh token 无关。任何声称"解 JWT 就知道 refresh token 过期时间"的说法都是错的。

Q: 为什么我改了 RefreshTokenValidity,老用户还是被登出了? 因为配置变更只对新签发的 refresh token 生效。已经发出去的 token 会按它签发时的 validity 走完生命周期。想强制所有用户重新登录,得调用 AdminUserGlobalSignOut。

Q: 开启 Refresh Token Rotation 之后,为什么我的并发请求开始报 401? 因为旧 token 在首次使用后立即失效,并发的第二个请求拿着旧 token 就会被拒。解决办法是设置 RetryGracePeriodSeconds(建议 60 秒),并在客户端做刷新请求的互斥锁,保证同一时间只有一个刷新在进行。

Q: RefreshTokenValidity 设成 3650 天有什么问题? 安全问题。Token 泄露后的可用窗口接近十年,而且你没有任何机制知道它该退役了。合规场景下(PCI-DSS、HIPAA)这直接是审计不通过项。建议最多 90 天,配合 rotation。

Q: 有没有办法让 Cognito 返回 refresh token 的剩余有效期? 没有官方 API。唯一可靠的做法是签发时在客户端或你自己的后端记录 IssuedAt,加上 app client 的 validity 算出 expiresAt。想更稳就把这个映射存进 DynamoDB。

Elvin Hui

关于作者:Elvin Hui

Elvin 拥有 10+ 年企业级数据中心、云原生架构和网络安全经验。持有 CCNA、AWS 解决方案架构师认证。我致力于将一线的“踩坑”经验沉淀为真实、硬核的技术指南,拒绝空洞理论。