1.19 Securing REST Services
Last updated
Last updated
傳統的web application如果有security上的需求, 最典型的不外乎就是要求使用者輸入username/password, 然而, 對REST service來說, 這可能會有一些地方要考量. 我們知道, REST service的客戶端可以很多樣化, 從瀏覽器到行動裝置, 甚至來呼叫API的還可能是一個非人類個體(machine-to-machine communication), 而客戶端代替使用者呼叫REST service其實也是很常見的.
這邊我會簡介以下幾種用來保護REST service的手段:
Session-based security
HTTP Basic Authentication
Digest Authentication
Certificate based security
XAuth
OAuth
Session-based基本上是依賴server side session在往來的request之間保持使用者的identity. 在傳統的web application中, 使用者通常都要成功通過login page才可以存取受保護的資源. 成功登入的使用者, 其資訊會被server保存在HTTP session之中, 而在使用者之後與server的互動(request)之中, 也會透過查詢session去嘗試獲取使用者的資訊, 確認是否是合法授權(authorization)的使用者. 若使用者不具備合法授權身份, 則拒絕request.
以下是Session-based的圖形流程:
這種做法看起來不錯, 但在RESTful的原則中, 這種做法違反了statelessness這一點; 同時, 由於server hold住了client的狀態(state), 所以這種作法基本上很難做到scalable. 在理想的情境下, client應該hold住自己的狀態且server應該要是無狀態的.
我們知道透過login表單可以獲取使用者的username/password, 然而, 對於service之間互相溝通的狀況, 這種作法基本上是不可行的(因為兩邊都不是人類). HTTP basic authentication提供了一種不論是在互動式(interactive)或非互動式(noninteractive)的情境下都可以由client送出認證資訊(authentication information)的機制.
在這種機制中, 當client送出針對受保護資源的請求時, 會先收到一個401"Unauthorized"的回應, 其中還帶著一個"WWW-Authenticate"的header, 該header的值會有一個"Basic"區塊, 表示我們要使用Basic authentication, 而緊接在後的"realm"區塊則指出了server上的受保護區塊:
在收到了這個response之後, client會先把username跟password透過一個"; (semicolon)"串起來(concatenate), 然後再將這個串好的字串用Base64編碼(encode)過. 最後透過標準的Authorization header把這些資訊送到server.
而server收到後就會去decode這些資訊並且驗證其中的credentials, 若通過的話, server就會幫忙處理這個request.
簡單的流程圖如下:
這種機制看來是可以讓server達到stateless了, 然而, 你應該可以看得出來, client只是對credentials進行編碼(encode), 並不是加密(encrypt), 所以在non-SSL/TLS connection的環境下, 是可能遭受到main-in-the-middle attack的, 然後password可能就會被偷走了.
這跟Basic authentication很相似, 不過user credentials是有加密的. 當client送出針對受保護資源的請求時, 會收到如下的response:
注意到了嗎? 在"WWW-authenticate"裡面指定的是"Digest" authentication, 不是像前面討論的"Basic", 而nonce則是一個隨機的token, 是之後要用來加密的. 至於qop, 其原意為"quality of protection", 通常會有兩種值: "auth"以及"auth-int".
qop "auth": 表示這個digest的目的是用來作authentication的
qop "auth-int": 表示這個digest除了用來作authentication, 還要用來確保request integrity
再來, 當client收到response之後, 根據qop的不同, 會產生不同的行為:
qop="auth"的場合
用以下公式生成digest:
qop="auth-int"的場合
用以下公式生成digest(會包含request body):
預設上會用MD5去算出hash value, 然後算好的digest會被包在"Authorization" header中然後送至server. 當server收到後, 也會算一次digest然後驗證user identity, 若通過則處理request, 簡單的流程圖如下:
基本上Digest authentication是比Basic authentication要來得安全的, 畢竟password不會用明文傳送. 然而, 在non-SSL/TLS communications中, 還是有可能被人(snooper)偷到digest, 然後replay這個request. 一個比較好的作法就是限制server產生的nonce為一次性(one-time use only)的. 最後, 由於server也要重算一次digest去做驗證, 所以server也需要去存取到明文的password, 因此server沒辦法使用更安全的一次性加密演算法如bcrypt, 而且也可能遭受到server side attack.
這種作法基本上都要仰賴於認證(certificate)之上, 透過certificate, 雙方可以互相驗證對方的identity. 像在SSL/TLS based的通訊中, client(如瀏覽器)通常會透過certificate去驗證server的identity, 以確定server確實跟自己聲稱的身份是一樣的. 這種機制還可以做一些延伸, 比方說server也可以要求client的certificate, 並將此作為SSL/TLS handshake的一部分, 以驗證client identity.
以下是簡單的流程圖:
這樣看來這種機制很像滿不錯的, 因為雙方不用發送shared secret, 比username/password model更安全. 然而, 關於certificate的部署以及維護是很麻煩的.
由於REST API變得越來越流行, 使用這些API的第三方應用程式數量也顯著的成長了, 而這些應用程式則需要利用username/password來和REST service溝通與互動. 這基本上是存在很多安全問題的, 畢竟這些第三方應用程式會擁有username/password, 只要第三方存在任何一點安全上的裂縫, 都可能導致使用者資訊外洩. 同時, 如果使用者更改了credential, 則同時也需要去更新所有的第三方應用程式才行. 最後, 這種機制基本上也不允許使用者去撤銷對於第三方應用程式的授權(authorization), 唯一能夠撤銷的方式就是改變password.
有鑒於以上原因, XAuth跟OAuth被發展了出來, 它們提供了一種不需要儲存password也可以存取受保護資源的機制. 在這種方法中, client端的application會透過類似login form這類的方式取得user credential. 取得之後, 將這些資訊送至server, 而這台server在收到credentials之後, 會先進行驗證, 若成功的話則會回覆給client一個token. Client在收到token之後就會丟棄username/password並且儲存token在本地端. 在要存取使用者的受保護資源時, client就會把token包在request中送出, 這基本上是會透過一個叫做"X-Auth-Token"的HTTP header去完成的. 至於token的壽命, 則要看當下service的實作去決定. 簡單流程圖如下:
如Twitter這類應用程式是允許第三方程式透過XAuth去存取其REST API的. 然而, 即便是像XAuth這樣的機制, 第三方還是需要先取得username/password, 這仍然會有誤用的風險. 不過考量到Xauth的簡潔, 這其實對相同組織內的client來說會是個不錯的選擇.
OAuth(Open Authorization), 是一種用來在不儲存使用者password的狀態下也能存取受保護資源的框架. 這邊我只會介紹OAuth 2.0.
在OAuth 2.0中, 定義了以下四個角色:
Resource Owner: 資源持有者通常指願意給予部份他們自身的帳戶或是資源之存取權限的人, 比如說一個Twitter/Facebook的使用者
Client: 這就是指想要存取使用者權限與資源的應用程式了
Authorization Server: 授權伺服器, 其作用是驗證使用者的identity並且給予client一個token去存取使用者的資源
Resource Server: 資源伺服器, 持有受保護的使用者資源, 譬如說Twitter API, 其可以存取到tweets/timeline等等
在client可以參與OAuth之前, client必須先向authorization server進行註冊的動作. 對大多數的公開API如Facebook/Twitter, 這一步大概就是填寫申請表並且提供client的資訊如application name, base domain及網站等. 在註冊成功之後, client會收到client id與client secret. Client id是用來唯一識別此client且公開提供的. 這些client credential在OAuth的互動過程中扮演了很重要的角色, 稍後會提到.
對任何production下的OAuth2.0互動來說, HTTPS是強制性的, 所以URI的開頭是https, CLIENT_ID則是用來提供authorization server當前client的identity, scope參數則是透過逗號來分開client所需要的scopes/roles. 在收到這個request之後, authorization server會顯示給使用者一個認證(authentication)用的畫面, 通常是login form, 讓使用者提供username/password. 在驗證使用者成功之後, authorization server會透過CALL_BACK_URI將使用者重導到client application. 而authorization server也會在CALL_BACK_URI後面附上一個authorization code, 以下就是一個authorization server可能產生的範例URL:
Client再來就會使用authorization code去向authorization server請求access token, 這個動作基本上會透過HTTP POST來完成, 如下請求:
所以你就可以看到, client在request中提供了自己的credential. Authorization server會驗證client的identity以及authorization code. 在成功驗證之後, 會回傳一個access token, 以下是以JSON格式為主的一個response範例:
有了access token, client就可以透過向resource server傳遞token來要求受保護的資源了, resource server會驗證access token並且提供相應的服務.
OAuth 2.0的強項之一就是其對於各種client的支援, 如web application, native application及user agent/browser application. 稍早之前談到的authorization code flow (authorization grant type)適用於web application類的client, 其中包含了web-based的UI與server後端. 這允許client將authorization code儲存在一個安全的後端並且在往後的互動中重新利用之. 其他client類型則有各自的flow去決定跟OAuth 2.0的四種角色的互動流程. 一個純Javascript-based的應用程式或是native application可能無法安全的儲存authorization code. 因此對這類的client來說, 從authorization server得到的callback就不會包含authorization code. 取而代之, 一種隱式的授權類型方法就會被採用了, 而用來存取受保護資源的access token則是直接交給client. 另外, 這類client也不會有client secret, 而是簡單的透過client id去做identification.
OAuth互動的第一步始於使用者對client(第三方應用程式)的使用表達出興趣, Client會向使用者要求授權去存取受保護資源, 並且將user/resource owner導向(redirect)至授權伺服器(Authorization server). 以下是一個client可以用來重導user的範例URI: