OAuth 2.0 扩展协议之 PKCE
前言
閱讀本文前需要了解 OAuth 2.0 授權(quán)協(xié)議的相關(guān)內(nèi)容, 可以參考我的上一篇文章?OAuth 2.0 的探險之旅[1]。
PKCE 全稱是 Proof Key for Code Exchange, 在2015年發(fā)布, 它是 OAuth 2.0 核心的一個擴展協(xié)議, 所以可以和現(xiàn)有的授權(quán)模式結(jié)合使用,比如 Authorization Code + PKCE, 這也是最佳實踐,PKCE 最初是為移動設(shè)備應(yīng)用和本地應(yīng)用創(chuàng)建的, 主要是為了減少公共客戶端的授權(quán)碼攔截攻擊。
在最新的 OAuth 2.1 規(guī)范中(草案), 推薦所有客戶端都使用 PKCE, 而不僅僅是公共客戶端, 并且移除了 Implicit 隱式和 Password 模式, 那之前使用這兩種模式的客戶端怎么辦? 是的, 您現(xiàn)在都可以嘗試使用 Authorization Code + PKCE 的授權(quán)模式。那 PKCE 為什么有這種魔力呢? 實際上它的原理是客戶端提供一個自創(chuàng)建的證明給授權(quán)服務(wù)器, 授權(quán)服務(wù)器通過它來驗證客戶端,把訪問令牌(access_token) 頒發(fā)給真實的客戶端而不是偽造的。
客戶端類型
上面說到了 PKCE 主要是為了減少公共客戶端的授權(quán)碼攔截攻擊, 那就有必要介紹下兩種客戶端類型了。
OAuth 2.0 核心規(guī)范定義了兩種客戶端類型, confidential 機密的, 和 public 公開的, 區(qū)分這兩種類型的方法是, 判斷這個客戶端是否有能力維護自己的機密性憑據(jù) client_secret。
?confidential
對于一個普通的web站點來說,雖然用戶可以訪問到前端頁面, 但是數(shù)據(jù)都來自服務(wù)器的后端api服務(wù), 前端只是獲取授權(quán)碼code, 通過 code 換取access_token 這一步是在后端的api完成的, 由于是內(nèi)部的服務(wù)器, 客戶端有能力維護密碼或者密鑰信息, 這種是機密的的客戶端。
?public
客戶端本身沒有能力保存密鑰信息, 比如桌面軟件, 手機App, 單頁面程序(SPA), 因為這些應(yīng)用是發(fā)布出去的, 實際上也就沒有安全可言, 惡意攻擊者可以通過反編譯等手段查看到客戶端的密鑰, 這種是公開的客戶端。
在 OAuth 2.0 授權(quán)碼模式(Authorization Code)中, 客戶端通過授權(quán)碼code向授權(quán)服務(wù)器獲取訪問令牌(access_token) 時,同時還需要在請求中攜帶客戶端密鑰(client_secret), 授權(quán)服務(wù)器對其進行驗證, 保證 access_token 頒發(fā)給了合法的客戶端, 對于公開的客戶端來說, 本身就有密鑰泄露的風險, 所以就不能使用常規(guī) OAuth 2.0 的授權(quán)碼模式, 于是就針對這種不能使用 client_secret 的場景, 衍生出了 Implicit 隱式模式, 這種模式從一開始就是不安全的。在經(jīng)過一段時間之后, PKCE 擴展協(xié)議推出, 就是為了解決公開客戶端的授權(quán)安全問題。
授權(quán)碼攔截攻擊
上面是OAuth 2.0 授權(quán)碼模式的完整流程, 授權(quán)碼攔截攻擊就是圖中的C步驟發(fā)生的, 也就是授權(quán)服務(wù)器返回給客戶端授權(quán)碼的時候, 這么多步驟中為什么 C 步驟是不安全的呢? 在 OAuth 2.0 核心規(guī)范中, 要求授權(quán)服務(wù)器的 anthorize endpoint 和 token endpoint 必須使用 TLS(安全傳輸層協(xié)議)保護, 但是授權(quán)服務(wù)器攜帶授權(quán)碼code返回到客戶端的回調(diào)地址時, 有可能不受TLS 的保護, 惡意程序就可以在這個過程中攔截授權(quán)碼code, 拿到 code 之后, 接下來就是通過 code 向授權(quán)服務(wù)器換取訪問令牌 access_token , 對于機密的客戶端來說, 請求 access_token 時需要攜帶客戶端的密鑰 client_secret , 而密鑰保存在后端服務(wù)器上, 所以惡意程序通過攔截拿到授權(quán)碼code 也沒有用, 而對于公開的客戶端(手機App, 桌面應(yīng)用)來說, 本身沒有能力保護 client_secret, 因為可以通過反編譯等手段, 拿到客戶端 client_secret, 也就可以通過授權(quán)碼 code 換取 access_token, 到這一步,惡意應(yīng)用就可以拿著 token 請求資源服務(wù)器了。
state 參數(shù), 在 OAuth 2.0 核心協(xié)議中, 通過 code 換取 token 步驟中, 推薦使用 state 參數(shù), 把請求和響應(yīng)關(guān)聯(lián)起來, 可以防止跨站點請求偽造-CSRF攻擊, 但是 state 并不能防止上面的授權(quán)碼攔截攻擊,因為請求和響應(yīng)并沒有被偽造, 而是響應(yīng)的授權(quán)碼被惡意程序攔截。
PKCE 協(xié)議流程
PKCE 協(xié)議本身是對 OAuth 2.0 的擴展, 它和之前的授權(quán)碼流程大體上是一致的, 區(qū)別在于, 在向授權(quán)服務(wù)器的 authorize endpoint 請求時,需要額外的?code_challenge?和?code_challenge_method?參數(shù), 向 token endpoint 請求時, 需要額外的?code_verifier?參數(shù), 最后授權(quán)服務(wù)器會對這三個參數(shù)進行對比驗證, 通過后頒發(fā)令牌。
code_verifier
對于每一個OAuth 授權(quán)請求, 客戶端會先創(chuàng)建一個代碼驗證器 code_verifier, 這是一個高熵加密的隨機字符串, 使用URI 非保留字符 (Unreserved characters), 范圍?[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", 因為非保留字符在傳遞時不需要進行 URL 編碼, 并且 code_verifier 的長度最小是 43, 最大是 128, code_verifier 要具有足夠的熵它是難以猜測的。
code_verifier 的擴充巴科斯范式 (ABNF) 如下:
code-verifier = 43*128unreservedunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"ALPHA = %x41-5A / %x61-7ADIGIT = %x30-39簡單點說就是在?[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"?范圍內(nèi),生成43-128位的隨機字符串。
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
對 code_verifier 進行轉(zhuǎn)換的方法, 這個參數(shù)會傳給授權(quán)服務(wù)器, 并且授權(quán)服務(wù)器會記住這個參數(shù), 頒發(fā)令牌的時候進行對比,?code_challenge == code_challenge_method(code_verifier)?, 若一致則頒發(fā)令牌。
code_challenge_method 可以設(shè)置為 plain (原始值) 或者 S256 (sha256哈希)。
code_challenge
使用 code_challenge_method 對 code_verifier 進行轉(zhuǎn)換得到 code_challenge, 可以使用下面的方式進行轉(zhuǎn)換
?plain
code_challenge = code_verifier
?S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
客戶端應(yīng)該首先考慮使用 S256 進行轉(zhuǎn)換, 如果不支持,才使用 plain , 此時 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));原理分析
上面我們說了授權(quán)碼攔截攻擊, 它是指在整個授權(quán)流程中, 只需要攔截到從授權(quán)服務(wù)器回調(diào)給客戶端的授權(quán)碼 code, 就可以去授權(quán)服務(wù)器申請令牌了, 因為客戶端是公開的, 就算有密鑰 client_secret 也是形同虛設(shè), 惡意程序拿到訪問令牌后, 就可以光明正大的請求資源服務(wù)器了。
PKCE 是怎么做的呢? 既然固定的 client_secret 是不安全的, 那就每次請求生成一個隨機的密鑰(code_verifier), 第一次請求到授權(quán)服務(wù)器的 authorize endpoint時, 攜帶 code_challenge 和 code_challenge_method, 也就是 code_verifier 轉(zhuǎn)換后的值和轉(zhuǎn)換方法, 然后授權(quán)服務(wù)器需要把這兩個參數(shù)緩存起來, 第二次請求到 token endpoint 時, 攜帶生成的隨機密鑰的原始值 (code_verifier) , 然后授權(quán)服務(wù)器使用下面的方法進行驗證:
?plain
code_challenge = code_verifier
?S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
通過后才頒發(fā)令牌, 那向授權(quán)服務(wù)器 authorize endpoint 和 token endpoint 發(fā)起的這兩次請求,該如何關(guān)聯(lián)起來呢? 通過 授權(quán)碼 code 即可, 所以就算惡意程序攔截到了授權(quán)碼 code, 但是沒有 code_verifier, 也是不能獲取訪問令牌的, 當然 PKCE 也可以用在機密(confidential)的客戶端, 那就是 client_secret + code_verifier 雙重密鑰了。
最后看一下請求參數(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)過程
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)注微信公眾號【全球技術(shù)精選】
總結(jié)
以上是生活随笔為你收集整理的OAuth 2.0 扩展协议之 PKCE的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 生活在任务栏的猫, CPU使用率越高它就
- 下一篇: .NET 6新特性试用 | record