OAuth 2.0 標準目前被普遍應用在第三方登陸場景中,如下是虛擬出來的角色,闡述 OAuth2 能幫咱們幹什麼,引用阮一峯這篇理解OAuth 2.0中的例子:php
有一個"雲沖印"的網站,能夠將用戶儲存在Google的照片,沖印出來。用戶爲了使用該服務,必須讓"雲沖印"讀取本身儲存在Google上的照片。html
問題是隻有獲得用戶的受權,Google纔會贊成"雲沖印"讀取這些照片。那麼,"雲沖印"怎樣得到用戶的受權呢?前端
傳統方法是,用戶將本身的Google用戶名和密碼,告訴"雲沖印",後者就能夠讀取用戶的照片了。這樣的作法有如下幾個嚴重的缺點。git
(1)"雲沖印"爲了後續的服務,會保存用戶的密碼,這樣很不安全。
(2)Google不得不部署密碼登陸,而咱們知道,單純的密碼登陸並不安全。
(3)"雲沖印"擁有了獲取用戶儲存在Google全部資料的權力,用戶無法限制"雲沖印"得到受權的範圍和有效期。
(4)用戶只有修改密碼,才能收回賦予"雲沖印"的權力。可是這樣作,會使得其餘全部得到用戶受權的第三方應用程序所有失效。
(5)只要有一個第三方應用程序被破解,就會致使用戶密碼泄漏,以及全部被密碼保護的數據泄漏。github
OAuth 就是爲了解決上面這些問題而誕生的。在詳解 OAuth 以前,須要明確一些基本的概念,從上面場景中抽象出如下概念。json
第三方應用程序segmentfault
Third-party application:第三方應用程序,本文中又稱"客戶端"(client),即上一節例子中的"雲沖印"。後端
HTTP服務提供商瀏覽器
HTTP service:HTTP服務提供商,本文中簡稱"服務提供商",即上一節例子中的Google。緩存
資源全部者
Resource Owner:資源全部者,本文中又稱"用戶"(user)。
用戶代理
User Agent:用戶代理,本文中就是指瀏覽器。
認證服務器
Authorization server:認證服務器,即服務提供商專門用來處理認證的服務器。
資源服務器
Resource server:資源服務器,即服務提供商存放用戶生成的資源的服務器。它與認證服務器,能夠是同一臺服務器,也能夠是不一樣的服務器。
知道了上面這些名詞,就不難理解,OAuth的做用就是讓"客戶端"安全可控地獲取"用戶"的受權,從而能夠和"服務商提供商"進行互動。
OAuth 在"客戶端"與"服務提供商"之間,設置了一個 受權層(authorization layer)。"客戶端"不能直接登陸"服務提供商",只能登陸受權層,以此將用戶與客戶端區分開來。"客戶端"登陸受權層所用的令牌(token),與用戶的密碼不一樣。用戶能夠在登陸的時候,指定受權層令牌的權限範圍和有效期。
"客戶端"登陸受權層之後,"服務提供商"根據令牌的權限範圍和有效期,向"客戶端"開放用戶儲存的資料。
官方 RFC 6749 文件中的 OAuth 2.0 流程圖有點晦澀,優化了 一下:
- 用戶訪問第三方應用程序(簡稱:客戶端)之後,客戶端要求用戶給予受權。
- 用戶贊成給予客戶端受權。
- 客戶端使用第 2 步得到的受權,向認證服務器申請令牌。
- 認證服務器對客戶端進行認證之後,確認無誤,贊成發放令牌。
- 客戶端使用令牌,向資源服務器申請獲取資源。
- 資源服務器確認令牌無誤,贊成向客戶端開放資源。
上述中的第 2 步 是關鍵,即用戶怎樣才能給於客戶端受權。有了這個受權之後,客戶端就能夠獲取令牌,進而憑令牌獲取資源。
上一小節能夠得出用戶對客戶端的受權動做是核心,客戶端必須獲得用戶的受權(authorization grant),才能得到令牌(access token)。OAuth 2.0定義了四種受權方式:
受權碼(authorization code)方式,指的是第三方應用先申請一個受權碼,而後再用該碼獲取令牌。
有些 Web 應用是純前端應用,沒有後端。這時就不能用上面的方式了,必須將令牌儲存在前端。RFC 6749 就規定了第二種方式,容許直接向前端頒發令牌。這種方式沒有受權碼這個中間步驟,因此稱爲(受權碼)"隱藏式"(implicit)。
若是你高度信任某個應用,RFC 6749 也容許用戶把用戶名和密碼,直接告訴該應用。該應用就使用你的密碼,申請令牌,這種方式稱爲"密碼式"(password)。
最後一種方式是憑證式(client credentials),適用於沒有前端的命令行應用,即在命令行下請求令牌。
受權碼模式(authorization code)是功能最完整、流程最嚴密安全的受權模式。它的特色就是經過客戶端的 後臺服務器,與"服務提供商"的認證服務器進行互動。
注意這種方式適用於那些有後端的 Web 應用。受權碼經過前端傳送,令牌則是儲存在後端,並且全部與資源服務器的通訊都在後端完成。這樣的先後端分離,能夠避免令牌泄漏。
受權碼模式流程以下:
- 用戶訪問客戶端,客戶端將用戶導向認證服務器。
- 用戶選擇是否給予客戶端受權。
- 假設用戶給予受權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個受權碼。
- 客戶端收到受權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的 後臺服務器 上完成的,對用戶不可見。
- 認證服務器覈對了受權碼和重定向URI,確認無誤後,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。
從上述的流程描述可知,只有第 2 步須要用戶進行受權操做,以後的流程都是在客戶端的後臺和認證服務器後臺以前進行"靜默"操做,對於用戶來講是無感知的。
下面是上面這些步驟所須要的參數。
第 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 步驟中,用戶跳轉後,B 網站會要求用戶登陸,而後詢問是否贊成給予 A 網站受權。
第 3 步驟中,服務器迴應客戶端的URI,包含如下參數:
code
:表示受權碼,必選項。該碼的有效期應該很短,一般設爲10分鐘,客戶端只能使用該碼一次,不然會被受權服務器拒絕。該碼與客戶端ID和重定向URI,是一一對應關係。state
:若是客戶端的請求中包含這個參數,認證服務器的迴應也必須如出一轍包含這個參數。在第 2 步驟用戶表示贊成以後,這時 B 網站就會跳回redirect_uri
參數指定的網址。跳轉時,會傳回一個受權碼,就像下面這樣。
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code
參數就是受權碼。
第 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 步驟中,認證服務器發送的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頭信息中明確指定不得緩存。
當客戶端(第三方應用程序)拿到訪問資源服務器的令牌時,即可以使用這個令牌進行資源訪問了。
在第三方應用程序拿到access_token
後,如何發送給資源服務器這個問題並無在 RFC6729 文件中定義,而是做爲一個單獨的 RFC6750 文件中獨立定義了。這裏作如下簡單的介紹,主要有三種方式以下:
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前面的頭部信息。
使用 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
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 下來,屬於安全防禦方面的一個考慮。
爲了防止客戶端使用一個令牌無限次數使用,令牌通常會有過時時間限制,當快要到期時,須要從新獲取令牌,若是再從新走受權碼的受權流程,對用戶體驗很是很差,因而 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 的時候爆發過很多相關的安全方面的漏洞,其實仔細分析後會發現大都都是沒有嚴格遵循 OAuth2 的安全相關的指導形成的,相關的漏洞事件自行搜索。
其實 OAuth2 在設計之初是已經作了不少安全方面的考慮,而且在 RFC6749 中加入了一些安全方面的規範指導。好比:
要求 Authorization server 進行有效的客戶端驗證;
client_serect, access_token, refresh_token, code等敏感信息的安全存儲(不得泄露給第三方)、傳輸通道的安全性(TSL的要求);
維持 refresh_token 和第三方應用的綁定,刷新失效機制;
維持 Authorization Code 和第三方應用的綁定,這也是state參數爲何是推薦的一點,以防止CSRF攻擊;
保證上述各類令牌信息的不可猜想行,以防止被猜想獲得;
安全無小事,這方面是要靠各方面(開放平臺,第三方開發者)共同防範的。
假設有用戶張三,攻擊者李四,第三方"雲沖印"應用(它集成了第三方社交帳號登陸,而且容許用戶將社交帳號和"雲沖印"中的帳號進行綁定),以及 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
的請求,並讓受害者的瀏覽器來接着完成後續的令牌申請流程。
要防止這樣的攻擊其實很容易,做爲第三方應用的開發者,只需在 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 的加密通道等;做爲第三方開發者在使用消費這些服務的時候也應該重視注意安全中存在的漏洞。
RFC6749 : The OAuth 2.0 Authorization Framework
豆瓣OAuth2 API(Authorization Code)
QQ OAuth2 API(Authorization Code)
微信公衆號獲取access_token(Client Credentials Grant)
參考博文: