• home > tools > cloudServices > Azure >

    OAuth 2.0 扩展协议之PKCE

    Author:zhoulujun Date:

    OAuth 2 0中O是如何做到的?在早些年代,网站之间互不连接。现在都是比如QQ 微信可以登录知乎、掘金、豆瓣等,国外的网站基本可以MSN、谷

    OAuth 2.0中O是如何做到的?

    在早些年代,网站之间互不连接。现在都是比如QQ/微信可以登录知乎、掘金、豆瓣等,国外的网站基本可以MSN、谷歌、Facebook的账号登录。

    通过 OAuth 2.0 开放互联的新世界里,经过 3 次修改,OAuth 2.0 (先是 1.0,后来是 1.0a)诞生了,其中的 O 就是OPEN(开放)互联的意思

    在《单点登录之身份验证与授权:CAS/OAuth/SAML/OpenID 》,OAuth 用于访问授权,而 SAML 和 OpenID Connect 用于用户认证

    OAuth2本身是一个授权框架,它并没有对用户的认证流程做出定义。它的初衷是解决不同服务之间的授权访问问题,它无法明确你认为正确的接收者就是那个接收者。目前只有OAuth2的扩展协议OIDC 1.0才具有用户认证功能。

    OIDC 在 OAuth 2.0 的基础上增加了很薄的一层,即引进了一种新的令牌:身份令牌, 使用 jwt 格式存储了经过验证的用户身份信息。这将开放互联提升了一个高度,并且为单点登录打开了一扇门(单点登录除了 OIDC 协议,还有 SAML 协议等)。

    OAuth 2.0 以及 OIDC 使用了一系列的流程来管理客户端应用、授权服务器和资源服务器之间的交互。比如微信授权登录知乎的例子,就是一个授权码流程,就是从浏览器触发,并且像这样来工作:

    v2-10353cb32d7238134f986c549886066c_b.webp

    注意以上流程图中的第 8 步,知乎不仅要带上授权码,还要带上事先由微信分配的秘密凭据,这是知乎网站的后端服务器才能获取到的:

    v2-418b8cc330de2fb8a1143878387706c5_b.webp

    以上的流程对传统的网站应用来说是完美的,但是对于单页应用来说,没有服务端的支持,就没有安全地存储这种秘密凭据的方式。如果存储在客户端,流程上虽然可以工作,但是任何人都能通过浏览器查看源码从而获取到这种秘密信息。在早些的 OAuth 2.0 时代,由于没有更好的选择,就发明了隐式许可流程来从授权服务器处获取访问令牌。这是一种不安全的方式,在 OAuth 2.1 中被废弃,并提供了 PKCE 这个更好的做法来代理隐式许可。

    PKCE 为什么安全?

    PKCE 全称是 Proof Key for Code Exchange,PKCE 是一种用于增强授权码模式安全性的方法,它可以防止恶意应用程序通过截获授权码和重定向 URI 来获得访问令牌。PKCE 通过将随机字符串(code_verifier)和其 SHA-256 哈希值(code_challenge)与授权请求一起发送,确保访问令牌只能由具有相应 code_verifier 的应用程序使用,保障用户的安全性

     PKCE 在2015年发布, 它是 OAuth 2.0 核心的一个扩展协议, 所以可以和现有的授权模式结合使用,比如 Authorization Code + PKCE, 这也是最佳实践,PKCE 最初是为移动设备应用和本地应用创建的, 主要是为了减少公共客户端的授权码拦截攻击

    在最新的 OAuth 2.1 规范中(草案), 推荐所有客户端都使用 PKCE, 而不仅仅是公共客户端, 并且移除了 Implicit 隐式和 Password 模式, 

    OAuth 2.0两种客户端类型

    OAuth 2.0 核心规范定义了两种客户端类型, confidential 机密的, 和 public 公开的, 区分这两种类型的方法是, 判断这个客户端是否有能力维护自己的机密性凭据 client_secret。

    Confidential Clients 机密型应用

    能够安全的存储凭证(client_secret),例如后端服务,可以理解为机密性应用

    对于一个普通的web站点来说,虽然用户可以访问到前端页面, 但是数据都来自服务器的后端api服务, 前端只是获取授权码code, 通过 code 换取access_token 这一步是在后端的api完成的, 由于是内部的服务器, 客户端有能力维护密码或者密钥信息, 这种是机密的的客户端。

    Public Clients 公共客户端

    无法安全存储凭证(client secrets),例如Windows 应用程序、移动端,

    客户端本身没有能力保存密钥信息, 比如桌面软件, 手机App, 单页面程序(SPA), 因为这些应用是发布出去的, 实际上也就没有安全可言, 恶意攻击者可以通过反编译等手段查看到客户端的密钥, 这种是公开的客户端。

    授权码模式 Authorization Code Grand

    在 OAuth 2.0 授权码模式(Authorization Code)中, 客户端通过授权码code向授权服务器获取访问令牌(access_token) 时,同时还需要在请求中携带客户端密钥(client_secret), 授权服务器对其进行验证, 保证 access_token 颁发给了合法的客户端。

    对于公开的客户端来说, 本身就有密钥泄露的风险, 所以就不能使用常规 OAuth 2.0 的授权码模式, 于是就针对这种不能使用 client_secret 的场景, 衍生出了 Implicit 隐式模式, 这种模式从一开始就是不安全的。在经过一段时间之后, PKCE 扩展协议推出, 就是为了解决公开客户端的授权安全问题

    授权码拦截攻击

    v2-273a8c4ec33089fddef63646ffe31149_r.jpg

    上面是OAuth 2.0 授权码模式的完整流程, 授权码拦截攻击就是图中的C步骤发生的, 也就是授权服务器返回给客户端授权码的时候,再简化下

    v2-a7ac81ffa80bd637ea0c4bf6bf6fff1f_720w.webp

    这么多步骤中为什么 C (4)步骤是不安全的呢? 


    在 OAuth 2.0 核心规范中, 要求授权服务器的 anthorize endpoint 和 token endpoint 必须使用 TLS(安全传输层协议)保护, 但是授权服务器携带授权码code返回到客户端的回调地址时, 有可能不受TLS 的保护, 恶意程序就可以在这个过程中拦截授权码code, 拿到 code 之后, 接下来就是通过 code 向授权服务器换取访问令牌 access_token , 对于机密的客户端来说, 请求 access_token 时需要携带客户端的密钥 client_secret , 而密钥保存在后端服务器上, 所以恶意程序通过拦截拿到授权码code 也没有用, 而对于公开的客户端(手机App, 桌面应用)来说, 本身没有能力保护 client_secret, 因为可以通过反编译等手段, 拿到客户端 client_secret, 也就可以通过授权码 code 换取 access_token, 到这一步,恶意应用就可以拿着 token 请求资源服务器了

    state 参数, 在 OAuth 2.0 核心协议中, 通过 code 换取 token 步骤中, 推荐使用 state 参数, 把请求和响应关联起来, 可以防止跨站点请求伪造-CSRF攻击, 但是 state 并不能防止上面的授权码拦截攻击,因为请求和响应并没有被伪造, 而是响应的授权码被恶意程序拦截。

    v2-1b799816cb75c22566e8ad13c0aa14e2_r.jpg

    PKCE 协议流程

    v2-c2c2df449829cc99a2e32824961140ed_720w.webp

    PKCE 协议本身是对 OAuth 2.0 的扩展, 它和之前的授权码流程大体上是一致的, 区别在于, 在向授权服务器的 authorize endpoint 请求时,需要额外的 code_challenge 和 code_challenge_method 参数, 向 token endpoint 请求时, 需要额外的 code_verifier 参数, 最后授权服务器会对这三个参数进行对比验证, 通过后颁发令牌。

    code_verifier

    对于每一个OAuth 授权请求, 客户端会先创建一个代码验证器 code_verifier, 这是一个高熵加密的随机字符串, 使用URI 非保留字符 (Unreserved characters), 范围 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", 因为非保留字符在传递时不需要进行 URL 编码, 并且 code_verifier 的长度最小是 43, 最大是 128, code_verifier 要具有足够的熵它是难以猜测的。

    简单点说就是在 [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 范围内,生成43-128位的随机字符串。

    // Required: Node.js crypto module
    // https://nodejs.org/api/crypto.html#crypto_crypto
    function base64URLEncode(str) {
        return str.toString('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }
    var verifier = base64URLEncode(crypto.randomBytes(32));


    code_challenge_method

    对 code_verifier 进行转换的方法, 这个参数会传给授权服务器, 并且授权服务器会记住这个参数, 颁发令牌的时候进行对比, code_challenge == code_challenge_method(code_verifier) , 若一致则颁发令牌。

    code_challenge_method 可以设置为 plain (原始值) 或者 S256 (sha256哈希)。

    code_challenge

    使用 code_challenge_method 对 code_verifier 进行转换得到 code_challenge, 可以使用下面的方式进行转换

    • plain(未加密或未散列的原始数据)  code_challenge = code_verifier

    • S256(SHA-256算法)  code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

    客户端应该首先考虑使用 S256 进行转换, 如果不支持,才使用 plain , 此时 code_challenge 和 code_verifier 的值相等。


    授权码 + PKCE 模式(Authorization Code With PKCE)

    v2-97430212cf46b0e6ae5fac719d61d212_720w.webp

    • 用户点击登录。

    • 在你的应用中,生成 code_verifier 和 code_challenge。

    • 拼接登录链接(包含 code_challenge ) 跳转到 Authing 请求认证。

    • Authing 发现用户没有登录,重定向到认证页面,要求用户完成认证。

    • 用户在浏览器完成认证。

    • Authing 服务器通过浏览器通过重定向将授权码(code)发送到你的应用前端。

    • 你的应用将授权码 (code) 和 code_verifier 发送到 Authing 请求获取 Token.

    • Authing 校验 code、code_verifier 和 code_challenge。

    • 校验通过,Authing 则返回 AccessToken 和 IdToken 以及可选的 RefreshToken。

    • 你的应用现在知道了用户的身份,后续使用 AccessToken 换取用户信息,调用资源方的 API 等



    参考文章:

    抛弃隐式许可,拥抱 PKCE https://www.zhihu.com/tardis/zm/art/652900498?source_id=1005

    OAuth 2.0 扩展协议之 PKCE https://www.cnblogs.com/myshowtime/p/15555538.html

    授权码 + PKCE 模式|OIDC & OAuth2.0 认证协议最佳实践系列【03】 https://zhuanlan.zhihu.com/p/625423020

    PKCE 在 OIDC 中如何保护客户端免受第三方拦截 https://zhuanlan.zhihu.com/p/715470100



    转载本站文章《OAuth 2.0 扩展协议之PKCE》,
    请注明出处:https://www.zhoulujun.cn/html/tools/cloudServices/Azure/9294.html