Keycloak を用いたリフレッシュトークンローテーション検証方法 メモ

* リフレッシュトークンローテーション機能をKeycloakを使って試してみたのでメモしておく。

## リフレッシュトークンとは

* アクセストークンの有効期限が切れた際に新しいアクセストークンを要求するために使用するトークン。
* ネイティブアプリで使用されることが多い。

## リフレッシュトークンローテーションとは

* リフレッシュトークンを用いてアクセストークンを再発行する際に新しくリフレッシュトークンも再発行し、以前のリフレッシュトークンを無効化する機能。

  ![RT.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586535/bb786199-af7b-71f1-3c3a-e800cc2a8075.png)


## Keycloakでの検証

### 設定

* 「Realm Settings」->「Tokens」を選択
* 「Revoke Refresh Token」を「ON」に設定
* 「SSO Session Idle」を30に設定※デフォルトの1のままだと「token is not active」になったため。



### 動作確認

1. リフレッシュトークン発行※認可コードグラントなどでリフレッシュトークンを発行する。

2. 1で発行したリフレッシュトークンを使用してトークン再発行

   リクエスト

   ````http
   POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
   Host: localhost:8080
   Content-Type: application/x-www-form-urlencoded
   Content-Length: 784
   
   client_id=python-client&client_secret=a07...213d1&refresh_token=eyJhb...DZag&grant_type=refresh_token
   ````

   レスポンス

   ```json
   {
       "access_token": "eyJhbGci...8eKzMz_kJ8yyvyQ",
       "expires_in": 60,
       "refresh_expires_in": 225,
       "refresh_token": "eyJhb...lOToY",
       "token_type": "Bearer",
       "id_token": "eyJhbGciOiJSUzI1NiI..._hWf40po2oMf7w",
       "not-before-policy": 0,
       "session_state": "db0815aa-3ac9-429c-9f59-f789bbe1953b",
       "scope": "openid profile email"
   }
   ```

3. 1世代前のリフレッシュトークン無効化確認

   リクエスト※2のリクエストで指定したリフレッシュトークンを指定

   ```http
   POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
   Host: localhost:8080
   Content-Type: application/x-www-form-urlencoded
   Content-Length: 784
   
   client_id=python-client&client_secret=a07...213d1&refresh_token=eyJhb...DZag&grant_type=refresh_token
   ```

   レスポンス

   ```json
   {
       "error": "invalid_grant",
       "error_description": "Token is not active"
   }
   ```

   ※2のリクエストで指定したリフレッシュトークンが無効化されていることがわかる。

4. 新規発行リフレッシュトークンの有効性確認

   リクエスト※新規発行されたリフレッシュトークンを使用する。

   ```http
   POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
   Host: localhost:8080
   Content-Type: application/x-www-form-urlencoded
   Content-Length: 784
   
   client_id=python-client&client_secret=a07...213d1&refresh_token=eyJhb...lOToY&grant_type=refresh_token
   ```

   レスポンス

   ```
   {
       "access_token": "eyJhbGciOiJS....30BE04DYQ",
       "expires_in": 60,
       "refresh_expires_in": 497,
       "refresh_token": "eyJhbGciO...28ZoQyPY",
       "token_type": "Bearer",
       "id_token": "eyJhbGciOiJSUzI1NiIsIn...hdQ",
       "not-before-policy": 0,
       "session_state": "cd77a6ba-eda2-4ecc-a0bb-7248cd9695fa",
       "scope": "openid profile email"
   }
   ```

   

### Keycloakリフレッシュトークン構造

* ヘッダー部

  ```json
  {
    "alg": "HS256",
    "typ": "JWT",
    "kid": "5361b8f...9ac2eb"
  }
  ```

  

* ペイロード部

  ```json
  {
    "exp": 1648350302,
    "iat": 1648350077,
    "jti": "09265c39...c6181bb52",
    "iss": "http://localhost:8080/auth/realms/master",
    "aud": "http://localhost:8080/auth/realms/master",
    "sub": "dc6f27d...dd61e64cd",
    "typ": "Refresh",
    "azp": "python-client",
    "session_state": "db0815...be1953b",
    "scope": "openid profile email",
    "sid": "db0815...e1953b"
  }
  ```