今天將會是一個複雜的主題,因為要說明多個應用程式互相影響的情境。
首先先來完成之前沒做好的東西,remember 的設定會影響 LoginRequest 與 ConsentRequest 的 skip 值,但之前的範例程式碼並沒有跟著調整。
首先是 Login Provider 的調整:
$loginRequest = $adminApi->getLoginRequest($loginChallenge);
if ($loginRequest->getSkip()) {
// 固定使用 LoginRequest 的 Subject
$acceptLoginRequest = new AcceptLoginRequest([
'subject' => $loginRequest->getSubject(),
]);
$completedRequest = $adminApi->acceptLoginRequest($loginChallenge, $acceptLoginRequest);
return Redirect::away($completedRequest->getRedirectTo());
}
之前有大概說明如果沒設定 remember=true
,skip 會是 false。參考關鍵原始碼的實作,只要 s.forwardAuthenticationRequest()
的第四個參數 subject
為空字串,在建立 LoginRequest 的時候,skip 就會設定為 false。
以下解釋 Hydra 要向使用者請求驗證的規則(也就是 skip 是 false 的原因)有幾個:
prompt=login
參數)remember=false
的情境。(原始碼 authenticationSession()
方法實作在取 Session 的條件是 remember=true
)max_age
參數(單位為秒),且先前身分驗證時間至今已經超過這個設定。若是不在上面條件的話,skip 就會為 true。
再來是 Consent Provider 的調整:
$consentRequest = $adminApi->getConsentRequest($consentChallenge);
if ($consentRequest->getSkip()) {
// 固定使用 request 裡面的 scope
$acceptConsentRequest = new AcceptConsentRequest([
'grantScope' => $consentRequest->getRequestedScope(),
]);
$completedRequest = $adminApi->acceptConsentRequest($consentChallenge, $acceptConsentRequest);
return Redirect::away($completedRequest->getRedirectTo());
}
參考原始碼,其實跟 LoginRequest 很類似,會有多個情境是 Hydra 需要向使用者重新請求授權,規則如下:
prompt=consent
參數)remember=false
的情境。解讀原始碼 SQL 的理解是:找使用者最近一次(requested_at DESC
)在應用程式主動(skip=false
)同意授權,且有勾選 remember
的記錄。完成了之後,再實作出兩個不同的 RP 出來。Hydra 允許同個 Domain 下有兩個不同的 RP(事實上 OAuth 2.0 或 OpenID Connect 也沒有限制這件事),所以我們註冊新的兩個 RP:
hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
create \
--id rp1 \
--secret secret1 \
--grant-types authorization_code,implicit,client_credentials,refresh_token \
--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
--scope openid \
--token-endpoint-auth-method client_secret_basic \
--callbacks http://127.0.0.1:8000/rp1/callback \
--post-logout-callbacks "http://127.0.0.1:8000/rp1/logout/callback"
hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
create \
--id rp2 \
--secret secret2 \
--grant-types authorization_code,implicit,client_credentials,refresh_token \
--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
--scope openid \
--token-endpoint-auth-method client_secret_basic \
--callbacks http://127.0.0.1:8000/rp2/callback \
--post-logout-callbacks "http://127.0.0.1:8000/rp2/logout/callback"
程式的部分前幾天都說明過了,這邊就直接參考 GitHub commit。
目前設計可以發現身分驗證上的一個盲點,流程是這樣:
對授權來說,使用者同意授權是使用者信任個別應用程式的關係,因此取消授權是要個別獨立取消授權的。但對身分驗證來說,會是裝置與授權伺服器的狀態,也就是 Session ID 的狀態。因此合理的行為應該是:當單點登入(SSO)完成時,所有 RP 都能共享這個登入狀態,但在單點登出(SLO)的時候,所有 RP 也需要「共享」這個狀態。
為此 OpenID Connect 定義了幾個規範,能夠在這個情境將登出狀態「共享」給其他的 RP。
實際上登出的流程是很複雜的,本系列就只說明筆者有經驗的 Back-Channel Logout,所謂的 Back-Channel 指的是 Backend,也就是由後端來請求清除登入狀態。流程是,當 Hydra 收到登出請求的時候,再由 Hydra 通知應用程式所提供的端點清除,這個端點稱之為 backchannel_logout_uri
,每個應用程式都有各自獨立且唯一的端點。
這個 URI 是註冊應用程式的時候需要提供的,因此需要再更新一次設定(RP2 請讀者比照辦理):
hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
update rp1 \
--grant-types authorization_code,implicit,client_credentials,refresh_token \
--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
--scope openid \
--token-endpoint-auth-method client_secret_basic \
--callbacks http://127.0.0.1:8000/rp1/callback \
--post-logout-callbacks "http://127.0.0.1:8000/rp1/logout/callback" \
--backchannel-logout-callback "http://127.0.0.1:8000/api/rp1/logout/backchannel" \
--backchannel-logout-session-required true
這裡新增兩個參數,一個是 --backchannel-logout-callback
是指跟 Hydra 約定好當登出的時候要呼叫這個端點;另一個 --backchannel-logout-session-required
文件說明是指 OP 在呼叫 RP 的時候要帶 sid
的訊息,但觀察 Hydra 原始碼和實際測試,其實是沒有差別的。
開路由,Laravel 注意要開在 api.php
裡,因為 web.php
裡面的 POST 方法預設會擋 CSRF:
Route::post('/rp1/logout/backchannel', Rp1LogoutBackChannel::class)->name('rp1.logout.backchannel');
Route::post('/rp2/logout/backchannel', Rp2LogoutBackChannel::class)->name('rp2.logout.backchannel');
Hydra 會送一個 logout_token
的參數到 Logout backchannel:
$logoutToken = $request->input('logout_token');
它也是個 JWT,內容如下:
{
"aud": [
"rp2"
],
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
},
"iat": 1665020471,
"iss": "http://127.0.0.1:4444/",
"jti": "371f6287-bef1-41e6-848f-fa6fe7aa1053",
"sid": "942ab335-6857-4bb1-9616-f1e876c21d31"
}
驗證方法如下:
aud
驗證方法跟驗證 ID Token 的方法一樣。events
參數必須存在,且裡面要有 http://schemas.openid.net/event/backchannel-logout
key。iat
與當下時間不能差太多,可依應用程式特性決定要多久。iss
驗證方法跟驗證 ID Token 的方法一樣。jti
做防重送攻擊。驗證完成後,再來就是把 sid
拿來清除應用程式的登入狀態(如 Session)了。 說起來很簡單,但實際要做的困難點在於登入的時候必須要拿 sid
跟 session 做對應。這應該還不會很困難,不過加上明天要做的任務就會很困難了。
今天完成的程式沒有處理 Session 與驗證 Logout Token 的部分,但有開好路由,詳細請看 GitHub commit。