原文地址:http://huoding.com/2011/11/08/126
去年我寫過一篇《OAuth那些事兒》,對OAuth做了一些簡單扼要的介紹,今天我打算寫一些細節,以闡明OAuth如何從1.0改變成1.0a,繼而改變成2.0的。
OAuth1.0
在OAuth誕生前,Web安全方面的標準協議只有OpenID,不過它關注的是驗證,即WHO的問題,而不是授權,即WHAT的問題。好在FlickrAuth和GoogleAuthSub等私有協議在授權方面做了不少有益的嘗試,從而為OAuth的誕生奠定了基礎。
OAuth1.0定義了三種角色:User、Service Provider、Consumer。如何理解?假設我們做了一個SNS,它有一個功能,可以讓會員把他們在Google上的聯絡人匯入到SNS上,那麼此時的會員是User,Google是Service Providere,而SNS則是Consumer。
+----------+ +----------+ | |--(A)- Obtaining a Request Token --------->| | | | | | | |<-(B)- Request Token ----------------------| | | | (Unauthorized) | | | | | | | | +--------+ | | | |>-(C)-| -+-(C)- Directing ---------->| | | | | -+-(D)- User authenticates ->| | | | | | +----------+ | Service | | Consumer | | User- | | | | Provider | | | | Agent -+-(D)->| User | | | | | | | | | | | | | | | +----------+ | | | |<-(E)-| -+-(E)- Request Token ------<| | | | +--------+ (Authorized) | | | | | | | |--(F)- Obtaining a Access Token ---------->| | | | | | | |<-(G)- Access Token -----------------------| | +----------+ +----------+
花絮:OAuth1.0的RFC沒有ASCII流程圖,於是我敲了幾百下鍵盤自己畫了一個,後經網友提示,Emacs可以很輕鬆的搞定ASCII圖:Emacs Screencast: Artist Mode,VIM當然也可以搞定,不過要藉助一個外掛:DrawIt,可惜我的鍵盤都要壞了。
Consumer申請Request Token(/oauth/1.0/request_token):
oauth_consumer_key oauth_signature_method oauth_signature oauth_timestamp oauth_nonce oauth_version
Service Provider返回Request Token:
oauth_token oauth_token_secret
Consumer重定向User到Service Provider(/oauth/1.0/authorize):
oauth_token oauth_callback
Service Provider在使用者授權後重定向User到Consumer:
oauth_token
Consumer申請Access Token(/oauth/1.0/access_token):
oauth_consumer_key oauth_token oauth_signature_method oauth_signature oauth_timestamp oauth_nonce oauth_version
Service Provider返回Access Token:
oauth_token oauth_token_secret
…
注:整個操作流程中,需要注意涉及兩種Token,分別是Request Token和Access Token,其中Request Token又涉及兩種狀態,分別是未授權和已授權。
OAuth1.0a
OAuth1.0存在安全漏洞,詳細介紹:Explaining the OAuth Session Fixation Attack,還有這篇:How the OAuth Security Battle Was Won, Open Web Style。
簡單點來說,這是一種會話固化攻擊,和常見的會話劫持攻擊不同的是,在會話固化攻擊中,攻擊者會初始化一個合法的會話,然後誘使使用者在這個會話上完成後續操作,從而達到攻擊的目的。反映到OAuth1.0上,攻擊者會先申請Request Token,然後誘使使用者授權這個Request Token,接著針對回撥地址的使用,又存在以下幾種攻擊手段:
- 如果Service Provider沒有限制回撥地址(應用設定沒有限定根域名一致),那麼攻擊者可以把oauth_callback設定成成自己的URL,當User完成授權後,通過這個URL自然就能拿到User的Access Token。
- 如果Consumer不使用回撥地址(桌面或手機程式),而是通過User手動拷貝貼上Request Token完成授權的話,那麼就存在一個競爭關係,只要攻擊者在User授權後,搶在User前面發起請求,就能拿到User的Access Token。
為了修復安全問題,OAuth1.0a出現了(RFC5849),主要修改了以下細節:
- Consumer申請Request Token時,必須傳遞oauth_callback,而Consumer申請Access Token時,不需要傳遞oauth_callback。通過前置oauth_callback的傳遞時機,讓oauth_callback參與簽名,從而避免攻擊者假冒oauth_callback。
- Service Provider獲得User授權後重定向User到Consumer時,返回oauth_verifier,它會被用在Consumer申請Access Token的過程中。攻擊者無法猜測它的值。
Consumer申請Request Token(/oauth/1.0a/request_token):
oauth_consumer_key oauth_signature_method oauth_signature oauth_timestamp oauth_nonce oauth_version oauth_callback
Service Provider返回Request Token:
oauth_token oauth_token_secret oauth_callback_confirmed
Consumer重定向User到Service Provider(/oauth/1.0a/authorize):
oauth_token
Service Provider在使用者授權後重定向User到Consumer:
oauth_token oauth_verifier
Consumer申請Access Token(/oauth/1.0a/access_token):
oauth_consumer_key oauth_token oauth_signature_method oauth_signature oauth_timestamp oauth_nonce oauth_version oauth_verifier
Service Provider返回Access Token:
oauth_token oauth_token_secret
注:Service Provider返回Request Token時,附帶返回的oauth_callback_confirmed是為了說明Service Provider是否支援OAuth1.0a版本。
…
簽名引數中,oauth_timestamp表示客戶端發起請求的時間,如未驗證會帶來安全問題。
在探討oauth_timestamp之前,先聊聊oauth_nonce,它是用來防止重放攻擊的,Service Provider應該驗證唯一性,不過儲存所有的oauth_nonce並不現實,所以一般只儲存一段時間(比如最近一小時)內的資料。
如果不驗證oauth_timestamp,那麼一旦攻擊者攔截到某個請求後,只要等到限定時間到了,oauth_nonce再次生效後就可以把請求原樣重發,簽名自然也能通過,完全是一個合法請求,所以說Service Provider必須驗證oauth_timestamp和系統時鐘的偏差是否在可接受範圍內(比如十分鐘),如此才能徹底杜絕重放攻擊。
…
需要單獨說一下桌面或手機應用應該如何使用OAuth1.0a。此類應用通常沒有服務端,無法設定Web形式的oauth_callback地址,此時應該把它設定成oob(out-of-band),當使用者選擇授權後,Service Provider在頁面上顯示PIN碼(也就是oauth_verifier),並引導使用者把它貼上到應用裡完成授權。
一個問題是應用如何開啟使用者授權頁面呢?很容易想到的做法是使用內嵌瀏覽器,說它是個錯誤的做法或許有點偏激,但它至少是個對使用者不友好的做法,因為一旦瀏覽器內嵌到程式裡,那麼使用者輸入的使用者名稱密碼就有被監聽的可能;對使用者友好的做法應該是開啟新視窗,彈出系統預設的瀏覽器,讓使用者在可信賴的上下文環境中完成授權流程。
不過這樣的方式需要使用者在瀏覽器和應用間手動切換,才能完成授權流程,某種程度上說,影響了使用者體驗,好在可以通過一些其它的技巧來規避這個問題,其中一個行之有效的辦法是Monitor web-browser title-bar,簡單點說,作業系統一般提供相應的API可以讓應用監聽桌面上所有視窗的標題,應用一旦發現某個視窗標題符合預定義格式,就可以認為它是我們要的PIN碼,無需使用者參與就可以完成授權流程。Google支援這種方式,並且有資料專門描述了細節:Auto-Detecting Approval(注:牆!)。
還有一點需要注意的是對桌面或移動應用來說,consumer_key和consumer_secret通常都是直接儲存在應用裡的,所以對攻擊者而言,理論上可以通過反編譯之類的手段解出來。進而通過consumer_key和consumer_secret簽名一個偽造的請求,並且在請求中把oauth_callback設定成自己控制的URL,來騙取使用者授權。為了遮蔽此類問題,Service Provider需要強制開發者必須預定義回撥地址:如果預定義的回撥地址是URL方式的,則需要驗證請求中的回撥地址和預定義的回撥地址是否主域名一致;如果預定義的回撥地址是oob方式的,則禁止請求以URL的方式回撥。
OAuth2.0
OAuth1.0雖然在安全性上經過修補已經沒有問題了,但還存在其它的缺點,其中最主要的莫過於以下兩點:其一,簽名邏輯過於複雜,對開發者不夠友好;其二,授權流程太過單一,除了Web應用以外,對桌面、移動應用來說不夠友好。
為了彌補這些短板,OAuth2.0做了以下改變:
首先,去掉簽名,改用SSL(HTTPS)確保安全性,所有的token不再有對應的secret存在,這也直接導致OAuth2.0不相容老版本。
其次,針對不同的情況使用不同的授權流程,和老版本只有一種授權流程相比,新版本提供了四種授權流程,可依據客觀情況選擇。
在詳細說明授權流程之前,我們需要先了解一下OAuth2.0中的角色:
OAuth1.0定義了三種角色:User、Service Provider、Consumer。而OAuth2.0則定義了四種角色:Resource Owner、Resource Server、Client、Authorization Server:
- Resource Owner:User
- Resource Server:Service Provider
- Client:Consumer
- Authorization Server:Service Provider
也就是說,OAuth2.0把原本OAuth1.0裡的Service Provider角色分拆成Resource Server和Authorization Server兩個角色,在授權時互動的是Authorization Server,在請求資源時互動的是Resource Server,當然,有時候他們是合二為一的。
下面我們具體介紹一下OAuth2.0提供的四種授權流程:
Authorization Code
可用範圍:此型別可用於有服務端的應用,是最貼近老版本的方式。
+----------+ | resource | | owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token)
Client向Authorization Server發出申請(/oauth/2.0/authorize):
response_type = code client_id redirect_uri scope state
Authorization Server在Resource Owner授權後給Client返回Authorization Code:
code state
Client向Authorization Server發出申請(/oauth/2.0/token):
grant_type = authorization_code code client_id client_secret redirect_uri
Authorization Server在Resource Owner授權後給Client返回Access Token:
access_token token_type expires_in refresh_token
說明:基本流程就是拿Authorization Code換Access Token。
Implicit Grant
可用範圍:此型別可用於沒有服務端的應用,比如Javascript應用。
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI --->| | | User- | | Authorization | | Agent -|----(B)-- User authenticates -->| Server | | | | | | |<---(C)--- Redirection URI ----<| | | | with Access Token +---------------+ | | in Fragment | | +---------------+ | |----(D)--- Redirection URI ---->| Web-Hosted | | | without Fragment | Client | | | | Resource | | (F) |<---(E)------- Script ---------<| | | | +---------------+ +-|--------+ | | (A) (G) Access Token | | ^ v +---------+ | | | Client | | | +---------+
Client向Authorization Server發出申請(/oauth/2.0/authorize):
response_type = token client_id redirect_uri scope state
Authorization Server在Resource Owner授權後給Client返回Access Token:
access_token token_type expires_in scope state
說明:沒有服務端的應用,其資訊只能儲存在客戶端,如果使用Authorization Code授權方式的話,無法保證client_secret的安全。BTW:不返回Refresh Token。
Resource Owner Password Credentials
可用範圍:不管有無服務端,此型別都可用。
+----------+ | Resource | | Owner | | | +----------+ v | Resource Owner (A) Password Credentials | v +---------+ +---------------+ | |>--(B)---- Resource Owner ------->| | | | Password Credentials | Authorization | | Client | | Server | | |<--(C)---- Access Token ---------<| | | | (w/ Optional Refresh Token) | | +---------+ +---------------+
Clien向Authorization Server發出申請(/oauth/2.0/token):
grant_type = password username password scope
AuthorizationServer給Client返回AccessToken:
access_token token_type expires_in refresh_token
說明:因為涉及使用者名稱和密碼,所以此授權型別僅適用於可信賴的應用。
Client Credentials
可用範圍:不管有無服務端,此型別都可用。
+---------+ +---------------+ | | | | | |>--(A)- Client Authentication --->| Authorization | | Client | | Server | | |<--(B)---- Access Token ---------<| | | | | | +---------+ +---------------+
Client向Authorization Server發出申請(/oauth/2.0/token):
grant_type = client_credentials client_id client_secret scope
Authorization Server給Client返回Access Token:
access_token token_type expires_in
說明:此授權型別僅適用於獲取與使用者無關的公共資訊。BTW:不返回Refresh Token。
…
流程中涉及兩種Token,分別是Access Token和Refresh Token。通常,Access Token的有效期比較短,而Refresh Token的有效期比較長,如此一來,當Access Token失效的時候,就需要用Refresh Token重新整理出有效的Access Token:
+--------+ +---------------+ | |--(A)------- Authorization Grant ------->| | | | | | | |<-(B)----------- Access Token -----------| | | | & Refresh Token | | | | | | | | +----------+ | | | |--(C)---- Access Token ---->| | | | | | | | | | | |<-(D)- Protected Resource --| Resource | | Authorization | | Client | | Server | | Server | | |--(E)---- Access Token ---->| | | | | | | | | | | |<-(F)- Invalid Token Error -| | | | | | +----------+ | | | | | | | |--(G)----------- Refresh Token --------->| | | | | | | |<-(H)----------- Access Token -----------| | +--------+ & Optional Refresh Token +---------------+
Client向Authorization Server發出申請(/oauth/2.0/token):
grant_type = refresh_token refresh_token client_id client_secret scope
Authorization Server給Client返回Access Token:
access_token expires_in refresh_token scope
…
不過並不是所有人都對OAuth2.0投贊成票,有空可以看看:OAuth 2.0對Web有害嗎?