OAuth2.0協議原理詳解
OAuth 2.0 是目前比較流行的做法,它率先被Google, Yahoo, Microsoft, Facebook等使用。之所以標註為 2.0,是因為最初有一個1.0協議,但這個1.0協議被弄得太複雜,易用性差,所以沒有得到普及。2.0是一個新的設計,協議簡單清晰,但它並不相容1.0,可以說與1.0沒什麼關係。所以,我就只介紹2.0。
協議的參與者
從引言部分的描述我們可以看出,OAuth的參與實體至少有如下4個:
- RO (resource owner): 資源所有者,對資源具有授權能力的人。如上文中的使用者Alice。
- RS (resource server): 資源伺服器,它儲存資源,並處理對資源的訪問請求。如Google資源伺服器,它所保管的資源就是使用者Alice的照片。
- Client: 第三方應用,它獲得RO的授權後便可以去訪問RO的資源。如網易印像服務。
- AS (authorization server): 授權伺服器,它認證RO的身份,為RO提供授權審批流程,並最終頒發授權令牌(Access Token)。讀者請注意,為了便於協議的描述,這裡只是在邏輯上把AS與RS區分開來;在物理上,AS與RS的功能可以由同一個伺服器來提供服務。
授權型別
在開放授權中,第三方應用(Client)可能是一個Web站點,也可能是在瀏覽器中執行的一段JavaScript程式碼,還可能是安裝在本地的一個應用程式。這些第三方應用都有各自的安全特性。對於Web站點來說,它與RO瀏覽器是分離的,它可以自己儲存協議中的敏感資料,這些金鑰可以不暴露給RO;對於javascript程式碼和本地安全的應用程式來說,它本來就執行在RO的瀏覽器中,RO是可以訪問到Client在協議中的敏感資料。
OAuth為了支援這些不同型別的第三方應用,提出了多種授權型別,如:
- 授權碼 (Authorization Code Grant)
- 隱式授權 (Implicit Grant)
- RO憑證授權 (Resource Owner Password Credentials Grant)
- Client憑證授權 (Client Credentials Grant)
由於本文旨在幫助使用者理解OAuth協議,所以我將先介紹這些授權型別的基本思路,然後選擇其中最核心、最難理解、也是最廣泛使用的一種授權型別——“授權碼”,進行深入的介紹。
2.1 授權碼模式
(A) web客戶端通過將終端使用者的user-agent重定向到授權伺服器來發起這個流程。客戶端傳入它的客戶端識別符號、請求作用域、本地狀態和一個重定向URI,在訪問被許可(或被拒絕)後授權伺服器會重新將終端使用者引導回這個URI。
(B) 授權伺服器驗證終端使用者(藉助於user-agent),並確定終端使用者是否許可客戶端的訪問請求。
(C) 假定終端使用者許可了這次訪問,授權伺服器會將user-agent重定向到之前提供的重定向URI上去。授權伺服器為客戶端傳回一個授權碼做獲取訪問令牌之用。
(D) 客戶端通過驗證並傳入上一步取得的授權碼從授權伺服器請求一個訪問令牌。(需要帶上ClientId和Secret,ClientId和Secret是通過平臺授予)
(E) 授權伺服器驗證客戶端私有證書和授權碼的有效性並返回訪問令牌。
2.2 隱授權模式(implicit grant type)
User-Agent適用於客戶端不能儲存客戶端私有證書的App(純客戶端App,無Server參與)。因為純客戶端的程式不能儲存金鑰
(A) 客戶端將user-agent引導到終端使用者授權endpoint。客戶端傳入它的客戶端識別符號、請求作用域、本地狀態和一個重定向URI,在訪問被許可(或被拒絕)後授權伺服器會重新將終端使用者引導回這個URI。
(B) 授權伺服器驗證終端使用者(通過user-agent)並確認終端使用者是許可還是拒絕了客戶端的訪問請求。
(C) 如果終端使用者許可了這次訪問,那麼授權伺服器會將user-agent引導到之前提供的重定向URI。重定向URI會在URI片斷{譯者注:URI片斷是指URI中#號之後的內容}中包含訪問令牌。
(D) user-agent響應重定向指令,向web伺服器傳送不包含URI片斷的請求。user-agent在本地儲存URI片斷。
(E) web伺服器返回一個web頁面(通常是嵌入了指令碼的HTML網頁),這個頁面能夠訪問完整的重定向URI,它包含了由user-agent儲存的URI片斷,同時這個頁面能夠將包含在URI片斷中的訪問令牌(和其它引數)提取出來。
(F) user-agent在本地執行由web伺服器提供的指令碼,該指令碼提取出訪問令牌並將它傳遞給客戶端。
2.3 密碼模式
密碼模式就是將密碼託管給第三方App,但是必須要保證第三方App高度可信。
(A)使用者向客戶端提供使用者名稱和密碼。
(B)客戶端將使用者名稱和密碼發給認證伺服器,向後者請求令牌。
(C)認證伺服器確認無誤後,向客戶端提供訪問令牌。
2.4 客戶端模式
這種模式不需要終端使用者的參與,只是Client和Server端的互動。通常只用於Client狀態的獲取。
(A)客戶端向認證伺服器進行身份認證,並要求一個訪問令牌。
(B)認證伺服器確認無誤後,向客戶端提供訪問令牌。
授權碼模式的例項化描述
面我以例項化方式來幫助讀者理解授權碼型別的授權協議的執行過程。假設:
(1) Alice有一個有效的Google帳號;
(2) Facebook.com已經在Google Authorization Server上註冊了Client身份,已經獲得(client_id, client_secret),注意client_secret是Client與AS之間的一個共享金鑰。
下圖展示了Alice、Facebook.com、Google資源伺服器、以及Google OAuth授權伺服器之間的協議執行過程。
協議所涉及到的細節都已經在圖上了,所以不打算再做詳細介紹了。若看懂了此圖,OAuth2.0就理解了。
讀者請注意,在步驟(4)中,Client需要拿“授權碼”去換“授權令牌”時,Client需要向AS證明自己的身份,即證明自己就是步驟(2)中Alice批准授權時的Grantee。這個身份證明的方法主要有兩種(圖3中使用了第1種):
(1) 通過https直接將client_secret傳送給AS,因為client_secret是由Client與AS所共享,所以只要傳送client_secret的通道安全即可。
(2) 通過訊息認證碼來認證Client身份,典型的演算法有HMAC-SHA1。在這種方式下,Client無需傳送client_secret,只需傳送訊息請求的signature即可。由於不需要向AS傳遞敏感資料,所以它只需要使用http即可。
此外, 在步驟(2)中,Google授權伺服器需要認證Alice的RO身份,並提供授權介面給Alice進行授權審批。今天Google提供的例項如下圖所示,僅供讀者理解OAuth這種“現場授權”或”線上授權”的含義。
OAuth2.0授權型別選擇
授權模式實際上就是指獲取token的方法,選擇授權模式主要取決於終端使用者使用的客戶端的型別,以及你服務對使用者的表現形式。
具體的選擇方式如下圖:
First Party or third party client?
First party指的是,你完全信任這個客戶端可以管理好終端使用者的認證憑據。比如說對於Spotify的開發者和所有者來說,他們完全信任他們的Spotify iphone客戶端。而third party客戶端是指我們不信任的客戶端。
Access Token Owner?
Access Token表示給一個客戶端授權訪問某些被保護的資源。如果我們只需要給一個機器授權訪問某些資源,而不需要給人授權,那麼這些資源應該實現client credential grant。
如果我們需要給人授權訪問資源,那麼需要視客戶端的型別做決定。
Client type?
客戶端型別主要是指客戶端是否有能力儲存祕鑰。
如果客戶端是個web app,並且有伺服器側的元件,那麼我們應該選擇authorization code grant
如果客戶端是一個純前端的web app(如:單頁面應用),如果客戶端可信,我們可以選擇password grant,如果客戶端不可信可以選擇implicit grant
如果客戶端是一個本地應用,比如一個手機app,應該選擇password grant。
第三方本地應用應該用authorization code grant(通過本地瀏覽器,不要用嵌入式瀏覽器,比如iOS強制使用者用Safari或者SFSafariViewController,而不用 WKWebView)。
OAuth設計上的安全性考慮
協議設計中,為什麼要使用authorization_code來交換access_token?這是讀者容易想到的一個問題。也就是說,在協議的第3步,為什麼不直接將access_token通過重定向方式返回給Client呢?比如:
1 2 3 4 5 |
HTTP/1.1 302 Location: https://www.facebook.com/?access_token=ya29.AHES6ZSXVKYTW2VAGZtnMjD&token_type=Bearer&expires_in=3600 |
如果直接返回access_token,協議將變得更加簡潔,而且少一次Client與AS之間的互動,效能也更優。那為何不這麼設計呢?協議文件[1]中並沒有給出這樣設計的理由,但也不難分析:
(1) 瀏覽器的redirect_uri是一個不安全通道,此方式不適合於傳遞敏感資料(如access_token)。因為uri可能通過HTTP referrer被傳遞給其它惡意站點,也可能存在於瀏覽器cacher或log檔案中,這就給攻擊者盜取access_token帶來了很多機會。另外,此協議也不應該假設RO使用者代理的行為是可信賴的,因為RO的瀏覽器可能早已被攻擊者植入了跨站指令碼用來監聽access_token。因此,access_token通過RO的使用者代理傳遞給Client,會顯著擴大access_token被洩露的風險。 但authorization_code可以通過redirect_uri方式來傳遞,是因為authorization_code並不像access_token一樣敏感。即使authorization_code被洩露,攻擊者也無法直接拿到access_token,因為拿authorization_code去交換access_token是需要驗證Client的真實身份。也就是說,除了Client之外,其他人拿authorization_code是沒有用的。 此外,access_token應該只頒發給Client使用,其他任何主體(包括RO)都不應該獲取access_token。協議的設計應能保證Client是唯一有能力獲取access_token的主體。引入authorization_code之後,便可以保證Client是access_token的唯一持有人。當然,Client也是唯一的有義務需要保護access_token不被洩露。
(2) 引入authorization_code還會帶來如下的好處。由於協議需要驗證Client的身份,如果不引入authorization_code,這個Client的身份認證只能通過第1步的redirect_uri來傳遞。同樣由於redirect_uri是一個不安全通道,這就額外要求Client必須使用數字簽名技術
來進行身份認證,而不能用簡單的密碼或口令認證方式。引入authorization_code之後,AS可以直接對Client進行身份認證(見步驟4和5),而且可以支援任意的Client認證方式(比如,簡單地直接將Client端金鑰傳送給AS)。
在我們理解了上述安全性考慮之後,讀者也許會有豁然開朗的感覺,懂得了引入authorization_code的妙處。那麼,是不是一定要引入authorization_code才能解決這些安全問題呢?當然不是。筆者將會在另一篇博文給出一個直接返回access_token的擴充套件授權型別解決方案,它在滿足相同安全性的條件下,使協議更簡潔,互動次數更少。
一切只是看上去很美好,但其實很多坑
OAuth2看上去很美好,但是細心觀察其實還是有一些漏洞的。
對於授權碼和access_token的篡改,在OAuth1中是反覆的對Code和Token進行簽名,來保證Token不會被篡改,但是OAuth2中卻沒有,因為OAuth2是基於Https的,所以如果沒有Https的支援OAuth2可能還不如OAuth1.
對於redirect_uri的校驗,OAuth1中沒有提到redirect_uri的校驗,那麼OAuth2中要求進行redirect_uri的校驗。但是如果校驗規則過鬆,也會導致跳轉的安全問題。 例如:校驗的時候只校驗根域名,或者二級域名,但是第三方App對自己的域名保護的不好,導致二級域名被hack那麼此時授權碼和Token會被竊取。 校驗規則不嚴謹,例如www.baidu.com 但是redirect_uri為:www.a.com.\www.baidu.com,這樣授權就被a.com釣走了。
對於CSRF攻擊(跨站請求偽造):由於這個授權過程伺服器和Client和使用者之間有幾次互動,但是在得到授權碼的時候需要一次回跳,但是這次回跳是可以被阻塞的。
攻擊者使用自己的賬戶申請第三方授權登陸
授權後服務端返回授權碼,但是此時組織授權回跳,此時Client並沒有接到授權碼,也就是阻斷了授權流程
攻擊者將此跳轉連結發給一個正在處於在Client登陸狀態的賬戶
誘騙正常使用者點選,那麼此時攻擊者第三方賬戶和被攻擊賬戶進行繫結(相當於賬戶綁定了第三方的賬戶)
攻擊者再次進行第三方授權登陸。這樣就劫持了誘騙的賬戶。轉賬?刪好友?等等就所以搞了。
解決辦法:
在進行授權碼申請或者是Token申請的時候帶上state引數,伺服器返回請求時要求攜帶state引數,在Client處理授權的時候校驗此引數。引數可以是當前賬戶的SessionId,或Cookie的簽名串。在Client申請accessToken會驗證相關state和當前使用者的關係,這樣就防止了篡改。
OAuth的校驗流程為什麼這麼複雜,直接授權之後redirect回accessToken不就結了嗎?為什麼還要返回auth_code之後請求accessToken?
首先,redirect是不安全的,隨時可以暫停回撥而拿到accessToken,拿到了accessToken也就意味著拿到了授權,但是auth_code是和client相對應的,那麼即使拿到了auth_code還需要再次申請accessToken,申請accessToken時需要校驗Client和state。同時協議設計的原則就是隻有Client能拿到accessToken而使用者是拿不到的。