OAuth 2.0 授權認證詳解

木鯨魚發表於2019-04-30

一、認識 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 流程圖有點晦澀,優化了 一下:

OAuth 2.0 授權認證詳解

  1. 使用者訪問第三方應用程式(簡稱:客戶端)以後,客戶端要求使用者給予授權。
  2. 使用者同意給予客戶端授權。
  3. 客戶端使用第 2 步獲得的授權,向認證伺服器申請令牌。
  4. 認證伺服器對客戶端進行認證以後,確認無誤,同意發放令牌。
  5. 客戶端使用令牌,向資源伺服器申請獲取資源。
  6. 資源伺服器確認令牌無誤,同意向客戶端開放資源。

上述中的第 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 應用。授權碼通過前端傳送,令牌則是儲存在後端,而且所有與資源伺服器的通訊都在後端完成。這樣的前後端分離,可以避免令牌洩漏。

OAuth 2.0 授權認證詳解

授權碼模式流程如下:

  1. 使用者訪問客戶端,客戶端將使用者導向認證伺服器。
  2. 使用者選擇是否給予客戶端授權。
  3. 假設使用者給予授權,認證伺服器將使用者導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
  4. 客戶端收到授權碼,附上早先的"重定向URI",向認證伺服器申請令牌。這一步是在客戶端的 後臺伺服器 上完成的,對使用者不可見。
  5. 認證伺服器核對了授權碼和重定向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 檔案中獨立定義了。這裡做以下簡單的介紹,主要有三種方式如下:

  1. URI Query Parameter
  2. Authorization Request Header Field
  3. 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 中加入了一些安全方面的規範指導。比如:

  1. 要求 Authorization server 進行有效的客戶端驗證;

  2. client_serect, access_token, refresh_token, code等敏感資訊的安全儲存(不得洩露給第三方)、傳輸通道的安全性(TSL的要求);

  3. 維持 refresh_token 和第三方應用的繫結,重新整理失效機制;

  4. 維持 Authorization Code 和第三方應用的繫結,這也是state引數為什麼是推薦的一點,以防止CSRF攻擊;

  5. 保證上述各種令牌資訊的不可猜測行,以防止被猜測得到;

安全無小事,這方面是要靠各方面(開放平臺,第三方開發者)共同防範的。

6.2 攻擊流程

假設有使用者張三,攻擊者李四,第三方"雲沖印"應用(它整合了第三方社交賬號登入,並且允許使用者將社交賬號和"雲沖印"中的賬號進行繫結),以及 OAuth2 服務提供者 Google。

OAuth 2.0 授權認證詳解

步驟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 攻擊的時序圖應該是下面這個樣子的:

OAuth 2.0 授權認證詳解

從上圖中可以看出,造成 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 參考資料

oauth.net/2/

Oauth Status Pages

RFC6749 : The OAuth 2.0 Authorization Framework

RFC6749中文版

7.2 案例

豆瓣OAuth2 API(Authorization Code)

QQ OAuth2 API(Authorization Code)

豆瓣OAuth2 API(Implicit )

QQ OAuth2 API(Implicit)

微信公眾號獲取access_token(Client Credentials Grant)

微博開發文件:授權機制

參考博文:

OAuth 2.0 的四種方式

理解OAuth 2.0

OAuth 2.0 的一個簡單解釋

OAuth2認證授權

理解OAuth2.0認證與客戶端授權碼模式詳解

移花接木:針對OAuth2的攻擊

OAuth授權與CSRF攻擊

OAuth研究&學習筆記

個人部落格:woodwhale's blog

部落格園:木鯨魚的部落格

相關文章