PKCE (Proof Key for Code Exchange)代码交换的证明密钥
使用授权码授权的 OAuth 2.0 公共客户端容易受到授权码截取攻击。PKCE 是来减轻这种威胁的技术。
公共客户端指的是无法维持其秘钥机密性的客户端。比如手机 APP、桌面应用或者纯浏览器应用(不含服务器,秘钥可网站源码放在一起的),这些应用很可能受到攻击进而导致秘钥的泄露(比如通过反编译、浏览器控制台查看网站的源码等手段)。
授权码授权的流程
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
-
(A)客户端将资源所有者引导到授权端点进行授权,参数有客户端标识
client_id
、请求范围scope
、本地状态state
和重定向URIredirect_uri
等,授权完成后授权服务器将浏览器重定向回该 URL。redirect_uri
可以是自定义的 schame,比如app-name://xxx
,也可以是本地启动一个的服务器,比如http://localhost:{port}/xxx
。 -
(B) 资源拥有者进行登录、授权。
-
(C)授权服务器将浏览器重定向回(在请求时或客户端注册时)提供的重定向 URI,并且带有授权码
code
和之前客户端提供的本地状态state
。 -
(D)客户端使用授权码
code
向授权服务器的令牌端点请求访问令牌,必须包含用于获得授权码的重定向 URI 来用于验证。 -
(E)授权服务器验证客户端的身份,并用接受到的重定向 URI 与(C)中的重定向 URI 验证是否匹配,最终授予访问令牌与可选的刷新令牌。
授权码截取攻击
授权码拦截攻击发送在上述流程中的 (C)中,虽然 OAuth 2.0 规定授权服务器的授权端点、令牌端点都必须是传输层安全(TLS)的。但是授权完成后的重定向地址,可能不是传输层安全的,例如 app-name://xxx
或 http://localhost:{port}/xxx
。攻击者可以拦截授权端点返回的授权代码。同时公共客户端无法维持其秘钥的机密性,可以假设攻击者能拿到 client_id
、client_secret
等。这时攻击者已经拿到了调用令牌端点的所有参数:code、redirect_uri、client_id、client_secret
,可以使用它们来获得访问令牌。
PKCE 协议流程
+-------------------+
| Authz Server |
+--------+ | +---------------+ |
| |--(A)- Authorization Request ---->| | |
| | + t(code_verifier), t_m | | Authorization | |
| | | | Endpoint | |
| |<-(B)---- Authorization Code -----| | |
| | | +---------------+ |
| Client | | |
| | | +---------------+ |
| |--(C)-- Access Token Request ---->| | |
| | + code_verifier | | Token | |
| | | | Endpoint | |
| |<-(D)------ Access Token ---------| | |
+--------+ | +---------------+ |
+-------------------+
-
A. 客户端生成一个随机字符串作为
code_verifier
,使用code_challenge_method
t_m 将code_verifier
转换成code_challenge
t(code_verifier)。然后按照授权码授权的流程中的(A)向授权端点发起请求,只是多了两个参数code_challenge
、code_challenge_method
。 -
B. 授权服务器需要将
code_challenge
、code_challenge_method
、code
、client_id
关联并保存起来,其他与授权码授权的流程中的(B)(C)类似,授权成功后将code
给到客户端。 -
C. 客户端向令牌端点请求访问令牌。除了授权码授权的流程中的(D)的参数外,还要包含 A 中生成的
code_verifier
。 -
D. 授权服务器根据令牌端点的请求参数查询出 B 中存储的
code_challenge
、code_challenge_method
,执行转换code_challenge_method(code_verifier)
得到code_challenge'
,如果code_challenge
与code_challenge'
不相等则拒绝请求。
经过上面的流程后,即使攻击者能拦截到 B 的 code
,也无法使用其兑换访问令牌,因为攻击者没有 code_verifier
。
协议细节
1 客户端创建 Code Verifier
code_verifier 是一个高熵加密的随机字符串,由 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
构成,最小长度为 43 个字符,最大长度为 128 个字符。
2 客户端创建 Code Challenge
以下是两种 code_challenge_method
下的 code_verifier
转换成 code_challenge
的公式:
plain
code_challenge = code_verifier
S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
如果客户端能使用 “S256”,则必须使用 “S256”,因为服务器强制使用 “S256”。只有在客户端不支持 “S256”,且通过外带配置知道服务端支持 “plain” 时,才允许客户端使用 “plain”。
3 客户端通过授权请求发送 Code Challenge
客户端使用以下附加参数将 code challenge 作为 OAuth 2.0 授权请求的一部分发送:
code_challenge
必需。 Code challenge.
code_challenge_method
可选,,若未在请求中指定,默认为 “plain”。 可选 “S256” 或 ”plain”。
4 服务器返回 code
当服务器在授权响应中发出授权码时,它必须将 code_challenge
和 code_challenge_method
的值与授权码关联起来,以便以后可以验证。
通常,code_challenge
和 code_challenge_method
值以加密形式存储在 code
本身中,但也可以存储在与该 code 关联的服务器上。 服务器不得以其他实体可以提取的形式在客户端请求中包含 code_challenge
值。
若客户端未在授权请求中发送 code_challenge
,或者客户端发送了不支持的 code_challenge_method
,授权端点必须返回授权错误响应,error
的值为 invalid_request
,error_description
、error_uri
应说明错误的性质。
5 客户端发送 Code 和 Code Verifier 到令牌端点
客户端向令牌端点发送访问令牌请求时,除了 OAuth 2.0 访问令牌请求中定义的参数外,它还会发送以下参数:
code_verifier
必需. Code verifier
6 服务器在返回访问令牌之前验证 code_verifier
令牌端点收到请求后,服务器会现通过先前关联的 code_challenge_method
将 code_verifier
转换为 code_challenge
,然后将新的 code_challenge
与先前关联的 code_challenge
比较。
如果 code_challenge_method
为 S256:
BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
如果 code_challenge_method
为 plain:
code_verifier == code_challenge
如果两个值不相等,则返回 invalid_grant 的响应。