OAuth 2.0 ,以 PHP 登入 Google 為例
參考資料
- [筆記] 認識 OAuth 2.0:一次了解各角色、各類型流程的差異 | by Mike Huang | 麥克的半路出家筆記 | Medium
- 串接 Google OAuth 2.0 實現第三方登入 | GrowingDNA 成長基因
- 為網路應用程式應用程式使用 OAuth 2.0 | Authorization | Google Developers
- OpenID Connect | Authentication | Google Developers
流程
- User 跟 Auth 說:我想要允許 Site 使用我的資料。
- Auth 回應 User :請把這個
code
轉交給 Site ,以讓 Site 等會向我證明他要的是您的資料。 - User 跟 Site 說:這是 Auth 給我的
code
。 - Site 跟 Auth 說:我收到這個
code
,請驗證。 - Auth 回應 Site :請用這個
token
向 Resource 要資料。 - Site 跟 Resource 說:我有這個
token
,我要操作 User 的資料。 - Resource 回應 Site 。
說明:
- 最後面的 6 跟 7 可重複進行,直到
token
過期。 - Auth 和 Resource 是一夥的,就本文而言即是 Google 的整個系統。他們之間有自己的溝通方式。
code
與token
都是亂數字串,都是用於驗證身分,但由於code
是經由 User 轉交的,又常是透過 HTTP Get ,因此有被使用者環境(如 Wifi 分享器)探知的風險。
故設計上code
只能用一次;token
才是反覆用來存取資料的。- 實際上第 1 步驟常是 User 點選 Site 網頁上的連結,而第 3 步驟常是 Auth 在回應中請瀏覽器轉址。
使用者的感受比較像是「Google 來問我,是否願意把資料給 Site 」,而不太會覺得是「我主動請 Google 提供資料給 Site 」,即使後者才是實情。
PHP 實作:讓使用者用 Google 登入
本文「不使用」 Google 提供的 PHP 程式庫 ,而全由原生 PHP 函數操作。
0. 準備
在 Google Cloud Console 建立專案
- 登入 Google 並連向 https://console.cloud.google.com/ 。
- 建立專案,進入「API 和 服務」。
- 點選「 OAuth 同意畫面」進行設定。
- 第一步驟中,網域可以不用填。
- 在第二步驟的「範圍」 (scope) 中,新增下列三個:
- …/auth/userinfo.email
- …/auth/userinfo.profile
- openid
- 第三步驟,將自己新增為「測試使用者」。
- 點選「憑證」
- 點選「建立憑證」→「OAuth 用戶端 ID 」。
- 「應用程式類型」選擇「網頁應用程式」。
- 在「已授權的重新導向 URI」,加入預計處理登入的頁面網址。
(如http://localhost/login.php
,頁面可能還不存在,本文後面才會寫。) - 存好「用戶端編號」(
client_id
) 和「用戶端密碼」(client_secret
)。
(吐槽:帳號比密碼還長…)
HTTP post request 函數
PHP 原生函數要發送 HTTP request 不像 JavaScript 有 fetch
那麼方便,以本文需求來說可以這樣:
1 | function http_post( |
注意即使是要進行 https
連線,倒數第二行的鍵值仍然必須是 http
。
(與本文無關,但:若是需要回覆的檔頭,可用 fopen()
和 stream_get_meta_data()
;若是要傳送檔案,則需使用 cURL 。)
接下來其實就是依照 Google 的文件 進行:
1. 生成連結,供使用者前往
即 OAuth 2.0 流程的步驟1 :User 跟 Auth 說「我想要允許 Site 使用我的資料。」
實作上是讓 User 連向 https://accounts.google.com/o/oauth2/v2/auth
,並以 GET 方式附上一些參數。如:
1 | $_SESSION['csrf_token'] = base64_encode(random_bytes(24)); // 32個亂數字元 |
redirect_uri
需與在 Google 專案中設定的「已授權的重新導向 URI」相同。
scope
不能超出專案中的「範圍」設定。除了一般開放資料之外, Google 建議採用漸進式授權,亦即,使用者有要用到某功能時,才去請求對應的授權。
state
雖然是可選,但有兩種常見應用:
- 記錄使用者原本在哪個頁面,方便驗證過後再轉址回去。這樣使用者體驗會比較好。
- 防止 #CSRF (跨站請求偽造, cross-site request forgery),詳參維基百科。
上述程式碼是將暫存的資料寫在 Session 裡,應該也有其他實作方式。
2. 接收使用者傳來的 code 等資料
使用者於 Google 那邊點選同意後,即會被 Google 轉址到 redirect_uri
,並在網址後加上 code
等資料,如 https://example.com/login.php?code=xxxxxxxx&state=yyyyyy
。
於此情形,即是在 login.php
中要處理 $_GET
。
1 | if($_GET['csrf_token'] !== $_SESSION['csrf_token']) |
在這裡可能要避免 CSRF ,我的作法如上。
3. 用 code 向 Google 要 token
這裡即需要 HTTP Post request 。用前述宣告的 http_post()
實作如下:
1 | $res_body = http_post('https://oauth2.googleapis.com/token', [ |
回來的資料是 JSON ,故用 json_decode()
即可轉成物件。
之後即可用 $access_token
向 Google 存取使用者的資料。
至此已成功驗證使用者有 Google 帳號,若沒有其他需求則可以到此就好。
4. 在取得 token 時也可取得的其他資料
上方程式碼中, $result
中除了 access token 之外,也有其他資料。整個 $res_body
是像:
1 | { |
expires_in
: token 還有幾秒有效。scope
: 實際授權範圍,以空格分隔,順序不一定。
refresh_token
只在步驟1時將 access_type
設為 offline
時出現,網站可據此直接向 Google 取得新的 access_token
,即使使用者已不在線上。方法類同步驟3 ,如下:
1 | $res_body = http_post('https://oauth2.googleapis.com/token', [ |
除了標示不同的兩處外, Google 的例示中這裡也不用 redirect_uri
。
(吐槽:為什麼 refresh_token
比 access_token
還短啊…)
我的測試經驗是:用 refresh_token
拿到的資料裡,也有 id_token
,但是解出來的資料「不一定」有姓名(given_name, family_name, name )的資訊。
id_token
只在步驟1時將 scope
設為包含 openid
時出現(當然在專案設定中也要有)。
依照 OpenID 協定,這是一個 JWT 字串 (JSON Web Token),也就是經過加密簽署的 base64url 編碼 JSON 物件。可以貼到 https://jwt.io/ 解碼,也可以用下列程式碼取得內容:
1 | // 延續步驟 3 |
上例中 base64url_decode()
是另外寫的函數,參閱PHP 處理 Base64 URL 的編碼、解碼方式 – Tsung’s Blog (longwin.com.tw),或 PHP 官網留言。
另注意 [RFC 4648](RFC 4648: The Base16, Base32, and Base64 Data Encodings (rfc-editor.org)) 其實有規範說 base64url
不應該被簡稱為 base64
。
id_token
包含資料可參閱 Google 的文件,可利用的有:
sub
: 使用者真正的 Google ID ,說是 ASCII ,但目前看來都是數字字元。(語源是 subject )email
: 若步驟1的scope
中有email
,這裡才會有。從這裡擷取的話,就不用多跟 Google 做一次連線。Google 表示「不適合做為主鍵。」- exp: 整數, token 將過期的實際時間,為 Unix 時間戳(1970 起經過的秒數)。注意與上方的
expires_in
是指「殘餘秒數」的意義不同。
亦即,其實不需要真的用到 access_token
,就已經可以取得使用者的最基本的資料。