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 请求绑定。两者可以配合使用。
推荐的授权流程
-
每次授权都生成新的高熵
code_verifier,长度须符合 RFC 7636 的要求。 -
计算
code_challenge:code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) -
在 authorization request 中发送以下参数:
response_type=code code_challenge=<calculated value> code_challenge_method=S256 state=<random value> -
使用
ASWebAuthenticationSession或 Chrome Auth Tab 打开授权 URL。 -
收到 callback 后先验证
state,再提取code。 -
调用 token endpoint,提交与这次授权对应的
code_verifier。 -
无论授权成功还是失败,都要删除
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 流程。
备注:内容仅供参考。
