假设某一个原生客户端(即没有服务端的纯桌面应用程序、安卓、IOS、浏览器插件程序等)有需要接入第三方OAuth2.0的需求(code 授权码模式)。

首先按照OAuth2.0授权码模式的标准,需要按如下顺序工作:

  1. 这个客户端首先需要请求OAuth提供商的获取code的URL。
  2. 服务提供商弹出登录页面。
  3. 用户登录或确认授权。
  4. 原生客户端截获服务提供商 redirect_uri 地址并获取code。
  5. 原生客户端使用 code 调用服务提供商的接口换取 token。

那么问题来了,第5步的时候,大家都知道使用 code 换取 token 的接口是需要同时提供 Client ID 和 Client Secret 的,而这两个值如果我们在原生客户端发起换取 token 的网络请求中携带的时候,这两个值很容易被网络不法分子截获而泄漏(常规来说有服务端的应用这个请求是由服务器和服务提供商进行交互的,服务器发起的网络请求不会在互联网上被截获),所以对于纯客户端来说,OAuth的这样使用已经不能满足实际需求了。

科技发展的车轮是不会停止的,所以衍生了 PKCE 授权码模式

PKCE,全称 Proof Key for Code Exchange。这其实是通过一种密码学手段确保恶意第三方即使截获Authorization Code 或者其他密钥,也无法向认证服务器交换Access Token。

PKCE的流程大概如下:

  1. 随机生成一串字符并作URL-Safe的Base64编码处理,结果用作 code_verifier(这个值记录下来后续会用到)
  2. 将这串字符通过SHA256哈希,并用URL-Safe的Base64编码处理,结果用作 code_challenge
  3. code_challenge 带上,跳转认证服务器,获取 Authorization Code
  4. code_verifier带上,换取Access Token

由于网络不法分子这样的中间人虽然可以截获 code_challenge,但是他并不能由 code_challenge 逆推 code_verifier,只有客户端自己才知道这两个值。因此即使中间人截获了 code_challenge,Authorization Code 等,也无法换取Access Token,避免了安全问题。

别忘了我们一开始提到的关于 Client ID 和 Client Secret 的问题,所以使用 PKCE 方式我们可以在不提供这两个值的情况下依然达到安全获取 token 的目的。

使用 PKCE 方式请求 code 需要注意多出了两个参数,如下图:
基于OAuth的PKCE授权码模式(增强安全)_客户端
同样,使用 code 换取 token 的接口,也使用了参数 code_verifier,如下图:
基于OAuth的PKCE授权码模式(增强安全)_客户端_02

如上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 授权码模式,有相关需求时需要跟服务提供商确认。

相关资料:

最后,在 OAuth 2.1 中,要求 PKCE 是 OAuth 2.1 必须支持的一种方式,所以基于 OAuth 2.1 的提供商我们一般可以认为他就是支持 PKCE 的。


(END)