iT邦幫忙

2022 iThome 鐵人賽

DAY 22
1
Software Development

30 天與九頭蛇先生做好朋友系列 第 22

清除所有登入狀態

  • 分享至 

  • xImage
  •  

昨天實作的內容是登出流程,這個指的是使用者「主動」按下登出的過程。因為登入應用程式時,是透過 Session ID 來標識相同裝置的,因此即便使用者登入很多不同的應用程式,Hydra 還是能透過 Session ID 查到這些資訊。

但,不同裝置就無法透過這個方法來實現了。比方說使用者有 Mac 與 Windows 兩台電腦,則兩台電腦都完成登入後,Mac 登出是不會影響 Windows 的,因為這兩台裝置所記錄的 Session ID 是不同的。

假想下面幾個情境

  1. 今天有個攻擊者從某些管道取得使用者的帳號密碼,並登入某個裝置,而真實的使用者也發現了攻擊者登入他的帳號。這時該如何把攻擊者的登入狀態移除呢?
  2. 同上面的問題,事實上有幾個攻擊者已經猜不出來了。假設想把全部的登入狀態都移除的話,該怎麼做?

兩個問題很相似,但有點不同,一個是針對特定裝置的登入狀態移除,另一個是移除所有登入狀態。這次 30 天鐵人賽只說明後者,把所有登入狀態清除。而移除特定裝置的登入狀態,就看還有沒有力氣寫第 31 天了。

清除應用程式端的登入狀態

昨天的 Backchannel 其實就是清除應用程式端的登入狀態的方法了,只是昨天的情境是 Hydra 主動發出,實際上 Back-Channel Logout 有定義另一個欄位是 sub

這個欄位跟 sid 是互有關係的,下表是這兩個欄位相互組合的意義:

sub sid 說明
沒有 沒有 不合法的 Token
沒有 清除指定 Session 的狀態
沒有 清除指定使用者的狀態
清除指定使用者,且指定 Session 的狀態。

從這個表可以了解,其實如果要清除應用程式端的狀態,只要丟一個帶有 sub 欄位的 Logout Token 就行了。但因為 Hydra 沒實作這件事,所以只能自己來產 JWT,還好 Hydra 有提供取 JWK 的 API:PUT /keys/{set}/{kid},從 DB table hydra_jwk 可以查得到,set 是指 hydra.openid.id-token,而 kid 就是指 private:hydra.openid.id-token 了。會這麼選擇的原因是,Back-Channel Logout 協定裡有提到驗證簽章的方法跟 ID Token 一樣;另外就是,等下是要實作簽章而不是驗證,因此需要私鑰。

$response = $admin->getJsonWebKey('private:hydra.openid.id-token', 'hydra.openid.id-token');

$privateKey = $response->getKeys()[0]->jsonSerialize();

再來是實作簽章的程式,之前驗證的套件有大概看過,這次要用 JWSBuilder。整體寫起來如下:

private function buildLogoutToken(array $privateKey, string $client, string $sub): string
{
    $jwsBuilder = new JWSBuilder(new AlgorithmManager([
        new RS256(),
    ]));

    $jws = $jwsBuilder
        ->withPayload(json_encode([
            'aud' => [
                $client,
            ],
            'events' => [
                'http://schemas.openid.net/event/backchannel-logout' => new stdClass(),
            ],
            'iat' => time(),
            'iss' => 'http://127.0.0.1:4444/',
            'jti' => Str::uuid()->toString(),
            'sub' => $sub,
        ]))
        ->addSignature(new JWK($privateKey), ['alg' => 'RS256',])
        ->build();

    $serializer = new CompactSerializer();

    return $serializer->serialize($jws);
}

JWSBuilder 要小心一個問題是,它是有狀態的物件,因此如果要產生新的 JWS 時,需要再使用 create() 方法清除狀態。上面因為 function 結束後,它的生命週期就結束了,所以沒有這個問題。

接著要取得所有應用程式,這個 Hydra 也有提供 API 了:

$clients = $admin->listOAuth2Clients()

最後就是每個應用程式都送出清除使用者狀態的請求就行了。

collect($clients)
    ->filter(fn(OAuth2Client $client) => !empty($client->getBackchannelLogoutUri()))
    ->each(function (OAuth2Client $client) use ($sub, $privateKey) {
        $logoutToken = $this->buildLogoutToken($privateKey, $client->getClientId(), $sub);
        $uri = $client->getBackchannelLogoutUri();

        Http::post($uri, [
            'logout_token' => $logoutToken,
        ]);
    });

授權伺服器端的實作不難,困難的地方在應用程式端的實作。目前覺得最適合的作法如下:

  1. 記錄 sid 對應用程式 Session 的 mapping(以 Redis 來說,使用 Key Value 即可)
  2. 記錄 subsid 的 mapping(以 Redis 來說,可以使用 SET)

當清除 sid 的時候,除了第一個記錄要清外,第二個記錄也要清。

當清除 sub 的時候,使用第二個記錄把相關的 sid 找出來,就能把第一個記錄清掉。

當兩個資訊都有的時候,先從 sub 找 sid,有找到的話就照清除 sid 的步驟處理。

應用程式清除 Session 程式碼就先偷懶不實作了。

清除 Hydra 的登入狀態

即便把應用程式的狀態清除,因為 Hydra 的登入狀態還在(remember 功能),因此在按下應用程式登入的時候,依然會被 skip 而跳過身分驗證流程,因此需要再呼叫 Hydra 的一個 API 來清除登入狀態:

$admin->revokeAuthenticationSession($sub)

程式碼可以參考 GitHub Commit


上一篇
通知應用程式登出
下一篇
Metadata 資訊兼斷賽
系列文
30 天與九頭蛇先生做好朋友23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言