大家好,我是本期的實驗室研究員——等天黑。今天我們的研究物件是OAuth擴充套件協議PKCE,其中在OAuth 2.1草案中, 推薦使用Authorization Code + PKCE的授權模式, PKCE為什麼如此重要? 接下來就讓我們一起到實驗室中一探究竟吧!
前言
PKCE 全稱是Proof Key for Code Exchange,在2015年釋出,它是OAuth 2.0核心的一個擴充套件協議,所以可以和現有的授權模式結合使用,比如Authorization Code + PKCE,這也是最佳實踐,PKCE最初是為移動裝置應用和本地應用建立的,主要是為了減少公共客戶端的授權碼攔截攻擊。
在最新的OAuth 2.1規範中(草案),推薦所有客戶端都使用 PKCE, 而不僅僅是公共客戶端, 並且移除了Implicit隱式和Password模式,那之前使用這兩種模式的客戶端怎麼辦? 是的,您現在都可以嘗試使用Authorization Code + PKCE的授權模式。那 PKCE為什麼有這種魔力呢? 實際上它的原理是客戶端提供一個自建立的證明給授權伺服器,授權伺服器通過它來驗證客戶端,把訪問令牌(access_token) 頒發給真實的客戶端而不是偽造的。
客戶端型別
上面說到了PKCE主要是為了減少公共客戶端的授權碼攔截攻擊,那就有必要介紹下兩種客戶端型別了。
OAuth 2.0 核心規範定義了兩種客戶端型別,confidential機密的,和public公開的,區分這兩種型別的方法是,判斷這個客戶端是否有能力維護自己的機密性憑據client_secret。
- confidential
對於一個普通的web站點來說,雖然使用者可以訪問到前端頁面,但是資料都來自伺服器的後端api服務,前端只是獲取授權碼code,通過code換取access_token這一步是在後端的api完成的,由於是內部的伺服器,客戶端有能力維護密碼或者金鑰資訊,這種是機密的的客戶端。 - public
客戶端本身沒有能力儲存金鑰資訊,比如桌面軟體,手機App,單頁面程式(SPA),因為這些應用是釋出出去的,實際上也就沒有安全可言,惡意攻擊者可以通過反編譯等手段檢視到客戶端的金鑰,這種是公開的客戶端。
在OAuth 2.0授權碼模式(Authorization Code)中,客戶端通過授權碼code向授權伺服器獲取訪問令牌(access_token) 時,同時還需要在請求中攜帶客戶端金鑰(client_secret),授權伺服器對其進行驗證,保證access_token頒發給了合法的客戶端,對於公開的客戶端來說,本身就有金鑰洩露的風險,所以就不能使用常規OAuth 2.0的授權碼模式,於是就針對這種不能使用client_secret的場景,衍生出了Implicit隱式模式,這種模式從一開始就是不安全的。在經過一段時間之後,PKCE擴充套件協議推出,就是為了解決公開客戶端的授權安全問題。
授權碼攔截攻擊
上面是OAuth 2.0授權碼模式的完整流程,授權碼攔截攻擊就是圖中的C步驟發生的,也就是授權伺服器返回給客戶端授權碼的時候,這麼多步驟中為什麼C步驟是不安全的呢? 在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並不能防止上面的授權碼攔截攻擊,因為請求和響應並沒有被偽造,而是響應的授權碼被惡意程式攔截。
PKCE 協議流程
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要具有足夠的熵它是難以猜測的。
code_verifier的擴充巴科斯正規化 (ABNF) 如下:
code-verifier = 43*128unreserved
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
ALPHA = %x41-5A / %x61-7A
DIGIT = %x30-39
簡單點說就是在[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
範圍內,生成43-128位的隨機字串。
javascript 示例
// 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));
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進行轉換的方法,這個引數會傳給授權伺服器,並且授權伺服器會記住這個引數,頒發令牌的時候進行對比,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
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
客戶端應該首先考慮使用S256進行轉換,如果不支援,才使用plain,此時 code_challenge和code_verifier的值相等。
javascript 示例
// Required: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function 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;
}
[InternetShortcut]
URL=https://segmentfault.com/a/1190000041093435/edit###
string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));
原理分析
上面我們說了授權碼攔截攻擊,它是指在整個授權流程中,只需要攔截到從授權伺服器回撥給客戶端的授權碼 code,就可以去授權伺服器申請令牌了,因為客戶端是公開的,就算有金鑰 client_secret 也是形同虛設,惡意程式拿到訪問令牌後,就可以光明正大的請求資源伺服器了。
PKCE是怎麼做的呢? 既然固定的client_secret是不安全的,那就每次請求生成一個隨機的金鑰(code_verifier),第一次請求到授權伺服器的authorize endpoint時, 攜帶code_challenge 和 code_challenge_method,也就是 code_verifier 轉換後的值和轉換方法,然後授權伺服器需要把這兩個引數快取起來,第二次請求到 token endpoint 時,攜帶生成的隨機金鑰的原始值 (code_verifier) ,然後授權伺服器使用下面的方法進行驗證:
- plain
code_challenge = code_verifier - S256
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
通過後才頒發令牌,那向授權伺服器authorize endpoint和token endpoint發起的這兩次請求,該如何關聯起來呢? 通過授權碼code即可,所以就算惡意程式攔截到了授權碼code,但是沒有code_verifier,也是不能獲取訪問令牌的,當然PKCE也可以用在機密(confidential)的客戶端,那就是client_secret + code_verifier雙重金鑰了。
最後看一下請求引數的示例:
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_krhUE2M0lOIH3jcxaZzfs5J4jtai5hOX4
POST /oauth2/token
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
https://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模式的授權過程。
參考文獻
- https://www.rfc-editor.org/rf...
- https://www.rfc-editor.org/rf...
- https://oauth.net/2/pkce
- https://datatracker.ietf.org/...
微軟最有價值專家(MVP)
微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。28年來,世界各地的技術社群領導者,因其線上上和線下的技術社群中分享專業知識和經驗而獲得此獎項。
MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社群投入極大的熱情並樂於助人的專家。MVP致力於通過演講、論壇問答、建立網站、撰寫部落格、分享視訊、開源專案、組織會議等方式來幫助他人,並最大程度地幫助微軟技術社群使用者使用Microsoft技術。
更多詳情請登入官方網站:
https://mvp.microsoft.com/zh-cn
歡迎關注微軟中國MSDN訂閱號,獲取更多最新發布!