現代網路應用日趨複雜,不大可能一項服務包山包海,更常見的情況是應用程式透過整合的方式,存取使用者在其他服務的資源,以進行整合或者提供進階的功能。
所以後端工程師很常會碰到 OAuth 2.0 這項標準,例如常見的 Google Sign In, Facebook Login 都有使用 OAuth 2.0 這項標準。
可以說 OAuth 2.0 是後端工程師的必修課題之一。
OAuth 2.0 (或簡稱 OAuth 2)簡介
OAuth 2.0 是 1 項授權標準(RFC 6749)。
OAuth 2.0 允許第三方應用程式在使用者授權的情況下,安全地存取使用者在另一個應用程式或服務上的資源,而不需要求使用者在另一個應用程式上的帳號和密碼。
資源 1 詞表示使用者的擁有的資料、檔案等等,之所以此處使用「資源」是因為 OAuth 2.0 RFC 中使用 “resource” 1 詞表示。
以下是典型適用 OAuth 2.0 的例子:
- 某相簿服務的使用者,授權某相簿服務可以取得使用者在 Google 相簿的所有相簿。
- GitHub 的使用者,授權某雲端服務存取其在 GitHub 上的特定 repository 以進行自動化部屬。
p.s. 常見的登入整合服務通常是利用 OAuth 取得使用者的姓名與電子郵件。
4 個 OAuth 2.0 的重要角色(Roles)
上述 2 個例子就帶出 4 個重要的角色(同 RFC 6749 中提到的 4 個角色):
- Resource Owner,能夠授權存取權限的角色,通常指的是使用者,由使用者決定是否要讓他人存取其擁有的資源。
- Resource Server,如例子中的 Google 與 GitHub 伺服器,存放使用者資源的伺服器。
- Authorization Server,如例子中負責驗證使用者身份的 Google 與 GitHub 伺服器。
- Client,發出 OAuth 2.0 授權請求的應用程式,也就是使用者所使用的第三方應用程式/服務,如例子中的某相簿服務、某雲端服務,對 authorization server 來說,它們就是 client。
OAuth 2.0 運作流程
根據 RFC 6749 定義,OAuth 2.0 的運作流程如下:
流程順序為 A - F,以下簡單說明。
(A) 引導使用者授權
使用者在第三方應用程式上進行操作,當第三方應用程式需要存取使用者在其他服務的資源時,會發出授權請求,引導使用者進行授權。
(B) 取得授權(authorization grant)
使用者同意授權後,此步驟會有 4 種不同的授權方式,最常見的方式是回傳授權碼(Authorization Code)給第三方應用程式使用。
4 種不同授權方式為:
- Authorization Code
- Implicit (不建議使用)
- Resource Owner Password Credentials
- Client Credentials
(C), (D) 要求 Access Token
第三方應用程式使用 (B) 步驟取得的授權,向資源擁有者請求 1 組 access token。資源擁有者會確認該組授權是否合法後,才回傳 access token 給第三方應用程式。這個 access token 只能用於存取使用者所授權的資源。
(E), (F) 存取使用者授權的資源
第三方應用程式需要存取使用者的資源時,會以 access token 進行存取, Resource Server 會驗證 access token 是否合法,才回傳使用者所擁有的資源給第三方服務。
不過上述流程是直接對 Resource Owner 發出授權請求, RFC 文件中也提到授權可以經由 Authorization Server 向 Resource Owner 發出請求:
The authorization request can be made directly to the resource owner, or preferably indirectly via the authorization server as an intermediary.
這也是現在比較常見的做法,它的流程變成:
最主要的不同是 (A), (B) 步驟需要透過 Authorization Server。
(A) 引導使用者授權
使用者在第三方應用程式上進行操作,這個應用程式會引導使用者到 Authorization Server(如 Google、Facebook 的授權網頁)進行授權。
(B) 取得授權(authorization grant)
使用者在 Authorization Server 授權完成之後,由 Authorization Server 回應授權給第三方應用程式。
相信以下網頁大家都不陌生,當我們在第三方應用程式按下 Continue with Google 或者 Sign up with Google 等按鈕,其實走的是上述透過 Authorization Server 進行授權的流程:
OAuth 2.0 的主要優點為:
安全性
使用者不需要向第三方應用程式提供自己的帳號密碼。
增加使用者控制資源的能力
使用者可以隨時撤銷(revoke)授權,從而控制第三方應用程式對其資源的存取。
p.s. 如果第三方應用程式在使用者撤銷授權之後,第三方應用程式就會立即無法存取使用者的資源,但是已經被存取且儲存在第三方應用程式內的資源則不受控制,這點還是仰賴第三方應用程式是否足夠自律以及尊重使用者隱私。總的來說,不要隨意授權是最好的作法。
跨服務整合能力
第三方應用程式可以在使用者授權的情況下,與其他服務進行整合。
OAuth 2.0 已經廣泛應用各種網路服務之中,包括登入服務(如 Facebook Login、Google Sign-In)、API 服務(如 GitHub API)以及雲端服務(如 AWS、Azure)等等。
真實世界的 OAuth 2.0 運作
接下來,我們看看真實世界中的 OAuth 2.0 是怎麼運作的。
本章節以最常見的 Authorization Code 授權方式進行說明。
註冊應用程式 / Client Registration
OAuth 2.0 並不是說要用就能馬上用的。
在開始整合之前,一定要到要整合的服務上註冊你所開發的應用程式。例如,我們想整合以 Google 帳號註冊的功能,就需要到 Google Cloud Console 或者 Firebase 註冊專案:
而應用程式的類型會影響授權流程,所以我們通常也需要設定應用程式類型:
我們舉網頁應用程式為例。
設定網頁應用程式通常需要設定 Redirection URI,在使用者授權之後 Authorization Server 會透過 Redirection URI 告訴應用程式獲得授權,如果授權方式是 authorization code,就會在呼叫 Redirection URI 時也附上授權碼。
此處我們通常會填上 1 個負責處理授權的 API endpoint,例如:
https://example.com/oauth/redirection/handler
完成註冊應用程式步驟之後,就會得到 client id 與 client secret,而 client secret 會用來與 Authorization Server 進行溝通,確保你所發出的 request 是合法的,所以 client secret 是不可外洩的。
引導使用者授權
註冊應用程式之後,就可以開始使用 OAuth 2.0 引導使用者進行授權,如果這個步驟有 SDK 可以使用的話,開發者只需要安裝相對應的 SDK 與呼叫相對應的函式即可,例如以下畫面即是使用 Firebase SDK 所開啟的引導授權網頁:
不過 SDK 都經過層層包裝,無助於我們了解真實情況。
接下來,我們會用 Google OAuth 2.0 Playground 模擬真實授權情況(你可以把 Google OAuth 2.0 Playground 想像成是我們所開發的第三方應用程式),我們舉 Google Sheets API 的授權為例:
從上圖可以看到我們需要選擇要使用什麼 API 與 scope,scope 其實就是授權範圍的概念。
我們選擇 Google Sheets API v4 與 https://www.googleapis.com/auth/spreadsheets.readonly
希望使用者授權我們讀取其擁有的所有試算表:
接著按下 Authorize APIs
按鈕,就會出現以下畫面,這就是使用者會看到的引導授權畫面:
重點是,上述網頁的網址視授權方式的不同,會使用不同參數,舉使用授權碼的授權方式為例,會附上 response_type
, client_id
, redirect_uri
3 個重要參數以及 1 個 scope
參數表示授權範圍:
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground&prompt=consent&response_type=code&client_id=407408718192.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fspreadsheets.readonly&access_type=offline&service=lso&o2v=2&ddm=0&flowName=GeneralOAuthFlow
response_type=code
代表使用授權碼方式,剩下就是我們所註冊的 client id 與 redirection URI,不過此處由於使用 Google OAuth 2.0 Playground 模擬第三方應用程式,所以 client id 與 redirection URI 都是 Google OAuth 2.0 Playground 的 client id 與 redirection URI。
從參數 redirect_uri
可以看到我們如果授權之後,Google OAuth 2.0 Playground 會把我們導回 https://developers.google.com/oauthplayground
這個網址。
取得授權碼(authorization code)
當使用者按下按鈕確定授權的時候, Authorization Server 就會使用轉址到 redirection URI,藉此讓第三方應用程式獲得授權。
上圖是使用者按下授權按鈕之後,被轉址的結果,可以看到轉址後網址多了 1 個 code
參數,這就是 authorization code。Authorization Server 會在 redirection URI 增加參數 code
,藉此告訴第三方應用程式授權碼為何,同時將使用者導回第三方應用程式的頁面,所以後端工程師都需要在 redirection URI 處理接收 code
參數的工作,以進一步取得 access token。
取得 access token
有了 authorization code 之後,就可以用 authorization code 交換 access token。
按下 Exchange authorization code for tokens 就可以看到 Google OAuth 2.0 Playground 發出 1 個 POST request,該 request 有使用 authorization code,並收到來自 Google 回應的 access token 與 refresh token:
這就是取得 access token 的過程。
同時,我們也可以注意到 access token 是有期限的,當 access token 過期之後,我們需要使用 refresh token 再向 Authorization Server 索取 1 組新的 access token,也就是下圖流程中的 (G) 與 (H) 部分:
剩下的就都是如何使用 access token 呼叫 API 取得資源的範疇了,在此不多加贅述。
這就是真實世界 OAuth 2.0 的運作啦~!
其他 3 種授權方式
目前為止,我們只提到 Authorization Code 的授權方式。
但其實還有 3 種沒有提到:
- Resource Owner Password Credentials
- Implicit (不建議使用)
- Client Credentials
這 3 種的流程都大同小異。
Resource Owner Password Credentials
這個流程是直接向使用者索取帳號、密碼,再以此帳號密碼向 Authorization Server 交換 access token 。
這個流程只適合高度可信賴的第三方應用程式使用,畢竟有洩漏帳號密碼的風險,所以目前不常見。
Implicit (不建議使用)
注意!Implicit 已經是不再建議使用的授權方式!由於它會直接將 access token 放在 fragment URI 中,這導致在 User-Agent 端就有洩漏 access token 的安全隱患!
有心人士可能從 Browser history, referrer header 就可以輕鬆拿到 access token,詳見 OAuth2 Implicit Grant and SPA。
談 Implicit 之前,先認識什麼是 Confidential client 與 Public client。
OAuth 2.0 的 RFC 文件提到 2 種類型的 clients:
- Confidential client
- Public client
它們的差別在於能不能好好的保存 client credentials (例如 client secret),畢竟 client credentials 會被用來請 authorization server 核發 access token。
而 Web application 可以將 client credentials 安全地存在後端伺服器,所以屬於 confidential client。
至於沒有後端伺服器支援的 native apps, single page apps 或者 browser extensions 等等,因為不能安全地儲存 client credentials,所以屬於 public client。
Implicit 就是專為 public client 所設計的 1 種流程。
Implicit 流程比較特殊,適合 in-browser application (例如 Single Page Application, SPA )使用,它流程中的引導授權、授權、轉址取得 access token 等部分都在 User-Agent (例如瀏覽器)中完成,並且在 Authorization Server 端進行認證時,需要額外送上 state
參數並儲存在 User-Agent 內,而 access token 是轉址後直接放在 URI fragment 中,例如:
#access_token=-V0Y9ux7h7kriR4VDSD3uEveHv_r_XorYc7guWNcTS27Hjzs4-D621I9R1uZPDyho79tPJun&token_type=Bearer&expires_in=86400&scope=photos&state=m5hYY_cDK1xUBxjZ
在 User-Agent 收到這個帶有 access token 的 fragment 時,必須先驗證 state
是否與 User-Agent 事先儲存的 state
相同,如果相同就將 access token 取出後,交由第三方應用程式使用。
注意,這個授權方式不支援 refresh token。
想體驗 Implicit 流程的話,可以使用 OAuth 2.0 Playground。
state 參數的作用
在 implicit 流程中,我們看到 1 個特殊的參數 —— state。
這個參數的作用是為了防止 CSRF (Cross-Site Request Forgery) 攻擊。
直接用 1 個案例解說更能理解 state 的作用。
假設有個第三方應用可以把使用者的照片存到 Google Dirve 進行備份,它不幸的成為攻擊者的目標。
因為 redirection URI 大家都可以從網址看到,那麼攻擊者可以偽造 1 個頁面含有攻擊者授權碼的 redirection URI 給受害者點擊,例如以下惡意 Redirection URI:
https://example.com/oauth/redirection/handler?code=<攻擊者的授權碼>
如此一來,這個第三方應用會認為受害者授權使用 Google Drive 進行備份,但是第三方應用程式拿到的卻是攻擊者的授權碼,所以會取得攻擊者的 access token 之後,再把受害者的資料存到攻擊者的 Google Drive!
為了解決 CSRF 攻擊所造成的資安隱患,所以:
- 在引導授權之前,第三方應用必須先產生 state 參數,state 參數可以是 1 個隨機字串、雜湊值或其他無法被預測的值,並且存在 local storage 或者 session cookie 內,這些是只有第三方應用程式自己才能存取的地方,或者說受到同源政策(same-origin policy)保護的地方,攻擊者無法得知這個隨機字串是什麼,所以也無法針對個別使用者產生。
- 在 authorization server 引導授權的網址加上第 1 步所產生的 state 參數,引導使用者授權。
- Authorization server 在確認使用者授權之後,同樣會在 redirection URI 加上 state 參數並轉回第三方應用。
- 第三方應用會在 redirection URI 收到 state 與 code 2 個參數,在使用 code 之前,必須先檢查 state 參數是否跟第 1 步驟所儲存的 state 值相同,如果沒有 state 值或值不相同,就很可能代表遭受 CSRF 攻擊,因此不該繼續 OAuth 流程。
根據 RFC 6749 規定,如果 client 有附上 state 參數,回應 redirection URI 時,也需要附上 state 參數給 client,所以 state 參數不僅僅只是 implicit 專屬, authorization code 也可以使用,而且建議必須使用!
Client Credentials
相對於其他授權方式,Client Credentials 是最簡單的 1 種,它僅需要向 Authorization Server 提供其憑證(client_id
, client_secret
),就可以直接取得 access token。
其實從流程圖可以看到,Client Credentials 不需經過使用者授權就能存取其資料,所以我們很難看到有哪些服務採取這種授權類型。比較有可能的應用場景是 Client 本身也是 Resource Owner 的情況,所以不需要經過授權就可以直接存取其所擁有資料,例如企業內部服務存取內部資料的情況,就比較適合使用 Client Credentials。
PKCE (Proof Key for Code Exchange) 擴充流程
p.s. PKCE (發音 pixy) 的 RFC 文件為 RFC 7636
沒有後端伺服器支援的 native apps, single page applications(SPA) 等應用,在實作 OAuth 2.0 時很容易遇到資安問題,譬如:
- 它們別無選擇只能把 client credentials 存在 app 內,這使得任何人都可以藉由反組譯(decompile)、逆向分析等手段就得到 client credentials。
- 它們更容易面臨被同一個裝置上的惡意 app 攔截授權碼的風險。
下圖是 RFC 7636 陳述的針對 pubic client 可能攻擊的手法:
簡單來說,public client 在 client credentials 曝光以及被攔截授權碼的情況下(或者能夠監聽到對 authorization server 所發出的 request,上圖 (2) 的部分),攻擊者就具備偽造請求 access token 的能力,從而導致使用者資源遭到攻擊者的存取,也就是圖中 (4), (5), (6) 的部分。
要使上述攻擊手段成功,有一些手段可以使用:
- 攻擊者藉由反組譯(decompile)、逆向分析等手段得到 client credentials。
- 攻擊者能夠在裝置上安裝 1 個惡意 app,並註冊與目標 app 相同 custom URI scheme,例如
myapp://
,達成攔截授權碼的效果,目前 iOS, Android 都能夠讓多個 app 註冊使用相同的 custom URI scheme。
考慮到這些問題,OAuth 2.0 提出稱為 PKCE (Proof Key for Code Exchange) 的擴充流程,基於 authorization code flow 做出改進。
這些改進是為了讓 authorization server 核發 access token 時更加安全,讓 client secret 或者 authorization code 就算曝光/被攔截也不會毫無防護能力。
PKCE 的運作很簡單,首先它會在發出授權請求之前,先產生 1 個無法被預測的隨機密語,稱為 code_verifier,接著將 code_verifier 進行雜湊(通常使用 SHA256 雜湊),再以 Base64 編碼後,這個經過 SHA256 雜湊與 Base64 編碼後的字串,稱為 code_challenge,code_calllenge 會跟著授權請求一起傳給 authorization server。
以虛構的程式碼表示的話:
code_verifier = crypto_random_string()
base64url(sha256(code_verifier))
轉址到 authorization server 引導使用者授權的網址會類似以下網址:
https://authorization-server.com/authorize?
response_type=code
&client_id=8rgLrPfuIQxY5cBMpMA1SVD9
&redirect_uri=https://www.oauth.com/playground/authorization-code-with-pkce.html
&scope=photo+offline_access
&state=NYxxUB2WOv4vx3-S
&code_challenge=qussZyew5mgO1np15zS0AtOqCE5DGgWDxMClLytpOM8
&code_challenge_method=S256
上述可以看到 2 個重要參數 code_challenge
與 code_challenge_method
,目的在於告訴 authorization 這次授權請求的 code_challenge
是使用 SHA256 雜湊後的結果。
Authorization server 在收到 code_challenge
與 code_challenge_method
之後,如果使用者也確定授權,就會將 code_challenge
, code_challenge_method
與授權碼一同儲存起來,並發出授權碼給 client。
code_challenge
, code_challenge_method
與授權碼在後端資料庫可能會是以下形式:
| auth_code | code_challenge | code_challenge_method |
|----------------|---------------------------------------------|-----------------------|
| theauthcode789 | qussZyew5mgO1np15zS0AtOqCE5DGgWDxMClLytpOM8 | S256 |
接著,client 取得授權碼之後,要再發出 access token 的請求時,除了授權碼之外,還需要額外附上一開始所產生的 code_verifier
給 authorization server 進行驗證,authorization server 會在收到請求之後,以授權碼查找相對應的 code_challenge
與 code_challenge_method
,並用審核 client 送上來的 code_verifier
用相同方式計算會與資料庫中的 code_challenge
相同,如果相同才會核發 access token。
PKCE 流程圖如下所示,總的來說,就是在 authorization code flow 的基礎上加上紅字的部分:
因為惡意 app 無法得知一開始我們所產生的 code_verifier
是什麼,加上它無法從雜湊反推 code_verifier
,所以就算惡意 app 攔截了授權碼,它也無法在請求 access token 時附上正確的 code_verifier
,最終導致即使惡意 app 擁有 client credentials 與授權碼,它也無法偽造請求取得 access token。
這就是 PKCE 的威力!
也因爲 PKCE 增強了 OAuth 2.0 的安全性,所以現在都會建議 client 實作 OAuth 2.0 時,如果 authorization server 有支援 PKCE 的話,就應該使用 PKCE 以增強安全性。
總結
OAuth 2.0 是現代網路應用服務相當常見的標準,不僅是後端工程師在整合其他服務很常見的技術之外,也是後端伺服器需要開放外部服務存取使用者資源的最佳選擇。
除了本文所提到的 4 種流程之外,其實還有 PKCE, Device Code, OpenID Connect 等擴充流程,其中 Device Code 適合應用在智慧型電視、裝置,也是相當值得了解的一個流程!有興趣的話,可以使用 OAuth 2.0 Playground 體驗 PKCE, Device Code, OpenID Connect 等流程!
以上!
Enjoy!
References
OAuth 2 Explained In Simple Terms
Authorization Code Flow with Proof Key for Code Exchange (PKCE)