OAuth 2.0 扩展协议之 PKCE
前言
閱讀本文前需要了解 OAuth 2.0 授權(quán)協(xié)議的相關(guān)內(nèi)容, 可以參考我的上一篇文章?OAuth 2.0 的探險(xiǎn)之旅[1]。
PKCE 全稱(chēng)是 Proof Key for Code Exchange, 在2015年發(fā)布, 它是 OAuth 2.0 核心的一個(gè)擴(kuò)展協(xié)議, 所以可以和現(xiàn)有的授權(quán)模式結(jié)合使用,比如 Authorization Code + PKCE, 這也是最佳實(shí)踐,PKCE 最初是為移動(dòng)設(shè)備應(yīng)用和本地應(yīng)用創(chuàng)建的, 主要是為了減少公共客戶(hù)端的授權(quán)碼攔截攻擊。
在最新的 OAuth 2.1 規(guī)范中(草案), 推薦所有客戶(hù)端都使用 PKCE, 而不僅僅是公共客戶(hù)端, 并且移除了 Implicit 隱式和 Password 模式, 那之前使用這兩種模式的客戶(hù)端怎么辦? 是的, 您現(xiàn)在都可以嘗試使用 Authorization Code + PKCE 的授權(quán)模式。那 PKCE 為什么有這種魔力呢? 實(shí)際上它的原理是客戶(hù)端提供一個(gè)自創(chuàng)建的證明給授權(quán)服務(wù)器, 授權(quán)服務(wù)器通過(guò)它來(lái)驗(yàn)證客戶(hù)端,把訪(fǎng)問(wèn)令牌(access_token) 頒發(fā)給真實(shí)的客戶(hù)端而不是偽造的。
客戶(hù)端類(lèi)型
上面說(shuō)到了 PKCE 主要是為了減少公共客戶(hù)端的授權(quán)碼攔截攻擊, 那就有必要介紹下兩種客戶(hù)端類(lèi)型了。
OAuth 2.0 核心規(guī)范定義了兩種客戶(hù)端類(lèi)型, confidential 機(jī)密的, 和 public 公開(kāi)的, 區(qū)分這兩種類(lèi)型的方法是, 判斷這個(gè)客戶(hù)端是否有能力維護(hù)自己的機(jī)密性憑據(jù) client_secret。
?confidential
對(duì)于一個(gè)普通的web站點(diǎn)來(lái)說(shuō),雖然用戶(hù)可以訪(fǎng)問(wèn)到前端頁(yè)面, 但是數(shù)據(jù)都來(lái)自服務(wù)器的后端api服務(wù), 前端只是獲取授權(quán)碼code, 通過(guò) code 換取access_token 這一步是在后端的api完成的, 由于是內(nèi)部的服務(wù)器, 客戶(hù)端有能力維護(hù)密碼或者密鑰信息, 這種是機(jī)密的的客戶(hù)端。
?public
客戶(hù)端本身沒(méi)有能力保存密鑰信息, 比如桌面軟件, 手機(jī)App, 單頁(yè)面程序(SPA), 因?yàn)檫@些應(yīng)用是發(fā)布出去的, 實(shí)際上也就沒(méi)有安全可言, 惡意攻擊者可以通過(guò)反編譯等手段查看到客戶(hù)端的密鑰, 這種是公開(kāi)的客戶(hù)端。
在 OAuth 2.0 授權(quán)碼模式(Authorization Code)中, 客戶(hù)端通過(guò)授權(quán)碼code向授權(quán)服務(wù)器獲取訪(fǎng)問(wèn)令牌(access_token) 時(shí),同時(shí)還需要在請(qǐng)求中攜帶客戶(hù)端密鑰(client_secret), 授權(quán)服務(wù)器對(duì)其進(jìn)行驗(yàn)證, 保證 access_token 頒發(fā)給了合法的客戶(hù)端, 對(duì)于公開(kāi)的客戶(hù)端來(lái)說(shuō), 本身就有密鑰泄露的風(fēng)險(xiǎn), 所以就不能使用常規(guī) OAuth 2.0 的授權(quán)碼模式, 于是就針對(duì)這種不能使用 client_secret 的場(chǎng)景, 衍生出了 Implicit 隱式模式, 這種模式從一開(kāi)始就是不安全的。在經(jīng)過(guò)一段時(shí)間之后, PKCE 擴(kuò)展協(xié)議推出, 就是為了解決公開(kāi)客戶(hù)端的授權(quán)安全問(wèn)題。
授權(quán)碼攔截攻擊
上面是OAuth 2.0 授權(quán)碼模式的完整流程, 授權(quán)碼攔截攻擊就是圖中的C步驟發(fā)生的, 也就是授權(quán)服務(wù)器返回給客戶(hù)端授權(quán)碼的時(shí)候, 這么多步驟中為什么 C 步驟是不安全的呢? 在 OAuth 2.0 核心規(guī)范中, 要求授權(quán)服務(wù)器的 anthorize endpoint 和 token endpoint 必須使用 TLS(安全傳輸層協(xié)議)保護(hù), 但是授權(quán)服務(wù)器攜帶授權(quán)碼code返回到客戶(hù)端的回調(diào)地址時(shí), 有可能不受TLS 的保護(hù), 惡意程序就可以在這個(gè)過(guò)程中攔截授權(quán)碼code, 拿到 code 之后, 接下來(lái)就是通過(guò) code 向授權(quán)服務(wù)器換取訪(fǎng)問(wèn)令牌 access_token , 對(duì)于機(jī)密的客戶(hù)端來(lái)說(shuō), 請(qǐng)求 access_token 時(shí)需要攜帶客戶(hù)端的密鑰 client_secret , 而密鑰保存在后端服務(wù)器上, 所以惡意程序通過(guò)攔截拿到授權(quán)碼code 也沒(méi)有用, 而對(duì)于公開(kāi)的客戶(hù)端(手機(jī)App, 桌面應(yīng)用)來(lái)說(shuō), 本身沒(méi)有能力保護(hù) client_secret, 因?yàn)榭梢酝ㄟ^(guò)反編譯等手段, 拿到客戶(hù)端 client_secret, 也就可以通過(guò)授權(quán)碼 code 換取 access_token, 到這一步,惡意應(yīng)用就可以拿著 token 請(qǐng)求資源服務(wù)器了。
state 參數(shù), 在 OAuth 2.0 核心協(xié)議中, 通過(guò) code 換取 token 步驟中, 推薦使用 state 參數(shù), 把請(qǐng)求和響應(yīng)關(guān)聯(lián)起來(lái), 可以防止跨站點(diǎn)請(qǐng)求偽造-CSRF攻擊, 但是 state 并不能防止上面的授權(quán)碼攔截攻擊,因?yàn)檎?qǐng)求和響應(yīng)并沒(méi)有被偽造, 而是響應(yīng)的授權(quán)碼被惡意程序攔截。
PKCE 協(xié)議流程
PKCE 協(xié)議本身是對(duì) OAuth 2.0 的擴(kuò)展, 它和之前的授權(quán)碼流程大體上是一致的, 區(qū)別在于, 在向授權(quán)服務(wù)器的 authorize endpoint 請(qǐng)求時(shí),需要額外的?code_challenge?和?code_challenge_method?參數(shù), 向 token endpoint 請(qǐng)求時(shí), 需要額外的?code_verifier?參數(shù), 最后授權(quán)服務(wù)器會(huì)對(duì)這三個(gè)參數(shù)進(jìn)行對(duì)比驗(yàn)證, 通過(guò)后頒發(fā)令牌。
code_verifier
對(duì)于每一個(gè)OAuth 授權(quán)請(qǐng)求, 客戶(hù)端會(huì)先創(chuàng)建一個(gè)代碼驗(yàn)證器 code_verifier, 這是一個(gè)高熵加密的隨機(jī)字符串, 使用URI 非保留字符 (Unreserved characters), 范圍?[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", 因?yàn)榉潜A糇址趥鬟f時(shí)不需要進(jìn)行 URL 編碼, 并且 code_verifier 的長(zhǎng)度最小是 43, 最大是 128, code_verifier 要具有足夠的熵它是難以猜測(cè)的。
code_verifier 的擴(kuò)充巴科斯范式 (ABNF) 如下:
code-verifier = 43*128unreservedunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"ALPHA = %x41-5A / %x61-7ADIGIT = %x30-39簡(jiǎn)單點(diǎn)說(shuō)就是在?[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"?范圍內(nèi),生成43-128位的隨機(jī)字符串。
javascript 示例
// Required: Node.js crypto module// https://nodejs.org/api/crypto.html#crypto_cryptofunction base64URLEncode(str) { return str.toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '');}var verifier = base64URLEncode(crypto.randomBytes(32));java 示例
// Required: Apache Commons Codec// https://commons.apache.org/proper/commons-codec/// Import the Base64 class.// import org.apache.commons.codec.binary.Base64;SecureRandom sr = new SecureRandom();byte[] code = new byte[32];sr.nextBytes(code);String verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(code);c# 示例
public static string randomDataBase64url(int length){ RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); byte[] bytes = new byte[length]; rng.GetBytes(bytes); return base64urlencodeNoPadding(bytes);}public static string base64urlencodeNoPadding(byte[] buffer){ string base64 = Convert.ToBase64String(buffer); base64 = base64.Replace("+", "-"); base64 = base64.Replace("/", "_"); base64 = base64.Replace("=", ""); return base64;}string code_verifier = randomDataBase64url(32);code_challenge_method
對(duì) code_verifier 進(jìn)行轉(zhuǎn)換的方法, 這個(gè)參數(shù)會(huì)傳給授權(quán)服務(wù)器, 并且授權(quán)服務(wù)器會(huì)記住這個(gè)參數(shù), 頒發(fā)令牌的時(shí)候進(jìn)行對(duì)比,?code_challenge == code_challenge_method(code_verifier)?, 若一致則頒發(fā)令牌。
code_challenge_method 可以設(shè)置為 plain (原始值) 或者 S256 (sha256哈希)。
code_challenge
使用 code_challenge_method 對(duì) code_verifier 進(jìn)行轉(zhuǎn)換得到 code_challenge, 可以使用下面的方式進(jìn)行轉(zhuǎn)換
?plain
code_challenge = code_verifier
?S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
客戶(hù)端應(yīng)該首先考慮使用 S256 進(jìn)行轉(zhuǎn)換, 如果不支持,才使用 plain , 此時(shí) code_challenge 和 code_verifier 的值相等。
javascript 示例
// Required: Node.js crypto module// https://nodejs.org/api/crypto.html#crypto_cryptofunction sha256(buffer) { return crypto.createHash('sha256').update(buffer).digest();}var challenge = base64URLEncode(sha256(verifier));java 示例
// Dependency: Apache Commons Codec// https://commons.apache.org/proper/commons-codec/// Import the Base64 class.// import org.apache.commons.codec.binary.Base64;byte[] bytes = verifier.getBytes("US-ASCII");MessageDigest md = MessageDigest.getInstance("SHA-256");md.update(bytes, 0, bytes.length);byte[] digest = md.digest();String challenge = Base64.encodeBase64URLSafeString(digest);C# 示例
public static string base64urlencodeNoPadding(byte[] buffer){ string base64 = Convert.ToBase64String(buffer); base64 = base64.Replace("+", "-"); base64 = base64.Replace("/", "_"); base64 = base64.Replace("=", ""); return base64;}string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));原理分析
上面我們說(shuō)了授權(quán)碼攔截攻擊, 它是指在整個(gè)授權(quán)流程中, 只需要攔截到從授權(quán)服務(wù)器回調(diào)給客戶(hù)端的授權(quán)碼 code, 就可以去授權(quán)服務(wù)器申請(qǐng)令牌了, 因?yàn)榭蛻?hù)端是公開(kāi)的, 就算有密鑰 client_secret 也是形同虛設(shè), 惡意程序拿到訪(fǎng)問(wèn)令牌后, 就可以光明正大的請(qǐng)求資源服務(wù)器了。
PKCE 是怎么做的呢? 既然固定的 client_secret 是不安全的, 那就每次請(qǐng)求生成一個(gè)隨機(jī)的密鑰(code_verifier), 第一次請(qǐng)求到授權(quán)服務(wù)器的 authorize endpoint時(shí), 攜帶 code_challenge 和 code_challenge_method, 也就是 code_verifier 轉(zhuǎn)換后的值和轉(zhuǎn)換方法, 然后授權(quán)服務(wù)器需要把這兩個(gè)參數(shù)緩存起來(lái), 第二次請(qǐng)求到 token endpoint 時(shí), 攜帶生成的隨機(jī)密鑰的原始值 (code_verifier) , 然后授權(quán)服務(wù)器使用下面的方法進(jìn)行驗(yàn)證:
?plain
code_challenge = code_verifier
?S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
通過(guò)后才頒發(fā)令牌, 那向授權(quán)服務(wù)器 authorize endpoint 和 token endpoint 發(fā)起的這兩次請(qǐng)求,該如何關(guān)聯(lián)起來(lái)呢? 通過(guò) 授權(quán)碼 code 即可, 所以就算惡意程序攔截到了授權(quán)碼 code, 但是沒(méi)有 code_verifier, 也是不能獲取訪(fǎng)問(wèn)令牌的, 當(dāng)然 PKCE 也可以用在機(jī)密(confidential)的客戶(hù)端, 那就是 client_secret + code_verifier 雙重密鑰了。
最后看一下請(qǐng)求參數(shù)的示例:
GET /oauth2/authorize https://www.authorization-server.com/oauth2/authorize?response_type=code&client_id=s6BhdRkqt3&scope=user&state=8b815ab1d177f5c8e &redirect_uri=https://www.client.com/callback&code_challenge_method=S256 &code_challenge=FWOeBX6Qw_krhUE2M0lOIH3jcxaZzfs5J4jtai5hOX4POST /oauth2/token Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencodedhttps://www.authorization-server.com/oauth2/token?grant_type=authorization_code&code=d8c2afe6ecca004eb4bd7024&redirect_uri=https://www.client.com/callback&code_verifier=2D9RWc5iTdtejle7GTMzQ9Mg15InNmqk3GZL-Hg5Iz0下邊使用 Postman 演示了使用 PKCE 模式的授權(quán)過(guò)程
References
https://www.rfc-editor.org/rfc/rfc6749
https://www.rfc-editor.org/rfc/rfc7636.html
https://oauth.net/2/pkce
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04
😃 歡迎關(guān)注微信公眾號(hào)【全球技術(shù)精選】
總結(jié)
以上是生活随笔為你收集整理的OAuth 2.0 扩展协议之 PKCE的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 生活在任务栏的猫, CPU使用率越高它就
- 下一篇: 基于事件驱动架构构建微服务第12部分:向