Keycloakのオフライントークンについて

日立製作所　中村雄一

この記事では、Keycloakのオフライントークン（Offline Token）について紹介します。APIの認可認証のユースケース等で、Keycloakにおけるリフレッシュトークンの扱いに不満がある場合に必要になってくるものです。

# アクセストークンとリフレッシュトークン
オフライントークンの詳細を見ていく前に、アクセストークンとリフレッシュトークンについて復習します。REST APIの認可・認証にOAuth/OpenID Connectを使うケースが金融分野を中心に広まっています。クライアントがＡＰＩ連携する際に、OAuth/OpenID Connectのフローの中で認証が行われ、クライアントに「アクセストークン」が発行されます。クライアントはこのアクセストークンを付与してＡＰＩコールをします。ＡＰＩサーバ側ではアクセストークンを利用して、処理を行います。
ここで、アクセストークンの有効期限は一般に短く(数分や数時間等)設定されます。アクセストークンは、「持っている人を信じる（ベアラトークン）」という考え方ですので、仮に盗まれてしまうと、簡単になりすましてＡＰＩコールされてしまうためです。しかし、アクセストークンの有効期限が切れるたびに、毎回再認証では不便ですから、「リフレッシュトークン」が発行されることが多いです。リフレッシュトークンは一般に有効期限が長く（数か月等）設定されます。リフレッシュトークンさえクライアントに残っていれば、アクセストークンを再発行することができます。なお、アクセストークン再発行の際にクライアント認証が必要となるため、単にリフレッシュトークンを盗んだけでは悪さはできません。このリフレッシュトークンを活用することで、リフレッシュトークンが切れた場合のみ再認証が必要、とすることができます。

#Keycloakでのリフレッシュトークンの取扱いの注意点
　Keycloakは、リフレッシュトークンをinfinispanというKVSに保管しています。Keycloakのクラスタ構成を組むことでリフレッシュトークンもレプリケーションすることができます。しかし、クラスタを全台停止・再起動した場合には、リフレッシュトークンは失われます。さらに、Keycloakのバージョンアップを行う際にはinfinispanのデータは引き継がれないため、リフレッシュトークンは失われます。
　リフレッシュトークンが失われると、クライアント側で再認証が必要になります。例えば、一度認証した後に、自動的にＡＰＩをバックグラウンドでコールするようなケースでは、再認証しない限り自動的なＡＰＩコールが止まってしまうので、問題になりえます。

# オフライントークンとは
このように、Keycloakのリフレッシュトークンは失われる可能性があり、ユースケースによっては問題になりえます。リフレッシュトークンを永続化したい！という場合に役に立つのが今回紹介する「オフライントークン」です。オフライントークンという用語はKeycloakの独自用語であり、一般的な技術用語では「リフレッシュトークン」です。ただし、リフレッシュトークのうち、「データベースに保存され永続化される」「scopeにoffline_accessの指定される」リフレッシュトークンを特に「オフライントークン」と呼んでKeycloakでは区別しています。「offline_access」は、OpenID Connectの仕様に定められているscopeであり、詳細な説明は省きますが、バックグラウンドでの自動的なAPIコールのようなユースケースを想定して策定されたパラメタです。

つまり、Keycloakでは、offline_accessをscopeに指定することで、リフレッシュトークンが内部的に「オフライントークン」と見なされ、ＤＢに永続化されます。またバージョンアップ時の移行方法もマニュアルに記載されています。オフライントークンを使うことにより、リフレッシュトークンの消失可能性を減らすことができます。
　一方、デメリットとしては、DBに永続化されるため、infinispanよりパフォーマンスが落ちる可能性があるのと、Keycloak4.1.0以前では、有効期限の設定が不足している点です(後述)。

## オフライントークンを試してみる
　実際にオフライントークンの振る舞いを、通常のリフレッシュトークンと比較してみていきます。
　今回、Keycloakにて、”sample”というレルムを作り、”client”という名前のクライアント(localhostにKeycloakと同居)がアクセストークン・リフレッシュトークンを要求します。

### 通常のリフレッシュトークンの場合
下記のように、クライアントが、Keycloakの認可エンドポイントに、scopeを「openid」のみでリクエストを送ります。

```
http://localhost:8080/auth/realms/sample/protocol/openid-connect/auth?client_id=client&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=openid
``` 

認証を行いトークンリクエストを行った結果、下記のようなリフレッシュトークンが返ってきました(実際はエンコードされている)。Scopeに「offline_access」は入っていません。

```json
{
  "jti": "8fb26726-fddf-4766-8c07-2f151a41ce9c",
  "exp": 1542861091,
  "nbf": 0,
  "iat": 1542859291,
＜略＞
  "scope": "openid email profile"
}
```

　ここで、Keycloakを強制的に再起動し、上記リフレッシュトークンを使ってクライアントからアクセストークンをリクエストすると、下記のようにエラーが返ってきます。

リクエスト↓

```
url:
http://localhost:8080/auth/realms/sample/protocol/openid-connect/token
body:
`grant_type=refresh_token&refresh_token=<refresh_token>&client_id=<client_id>&client_secret=<client_secret>`
```

エラー↓

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

これは、再起動によりキャッシュ中のリフレッシュトークンが消えてしまったからです。

###オフライントークンの場合
　では、オフライントークンを試してみます。クライアントからの認可エンドポイントへのリクエストにて、下記のように「offline_access」を付加します。

```
http://localhost:8080/auth/realms/sample/protocol/openid-connect/auth?client_id=client&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=openid+offline_access
```

　認証とトークンリクエストの結果、同様にリフレッシュトークンが返ってきます。Scopeに「offlien_access」が追加されているのが違いです。

リフレッシュトークン↓

```json
{
  "jti": "3fadb585-cf6c-4e25-8382-ab8b83fdaf94",
  "exp": 0,
  "nbf": 0,
  "iat": 1542859546,
＜略＞
  "scope": "openid email profile offline_access"
}
```
　ここで、Keycloakを強制的に再起動させます。リフレッシュトークン(＝オフライントークン)を使ってアクセストークンをリクエストすると、今度はエラーは返らずに、正常に動作します。

##オフライントークンをクライアントから削除する
クライアント側から、オフライントークンを削除したい場合は、logoutエンドポイントを使います。リフレッシュトークンの場合とまったく同じです。
リクエスト例を記します。

```
url:
http://localhost:8080/auth/realms/sample/protocol/openid-connect/logout
body:
`refresh_token=<refresh_token>&client_id=<client_id>&client_secret=<client_secret>`
```

 
logoutエンドポイントへのリクエストが成功した状態でリフレッシュトークンを使おうとすると、下記のエラーが出るようになり、リフレッシュトークンを使うことができません。

```
{"error":"invalid_grant","error_description":"Offline user session not found"}
```

# オフライントークンの有効期限について注意点
　Keycloakでは、内部的にはリフレッシュトークンとオフライントークンが区別して扱われているために(内部的にはオフライントークンは「offline sesssion」の一部として管理)、有効期限設定の項目が異なることに注意が必要です（表）。別々に設定を行う必要があります。


|リフレッシュトークンの設定項目|オフライントークンの設定項目|意味(API連携の場合)|
|--:|--:|--:|
|SSO Session Idle|Offline Session Idle|リフレッシュトークンを一定期間使わない場合に無効になる時間を指定|
|SSO Session Max|Offline Session Max|リフレッシュトークン発行後に強制的に無効になる時間を指定|

上記表の詳細はKeycloakのドキュメントを参照ください。[公式サイト](https://www.keycloak.org/documentation.html)
より、所望のバージョンを指定、「Server Administration Guide」の「User Session Management」の章に記載があります。

なお、ここで設定項目「Offline Session Max」は、Keycloak 4.1.0以前には存在しないため、古いバージョンではオフライントークン発行後に強制的に無効化する時間を指定できません。なお、日立の@tnorimatさん(https://github.com/tnorimat )がパッチ投稿（https://github.com/keycloak/keycloak/pull/5307/ )してOffline Session Maxをサポートするようにし、4.1.0以降で使えるようになっています。

以上でオフライントークン紹介は終わりです。少々ややこしいですが、OpenID Connectの仕様に忠実に実装しようとした結果こうなっていると推察されます。Keycloakは「真面目に」仕様を実装していると思いました。