一、認識 OAuth 2.0
1.1 OAuth 2.0 應用場景
OAuth 2.0 標准目前被廣泛應用在第三方登錄場景中,以下是虛擬出來的角色,闡述 OAuth2 能幫我們干什么,引用阮一峰這篇理解OAuth 2.0中的例子:
有一個"雲沖印"的網站,可以將用戶儲存在Google的照片,沖印出來。用戶為了使用該服務,必須讓"雲沖印"讀取自己儲存在Google上的照片。
問題是只有得到用戶的授權,Google才會同意"雲沖印"讀取這些照片。那么,"雲沖印"怎樣獲得用戶的授權呢?
傳統方法是,用戶將自己的Google用戶名和密碼,告訴"雲沖印",后者就可以讀取用戶的照片了。這樣的做法有以下幾個嚴重的缺點。
(1)"雲沖印"為了后續的服務,會保存用戶的密碼,這樣很不安全。
(2)Google不得不部署密碼登錄,而我們知道,單純的密碼登錄並不安全。
(3)"雲沖印"擁有了獲取用戶儲存在Google所有資料的權力,用戶沒法限制"雲沖印"獲得授權的范圍和有效期。
(4)用戶只有修改密碼,才能收回賦予"雲沖印"的權力。但是這樣做,會使得其他所有獲得用戶授權的第三方應用程序全部失效。
(5)只要有一個第三方應用程序被破解,就會導致用戶密碼泄漏,以及所有被密碼保護的數據泄漏。
1.2 名詞概念
OAuth 就是為了解決上面這些問題而誕生的。在詳解 OAuth 之前,需要明確一些基本的概念,從上面場景中抽象出以下概念。
第三方應用程序
Third-party application:第三方應用程序,本文中又稱"客戶端"(client),即上一節例子中的"雲沖印"。
HTTP服務提供商
HTTP service:HTTP服務提供商,本文中簡稱"服務提供商",即上一節例子中的Google。
資源所有者
Resource Owner:資源所有者,本文中又稱"用戶"(user)。
用戶代理
User Agent:用戶代理,本文中就是指瀏覽器。
認證服務器
Authorization server:認證服務器,即服務提供商專門用來處理認證的服務器。
資源服務器
Resource server:資源服務器,即服務提供商存放用戶生成的資源的服務器。它與認證服務器,可以是同一台服務器,也可以是不同的服務器。
知道了上面這些名詞,就不難理解,OAuth的作用就是讓"客戶端"安全可控地獲取"用戶"的授權,從而可以和"服務商提供商"進行互動。
二、OAuth 的授權認證流程
2.1 認證思路
OAuth 在"客戶端"與"服務提供商"之間,設置了一個 授權層(authorization layer)。"客戶端"不能直接登錄"服務提供商",只能登錄授權層,以此將用戶與客戶端區分開來。"客戶端"登錄授權層所用的令牌(token),與用戶的密碼不同。用戶可以在登錄的時候,指定授權層令牌的權限范圍和有效期。
"客戶端"登錄授權層以后,"服務提供商"根據令牌的權限范圍和有效期,向"客戶端"開放用戶儲存的資料。
2.2 認證流程
官方 RFC 6749 文件中的 OAuth 2.0 流程圖有點晦澀,優化了 一下:
- 用戶訪問第三方應用程序(簡稱:客戶端)以后,客戶端要求用戶給予授權。
- 用戶同意給予客戶端授權。
- 客戶端使用第 2 步獲得的授權,向認證服務器申請令牌。
- 認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。
- 客戶端使用令牌,向資源服務器申請獲取資源。
- 資源服務器確認令牌無誤,同意向客戶端開放資源。
上述中的第 2 步 是關鍵,即用戶怎樣才能給於客戶端授權。有了這個授權以后,客戶端就可以獲取令牌,進而憑令牌獲取資源。
三、四種授權模式
上一小節可以得出用戶對客戶端的授權動作是核心,客戶端必須得到用戶的授權(authorization grant),才能獲得令牌(access token)。OAuth 2.0定義了四種授權方式:
3.1 授權碼模式(authorization code)
授權碼(authorization code)方式,指的是第三方應用先申請一個授權碼,然后再用該碼獲取令牌。
3.2 簡化模式(implicit)
有些 Web 應用是純前端應用,沒有后端。這時就不能用上面的方式了,必須將令牌儲存在前端。RFC 6749 就規定了第二種方式,允許直接向前端頒發令牌。這種方式沒有授權碼這個中間步驟,所以稱為(授權碼)"隱藏式"(implicit)。
3.3 密碼模式(resource owner password credentials)
如果你高度信任某個應用,RFC 6749 也允許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌,這種方式稱為"密碼式"(password)。
3.4 客戶端模式(client credentials)
最后一種方式是憑證式(client credentials),適用於沒有前端的命令行應用,即在命令行下請求令牌。
四、授權碼模式詳解
4.1 授權碼模式流程
授權碼模式(authorization code)是功能最完整、流程最嚴密安全的授權模式。它的特點就是通過客戶端的 后台服務器,與"服務提供商"的認證服務器進行互動。
注意這種方式適用於那些有后端的 Web 應用。授權碼通過前端傳送,令牌則是儲存在后端,而且所有與資源服務器的通信都在后端完成。這樣的前后端分離,可以避免令牌泄漏。
授權碼模式流程如下:
- 用戶訪問客戶端,客戶端將用戶導向認證服務器。
- 用戶選擇是否給予客戶端授權。
- 假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
- 客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的 后台服務器 上完成的,對用戶不可見。
- 認證服務器核對了授權碼和重定向URI,確認無誤后,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
從上述的流程描述可知,只有第 2 步需要用戶進行授權操作,之后的流程都是在客戶端的后台和認證服務器后台之前進行"靜默"操作,對於用戶來說是無感知的。
下面是上面這些步驟所需要的參數。
4.2 授權碼模式流程的五個步驟
第 1 步驟
參數說明
第 1 步驟中,客戶端申請認證的URI,包含以下參數:
response_type
:表示授權類型,必選項,此處的值固定為"code"client_id
:表示客戶端的ID,必選項redirect_uri
:表示重定向URI,可選項scope
:表示申請的權限范圍,可選項state
:表示客戶端的當前狀態,可以指定任意值,認證服務器會原封不動地返回這個值。
示例
A 網站提供一個鏈接,用戶點擊后就會跳轉到 B 網站,授權用戶數據給 A 網站使用。下面就是 A 網站跳轉 B 網站的一個示意鏈接:
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面 URL 中:
response_type
參數表示要求返回授權碼(code
);
client_id
參數讓 B 網站知道是誰在請求;
redirect_uri
參數是 B 網站接受或拒絕請求后的跳轉網址;
scope
參數表示要求的授權范圍(這里是只讀)。
第 2 步驟
第 2 步驟中,用戶跳轉后,B 網站會要求用戶登錄,然后詢問是否同意給予 A 網站授權。
第 3 步驟
參數說明
第 3 步驟中,服務器回應客戶端的URI,包含以下參數:
code
:表示授權碼,必選項。該碼的有效期應該很短,通常設為10分鍾,客戶端只能使用該碼一次,否則會被授權服務器拒絕。該碼與客戶端ID和重定向URI,是一一對應關系。state
:如果客戶端的請求中包含這個參數,認證服務器的回應也必須一模一樣包含這個參數。
示例
在第 2 步驟用戶表示同意之后,這時 B 網站就會跳回redirect_uri
參數指定的網址。跳轉時,會傳回一個授權碼,就像下面這樣。
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code
參數就是授權碼。
第 4 步驟
參數說明
第 4 步驟中,客戶端向認證服務器申請令牌的HTTP請求,包含以下參數:
grant_type
:表示使用的授權模式,必選項,此處的值固定為"authorization_code"。code
:表示上一步獲得的授權碼,必選項。redirect_uri
:表示重定向URI,必選項,且必須與A步驟中的該參數值保持一致。client_id
:表示客戶端ID,必選項。
示例
在第 3 步驟中,A 網站拿到授權碼以后,就可以在后端,向 B 網站請求令牌。
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
上面 URL 中:
client_id
參數和client_secret
參數用來讓 B 確認 A 的身份(client_secret
參數是保密的,因此只能在后端發請求);
grant_type
參數的值是AUTHORIZATION_CODE
,表示采用的授權方式是授權碼;
code
參數是上一步拿到的授權碼;
redirect_uri
參數是令牌頒發后的回調網址。
第 5 步驟
參數說明
第 5 步驟中,認證服務器發送的HTTP回復,包含以下參數:
access_token
:表示訪問令牌,必選項。token_type
:表示令牌類型,該值大小寫不敏感,必選項,可以是bearer類型或mac類型。expires_in
:表示過期時間,單位為秒。如果省略該參數,必須其他方式設置過期時間。refresh_token
:表示更新令牌,用來獲取下一次的訪問令牌,可選項。scope
:表示權限范圍,如果與客戶端申請的范圍一致,此項可省略。
示例
第 4 步驟中,B 網站收到請求以后,就會頒發令牌。具體做法是向redirect_uri
指定的網址,發送一段 JSON 數據:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
上面 JSON 數據中,access_token
字段就是令牌,A 網站在后端拿到了。注意:HTTP頭信息中明確指定不得緩存。
五、令牌(Token)傳遞方式
當客戶端(第三方應用程序)拿到訪問資源服務器的令牌時,便可以使用這個令牌進行資源訪問了。
在第三方應用程序拿到access_token
后,如何發送給資源服務器這個問題並沒有在 RFC6729 文件中定義,而是作為一個單獨的 RFC6750 文件中獨立定義了。這里做以下簡單的介紹,主要有三種方式如下:
- URI Query Parameter
- Authorization Request Header Field
- Form-Encoded Body Parameter
5.1 請求頭參數傳遞
Authorization Request Header Field,因為在HTTP應用層協議中,專門有定義一個授權使用的Request Header,所以也可以使用這種方式:
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
其中"Bearer "是固定的在access_token前面的頭部信息。
5.2 表單編碼傳遞
使用 Request Body 這種方式,有一個額外的要求,就是 Request Header 的Content-Type
必須是固定的application/x-www-form-urlencoded
,此外還有一個限制就是 不可以使用 GET 訪問,這個好理解,畢竟 GET 請求是不能攜帶 Request Body 的。
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
access_token=mF_9.B5f-4.1JqM
5.3 URI 請求參數傳遞
URI Query Parameter,這種使用途徑應該是最常見的一種方式,非常簡單,比如:
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com
在我們請求受保護的資源的 Url 后面追加一個 access_token 的參數即可。另外還有一點要求,就是 Client 需要設置以下 Request Header 的 Cache-Control:no-store,用來阻止 access_token 不會被 Web 中間件給 log 下來,屬於安全防護方面的一個考慮。
5.4 令牌的刷新
為了防止客戶端使用一個令牌無限次數使用,令牌一般會有過期時間限制,當快要到期時,需要重新獲取令牌,如果再重新走授權碼的授權流程,對用戶體驗非常不好,於是 OAuth 2.0 允許用戶自動更新令牌。
具體方法是,B 網站頒發令牌的時候,一次性頒發兩個令牌,一個用於獲取數據,另一個用於獲取新的令牌(refresh token 字段)。令牌到期前,用戶使用 refresh token 發一個請求,去更新令牌。
https://b.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN
上面 URL 中:
grant_type
參數為refresh_token
表示要求更新令牌,此處的值固定為refresh_token
,必選項;
client_id
參數和client_secret
參數用於確認身份;
refresh_token
參數就是用於更新令牌的令牌。
B 網站驗證通過以后,就會頒發新的令牌。
注意: 第三方應用服務器拿到刷新令牌必須存於服務器,通過后台進行重新獲取新的令牌,以保障刷新令牌的保密性。
六、OAuth2的安全問題
6.1 CSRF攻擊
應用程序在早期使用 OAuth2 的時候爆發過不少相關的安全方面的漏洞,其實仔細分析后會發現大都都是沒有嚴格遵循 OAuth2 的安全相關的指導造成的,相關的漏洞事件自行搜索。
其實 OAuth2 在設計之初是已經做了很多安全方面的考慮,並且在 RFC6749 中加入了一些安全方面的規范指導。比如:
-
要求 Authorization server 進行有效的客戶端驗證;
-
client_serect, access_token, refresh_token, code等敏感信息的安全存儲(不得泄露給第三方)、傳輸通道的安全性(TSL的要求);
-
維持 refresh_token 和第三方應用的綁定,刷新失效機制;
-
維持 Authorization Code 和第三方應用的綁定,這也是state參數為什么是推薦的一點,以防止CSRF攻擊;
-
保證上述各種令牌信息的不可猜測行,以防止被猜測得到;
安全無小事,這方面是要靠各方面(開放平台,第三方開發者)共同防范的。
6.2 攻擊流程
假設有用戶張三,攻擊者李四,第三方"雲沖印"應用(它集成了第三方社交賬號登錄,並且允許用戶將社交賬號和"雲沖印"中的賬號進行綁定),以及 OAuth2 服務提供者 Google。
步驟1
攻擊者李四登錄"雲沖印"網站,並且選擇綁定自己的 Google 賬號
步驟2
"雲沖印"網站將李四重定向到 Google,由於他之前已經登錄過 Google,所以 Google 直接向他顯示是否授權"雲沖印"訪問的頁面。
步驟3
李四在點擊"同意授權"之后,截獲 Google 服務器返回的含有Authorization code
參數的HTTP響應。
步驟4
李四精心構造一個 Web 頁面,它會觸發"雲沖印"網站向 Google 發起令牌申請的請求,而這個請求中的Authorization Code
參數正是上一步截獲到的 code。
步驟5
李四將這個 Web 頁面放到互聯網上,等待或者誘騙受害者張三來訪問。
步驟6
張三之前登錄了"雲沖印"網站,只是沒有把自己的賬號和其他社交賬號綁定起來。在張三訪問了李四准備的這個 Web 頁面,令牌申請流程在張三的瀏覽器里被順利觸發,"雲沖印"網站從 Google 那里獲取到access_token
,但是這個 token 以及通過它進一步獲取到的用戶信息卻都是攻擊者李四的。
步驟7
"雲沖印"網站將李四的 Google 賬號同張三的"雲沖印"賬號關聯綁定起來,從此以后,李四就可以用自己的 Google 賬號通過 OAuth 登錄到張三在 "雲沖印" 網站中的賬號,堂而皇之的冒充張三的身份執行各種操作。
從整體上來看,本次 CSRF 攻擊的時序圖應該是下面這個樣子的:
從上圖中可以看出,造成 CSRF 攻擊漏洞問題的關鍵點在於,OAuth2 的認證流程是分為好幾步來完成的,在上一章節授權碼模式流程中的流程圖中的第 4步驟中,第三方應用在收到一個 GET 請求時,除了能知道當前用戶的 cookie,以及 URL 中的Authorization Code
之外,難以分辨出這個請求到底是用戶本人的意願,還是攻擊者利用用戶的身份偽造出來的請求。
於是,攻擊者就能使用移花接木的手段,提前准備一個含有自己的Authorization Code
的請求,並讓受害者的瀏覽器來接着完成后續的令牌申請流程。
6.3 解決方案
要防止這樣的攻擊其實很容易,作為第三方應用的開發者,只需在 OAuth 認證過程中加入state
參數,並驗證它的參數值即可。具體細節如下:
- 在將用戶重定向到資源認證服務器授權界面的時候,為當前用戶生成一個隨機的字符串,並作為
state
參數加入到URL中,同時存儲一份到 session 中。 - 當第三方應用收到資源服務提供者返回的
Authorization Code
請求的時候,驗證接收到的state
參數值。如果是正確合法的請求,那么此時接收到的參數值應該和上一步提到的為該用戶生成的state
參數值(存於當前用戶的 session 中)完全一致,否則就是異常請求。 state
參數值需要具備下面幾個特性:- 不可預測性:足夠的隨機,使得攻擊者難以猜到正確的參數值
- 關聯性:
state
參數值和當前用戶會話(user session)是相互關聯的 - 唯一性:每個用戶,甚至每次請求生成的
state
參數值都是唯一的 - 時效性:
state
參數一旦被使用則立即失效
state
參數在 OAuth2 認證過程中不是必選參數,因此在早期第三方應用開發者在集成 OAuth2 認證的時候很容易會忽略它的存在,導致應用易受 CSRF 攻擊。所以必須對這個安全問題重視起來。
安全是雙方的,需要第三方應用和資源服務提供商均要嚴格遵守安全規范。如 QQ 互聯的 OAuth2 API 中,state 參數是強制必選的參數,授權接口是基於 HTTPS 的加密通道等;作為第三方開發者在使用消費這些服務的時候也應該重視注意安全中存在的漏洞。
七、OAuth2 參考資料及案例
7.1 參考資料
RFC6749 : The OAuth 2.0 Authorization Framework
7.2 案例
豆瓣OAuth2 API(Authorization Code)
QQ OAuth2 API(Authorization Code)
微信公眾號獲取access_token(Client Credentials Grant)
參考博文: