假设某一个原生客户端(即没有服务端的纯桌面应用程序、安卓、IOS、浏览器插件程序等)有需要接入第三方OAuth2.0的需求(code 授权码模式)。
首先按照OAuth2.0授权码模式的标准,需要按如下顺序工作:
- 这个客户端首先需要请求OAuth提供商的获取code的URL。
- 服务提供商弹出登录页面。
- 用户登录或确认授权。
- 原生客户端截获服务提供商 redirect_uri 地址并获取code。
- 原生客户端使用 code 调用服务提供商的接口换取 token。
那么问题来了,第5步的时候,大家都知道使用 code 换取 token 的接口是需要同时提供 Client ID 和 Client Secret 的,而这两个值如果我们在原生客户端发起换取 token 的网络请求中携带的时候,这两个值很容易被网络不法分子截获而泄漏(常规来说有服务端的应用这个请求是由服务器和服务提供商进行交互的,服务器发起的网络请求不会在互联网上被截获),所以对于纯客户端来说,OAuth的这样使用已经不能满足实际需求了。
科技发展的车轮是不会停止的,所以衍生了 PKCE 授权码模式。
PKCE,全称 Proof Key for Code Exchange。这其实是通过一种密码学手段确保恶意第三方即使截获Authorization Code 或者其他密钥,也无法向认证服务器交换Access Token。
PKCE的流程大概如下:
- 随机生成一串字符并作URL-Safe的Base64编码处理,结果用作
code_verifier
(这个值记录下来后续会用到) - 将这串字符通过SHA256哈希,并用URL-Safe的Base64编码处理,结果用作
code_challenge
- 把
code_challenge
带上,跳转认证服务器,获取 Authorization Code - 把
code_verifier
带上,换取Access Token
由于网络不法分子这样的中间人虽然可以截获 code_challenge,但是他并不能由 code_challenge 逆推 code_verifier,只有客户端自己才知道这两个值。因此即使中间人截获了 code_challenge,Authorization Code 等,也无法换取Access Token,避免了安全问题。
别忘了我们一开始提到的关于 Client ID 和 Client Secret 的问题,所以使用 PKCE 方式我们可以在不提供这两个值的情况下依然达到安全获取 token 的目的。
使用 PKCE 方式请求 code 需要注意多出了两个参数,如下图:
同样,使用 code 换取 token 的接口,也使用了参数 code_verifier
,如下图:
如上2个截图及更多接口参数详解,详见: https://fusionauth.io/docs/v1/tech/oauth/endpoints/#authorize
附:在线生成 PKCE Code Verifier and Code Challenge
客户端代码(JavaScript)
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js"></script>
<script>
function generateCodeVerifier() {
var code_verifier = generateRandomString(32)
document.getElementById("code_verifier").value = code_verifier
}
function generateRandomString(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function generateCodeChallenge(code_verifier) {
return code_challenge = base64URL(CryptoJS.SHA256(code_verifier))
}
function base64URL(string) {
return string.toString(CryptoJS.enc.Base64).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
function submit() {
var code_verifier = document.getElementById("code_verifier").value
var code_challenge = generateCodeChallenge(code_verifier)
document.getElementById("code_challenge").innerHTML = code_challenge
document.getElementById("code_challenge_div").style.display ="block"
}
</script>
</head>
<body>
<div>
<label for="code_verifier">Code Verifier: </label>
<input type="text" id="code_verifier" name="code_verifier" size="38">
</div>
<br>
<div style="display:none" id="code_challenge_div">
Code Challenge:
<span id="code_challenge">
</span>
</div>
<br>
<div>
<button onclick="generateCodeVerifier()">Generate Code Verifier</button>
<button onclick="submit()">Generate Code Challenge</button>
</div>
</body>
</html>
注意:并不是所有OAuth提供商都支持 PKCE 授权码模式,有相关需求时需要跟服务提供商确认。
相关资料:
- RFC 7636 - OAuth PKCE
- RFC 8252 - OAuth 2.0 for Native Apps
- OAuth2.0和OAuth2.1的区别 仔细(一定要仔细)阅读这篇文章中列出的几个区别(全部都是加强安全性的)
- 使用JWT和刷新令牌的授权码方式进行OAuth登录 使用Cookie HttpOnly 特性存储敏感数据(JWT和refresh_token)能提高一定的安全性
最后,在 OAuth 2.1 中,要求 PKCE 是 OAuth 2.1 必须支持的一种方式,所以基于 OAuth 2.1 的提供商我们一般可以认为他就是支持 PKCE 的。
(END)