PAYATHON 2026

ASWebAuthenticationSession 和 Auth Tab 仍需 PKCE 吗?

技术老齐

结论

需要。只要移动应用仍属于 RFC 8252 定义的 public native app client,就应继续使用 PKCE,通常选择 S256。

ASWebAuthenticationSession 和 Chrome Auth Tab 能提高回调交付的安全性,降低其他应用抢占 callback 或干扰 Intent 的风险。不过,客户端仍然是 public client,也仍需遵守 RFC 8252 第 6 节 对 PKCE 的要求。

两类机制解决的问题不同

平台提供的安全回调用于确定授权完成后由哪个应用或会话接收 callback。

PKCE 则用于确认拿到 authorization code 的一方,正是发起这次授权请求的客户端实例。

授权开始时,应用为本次交易生成 code_verifier,并把它的摘要作为 code_challenge 发送给 authorization server。兑换 token 时,应用提交原始 code_verifier,服务器验证通过后才接受 authorization code。

即使攻击者通过其他路径拿到了 authorization code,只要没有 code_verifier,就无法完成 token 交换。安全 callback 用于降低部分拦截风险,PKCE 在 OAuth 协议层将授权请求与 token 请求绑定。两者可以配合使用。

推荐的授权流程

  1. 每次授权都生成新的高熵 code_verifier,长度须符合 RFC 7636 的要求。

  2. 计算 code_challenge:

    code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
    
  3. 在 authorization request 中发送以下参数:

    response_type=code
    code_challenge=<calculated value>
    code_challenge_method=S256
    state=<random value>
    
  4. 使用 ASWebAuthenticationSession 或 Chrome Auth Tab 打开授权 URL。

  5. 收到 callback 后先验证 state,再提取 code。

  6. 调用 token endpoint,提交与这次授权对应的 code_verifier。

  7. 无论授权成功还是失败,都要删除 code_verifier 和 state。

授权请求示例:

GET /authorize?
    response_type=code&
    client_id=mobile-app&
    redirect_uri=com.example.app:/oauth/callback&
    scope=openid%20profile&
    state=RANDOM_STATE&
    code_challenge=BASE64URL_SHA256_VALUE&
    code_challenge_method=S256

token 请求示例:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
client_id=mobile-app&
redirect_uri=com.example.app:/oauth/callback&
code_verifier=ORIGINAL_CODE_VERIFIER

authorization server 必须检查 code_verifier 是否与最初的 code_challenge 匹配。如果服务器端没有强制校验,仅由客户端生成这些参数并不能提供 PKCE 保护。

跨平台生成 PKCE 参数的示例

以下 Dart 示例适用于 Flutter 客户端,其中 crypto 来自 package:crypto/crypto.dart:

import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';

String randomBase64Url(int byteLength) {
  final random = Random.secure();
  final bytes = List<int>.generate(
    byteLength,
    (_) => random.nextInt(256),
  );

  return base64Url.encode(bytes).replaceAll('=', '');
}

({String verifier, String challenge}) createPkce() {
  // 32 个随机字节编码后得到 43 个字符的 verifier。
  final verifier = randomBase64Url(32);

  final digest = sha256.convert(ascii.encode(verifier));
  final challenge = base64Url.encode(digest.bytes).replaceAll('=', '');

  return (
    verifier: verifier,
    challenge: challenge,
  );
}

void main() {
  final pkce = createPkce();

  // pkce.challenge 放入 authorization request。
  // pkce.verifier 仅保存在本次授权交易中,
  // 收到 callback 后用于 token request。
}

不要把 code_verifier 放进 authorization URL,也不要将它写入日志。如果存在并发登录,应使用 state 将每个 callback 映射到对应的 code_verifier,不能只维护一个全局值。

state、nonce 和 PKCE 不能互相替代

这几个参数分别处理不同的风险:

  • state 用于关联授权请求与 callback,并防范登录 CSRF。
  • nonce 用于 OpenID Connect 流程,验证 ID Token 与当前认证请求的关联。
  • PKCE 将 authorization code 绑定到持有 code_verifier 的客户端实例。

实际接入时,通常需要同时使用 state 和 PKCE。采用 OpenID Connect 时,还应按照服务端要求使用并验证 nonce。

注意事项

  • 优先使用 code_challenge_method=S256。即使 callback 已受平台保护,也不要降级为 plain 或省略 PKCE。
  • 每次授权都必须生成新的 code_verifier,不得在不同登录请求之间复用。
  • 移动应用中内置的固定 client_secret 无法可靠保密,因此不能代替 PKCE。
  • ASWebAuthenticationSession 或 Auth Tab 只负责浏览器授权和 callback 交付,不会自动为应用生成、保存或提交 PKCE 参数。OAuth SDK 是否会自动处理这些参数,取决于具体配置。
  • 如果 authorization server 允许 native public client 在没有 PKCE 的情况下兑换 authorization code,那么即使平台 callback 很安全,服务端策略仍不符合 RFC 8252 的建议。
  • 如果 authorization code 由受控后端兑换,并且移动端采用另一套后端代理架构,就需要结合完整的客户端注册、redirect URI 和 token 交换路径重新分析,不能直接套用上述 public native client 流程。

备注:内容仅供参考。