Token Revocation (RFC 7009)はなぜ重要か？

初版: 2020/12/16
著者: 田畑義之, 株式会社日立製作所 (GitHubアカウント: [@y-tabata](https://github.com/y-tabata))

# はじめに
OSSのIAM(Identity and Access Management)製品で、OAuthの認可サーバとしても使えるKeycloakが、バージョン10.0.0でToken Revocation (RFC 7009)をサポートしました[^1]。(ちなみに筆者が[PR #6704](https://github.com/keycloak/keycloak/pull/6704)でコミットしました。)
![release_note.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/78a9a704-c446-c831-9c35-d5804bd97530.png)

エンハンスリクエスト用のJIRAチケット[^2]にも多数のVoteが集まっており、このToken Revocationがいかに渇望されていたかがわかります。
![jira.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/a0106ae0-39d0-9a9a-b50e-3b53cf6de3b4.png)

今回は、OAuthの認可サーバにとって、このToken Revocationがいかに重要かということを説明しようと思います。Keycloakに関する詳細は、Think ITの連載「[Keycloakで実現するAPIセキュリティ](https://thinkit.co.jp/series/9721)」をご参照ください。

本記事は、あくまで執筆者の見解であり、日立製作所及びRed Hatの公式なドキュメントではありません。

# Token Revocation (RFC 7009)とは
Token Revocationとは、[RFC 7009](https://tools.ietf.org/html/rfc7009)で定義されているトークン無効化方法です。認可サーバがtokenパラメータで渡されたトークンを無効にすることで、アプリケーション(RP)は以降当該トークンを用いたAPIコールないしトークンリフレッシュができなくなります。

```none:Token&#x20;Revocationリクエスト例
POST /revoke HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
```

ではログアウトとはどう違うのでしょうか。Keycloakは初期のころから、Logout Endpointを通じたログアウトをサポートしています。ログアウトした場合も、認可サーバがログアウトしたユーザのトークンを無効にすることで、アプリケーションは以降当該トークンを用いたAPIコールないしトークンリフレッシュができなくなります。

以降、ログアウトを引き合いに出しつつ、Token Revocationの必要性を説明していきます。

## Token Revocationとログアウトの違い
近年、様々なアプリケーションのログイン画面で、「GitHubアカウントでログイン」や「Twitterアカウントでログイン」といったようなものをよく見かけるかと思います。
![login_screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/52e88c7f-a039-b5ae-6ba1-75e750e579c2.png)
出典: https://qiita.com/

これは、アプリケーションがGitHubやTwitterの認可サーバに認証を委譲していることを意味します。つまり、今や1つのユーザアカウントで複数のアプリケーションにログインするということは日常的に行われています。
![multi_application.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/4f86b318-8212-017a-3a9e-c80c484f27fb.png)

この状態で、ユーザAがアプリケーション1からログアウトすることを考えます。
ユーザAによるログアウト操作で、アプリケーション1がLogout Endpointをコールしたとしましょう。
![call_logout_endpoint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/fac50fb3-7ecb-aad9-501f-38a18b8f80d1.png)

すると、ユーザAのトークンが無効になります。このとき問題が発生します。ユーザAは、アプリケーション1のみからログアウトしたつもりが、なぜかアプリケーション2からもログアウトされてしまいます。1回のログアウトですべてのアプリケーションからログアウトできるというのは、SSO用途を考えれば便利な機能ですが、今回のようなユースケースでは、ユーザビリティに課題があります。
![logout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/c9ef5bc8-cfba-0608-3baf-588e46614990.png)

一方、ユーザAによるログアウト操作で、アプリケーション1がToken Revocation Endpointをコールした場合はどうでしょうか。
![call_token_revocation_endpoint.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/08ec5f0f-c8e8-b951-124e-d57e567a1e6a.png)

この場合は、Token Revocation Endpointコール時にtokenパラメータに指定したトークン、つまりアプリケーション1に払い出されたトークンのみが無効になるため、アプリケーション2からログアウトされることはありません。
![revoke.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/30f79eb0-b2d7-c917-3aa7-2f5b17a8b79d.png)

以上より、昨今のこのようなユースケースにおいて、このToken Revacationがいかに重要な機能かをお分かりいただけたかと思います。

# KeycloakのToken Revocationを使ってみる
Keycloakを用いて、Token Revocationを試してみましょう。Keycloakは2020/11/27時点のmasterブランチのものを使用します。

まずは2つのアプリケーションに同一ユーザでログインします。今回はsample_applicationというアプリケーションとaccount-consoleというアプリケーションにログインしました。
![multi_client_sessions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/2493668f-b19d-6cc4-5bcf-58ce541c4389.png)

まずは、Logout Endpointを叩いてみましょう。

```bash
$ curl -i http://keycloak.example.com:8080/auth/realms/
sample_service/protocol/openid-connect/logout -d "refresh_token=$refresh_token&c
lient_id=sample_application&client_secret=***"
HTTP/1.1 204 No Content
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Referrer-Policy: no-referrer
Content-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none';
Date: Fri, 04 Dec 2020 01:56:49 GMT
X-Robots-Tag: none
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
```

想定通り、セッションが空になり、2つのアプリケーションの両方からログアウトされました。
![no_session.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/45e8e2f3-7e16-345b-33f4-e133add5a9c5.png)

次に、再度2つのアプリケーションに同一ユーザでログインし、今度はToken Revocation Endpointを叩いてみましょう。ここでは、sample_applicationに発行したトークンを無効にします。

```bash
$ curl -i http://keycloak.example.com:8080/auth/realms/sample_service/protocol/openid-connect/revoke -d "token=$refresh_token&client_id
=sample_application&client_secret=***"
HTTP/1.1 200 OK
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Referrer-Policy: no-referrer
Content-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none';
Date: Fri, 04 Dec 2020 02:02:30 GMT
Connection: keep-alive
X-Robots-Tag: none
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Content-Length: 0
```

想定通り、sample_applicationのセッションのみが削除され、account-consoleのセッションが残りました。
![revoke_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267112/e4309fcd-0354-41e5-0896-a16d0f2bffbe.png)


ちなみに、Keycloakは[RFC 8414](https://tools.ietf.org/html/rfc8414)、[RFC 8615](https://tools.ietf.org/html/rfc5785)に準拠したAuthorization Server Metadataを/auth/realms/{realm}/.well-known/openid-configurationというエンドポイントで公開しており、そのエンドポイントからも今回使用した2つのエンドポイントを確認できます。(Authorization Server MetadataへのToken Revocation Endpointの追加も筆者が[PR #7106](https://github.com/keycloak/keycloak/pull/7106)でコミットしました。)

```bash
$ curl http://keycloak.example.com:8080/auth/realms/sample_service/.well-known/openid-configuration | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3371  100  3371    0     0   219k      0 --:--:-- --:--:-- --:--:--  235k
{
  ...
  "end_session_endpoint": "http://keycloak.example.com:8080/auth/realms/sample_service/protocol/openid-connect/logout",
  ...
  "revocation_endpoint": "http://keycloak.example.com:8080/auth/realms/sample_service/protocol/openid-connect/revoke",
  "revocation_endpoint_auth_methods_supported": [
    "private_key_jwt",
    "client_secret_basic",
    "client_secret_post",
    "tls_client_auth",
    "client_secret_jwt"
  ],
  "revocation_endpoint_auth_signing_alg_values_supported": [
    "PS384",
    "ES384",
    "RS384",
    "HS256",
    "HS512",
    "ES256",
    "RS256",
    "HS384",
    "ES512",
    "PS256",
    "PS512",
    "RS512"
  ],
  ...
}
```

# おわりに
本稿では、Token Revocationについてご紹介しました。Token Revocationをサポートしたことで、KeycloakはOAuth周りの機能を一通りそろえたとともに、現在はFAPI (Financial-grade API)への対応も絶賛推進中です[^3]。是非この機会にKeycloakを使ってみてはいかがでしょうか。



[^1]: [Keycloak 10.0.0のリリースノート](https://www.keycloak.org/docs/latest/release_notes/index.html#oauth-2-0-token-revocation-rfc-7009)
[^2]: [Token Revocationのエンハンスリクエスト用のJIRAチケット](https://issues.redhat.com/browse/KEYCLOAK-5325)
[^3]: [FAPI-SIG](https://github.com/keycloak/kc-sig-fapi)