Keycloak と Spring Security で Token Introspection

株式会社 日立製作所の茂木です。今回は Keycloak と Spring Security の連携方法を紹介します。

OAuth ではアクセストークンの有効性を確認する方法として、[RFC 7662 OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662)があります。Token Introspection は、アクセストークンの有効性やトークンに紐づくユーザ情報を Authorization Server から取得する方法を定めたものです。一般的にはアクセストークンが Opaque (不透明：ランダムな文字列など)である場合に利用されますが、アクセストークンの状態(ログアウトなど)を即座に反映できるため、高いセキュリティが必要な場合にも有用です。

今回は Spring Security が提供する機能を利用して、Token Introspection を利用する Resource Server を作成してみます。

# OAuth 2.0 Resource Server

Spring Security のモジュールである OAuth 2.0 Resource Server は、アクセストークンの検証や認証情報の取得といった機能を提供します。このモジュールを利用すると、OAuth の Resource Server を簡単に作成できるようになります。Spring Security 5.1 の時にはアクセストークンが JWT 形式にしか対応していませんでしたが、5.2 からは Token Introspection も利用できるようになりました (Keycloak はどちらの方法にも対応しています)。

ちなみに [Spring Security OAuth](https://spring.io/projects/spring-security-oauth) というライブラリがありますが、こちらは今回扱うものとは別物です。現在は Deprecated となっており、利用は推奨されていませんので、調べる際は注意が必要です。

# 動作検証

Spring Security の OAuth 2.0 Resource Server と Keycloak を利用して、Token Introspection によるアクセス検証動作の確認をしてみます。

## 検証環境

検証に利用した環境は以下の通りです。

- Keycloak 8.0.0
- Spring Boot 2.2.1
- Spring Security 5.2.1.RELEASE
- oauth2-oidc-sdk 6.14 (後述)

## Spring Initializr でのプロジェクト作成

まずは [Spring Initializr](https://start.spring.io/) を利用して Spring Boot のプロジェクトを作成します。Dependencies では下記のように、Spring Web と OAuth2 Resource Server を選択してください。

![spring-initializr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273221/bac4bffe-e627-8929-e3da-9b8802f0ae12.png)

Spring Security の OAuth/OpenID Connect 周りの機能には、`oauth2-oidc-sdk`というライブラリが利用されています。依存関係が Optional になっているため、依存関係を追加する必要があります。今回は Spring Security の[プロジェクトで指定されている](https://github.com/spring-projects/spring-security/blob/5.2.1.RELEASE/gradle/dependency-management.gradle#L60)`6.14`を指定しました。

```xml
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>oauth2-oidc-sdk</artifactId>
            <version>6.14</version>
        </dependency>
```

以上でプロジェクトの作成は完了です。

## Spring Boot の設定

次に`application.yaml`に Token Introspection の設定を行っていきます。設定に必要な値は Token Introspection Endpoint の URL と、Token Introspection を実行するために必要なクライアント認証情報(Client ID, Client Secret)です。

Keycloak の Token Introspection Endpoint は `/auth/realms/<Realm名>/protocol/openid-connect/token/introspect`ですので、その値を`introspection-uri`に指定します。Client Secret は Keycloak の設定時に生成されるので、後ほどその値を指定します。

```yaml
server:
  port: 8000
spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: http://localhost:8080/auth/realms/sample/protocol/openid-connect/token/introspect
          client-id: sample
          client-secret: <client_secretの値>
```

次にセキュリティの設定を行います。`WebSecurityConfigurerAdapter`を継承したクラスを作成し、アクセス制御に関する設定を行っていきます。Spring Security 内部では scope の値は `SCOPE_`という Prefix がついた Authority として保持されるので、これを利用して scope ベースのアクセス制御も行えます。

```java
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(authorizedRequest -> {
            authorizedRequest
                .antMatchers("/me").hasAuthority("SCOPE_profile")
                .anyRequest().authenticated();
        })
        .oauth2ResourceServer().opaqueToken();
    }
}
```

最後に適当な`RestController`を作成します。

```java
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

    @RequestMapping("/")
    public String index() {
        return "hello";
    }

    @RequestMapping("/me")
    public Object profile(@AuthenticationPrincipal BearerTokenAuthentication auth) {
        return auth.getTokenAttributes().get("preferred_username");
    }
}
```

OAuth 2.0 Resource Server は Token Introspection で取得したユーザ情報を`BearerTokenAuthentication`に格納します。Controller でトークンに紐づくユーザ情報を利用する場合は、このインスタンスから情報を取得して利用します。

以上の設定でリクエストヘッダに付与されたアクセストークンを、Token Introspection で検証するようになります。

## Keycloak の設定

次に Keycloak の設定をしていきます。Token Introspection では、リクエストを実行するために Client ID, Client Secret による認証が必要になるので、下記のように Client 情報を登録します。

![クライアントの追加](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273221/ea522be7-9d71-afd9-6e25-9b2db3eb3552.png)

ここで登録した Client ID 及び生成された Client Secret の値を、先ほどの`application.yaml`に指定します。

また、Token Introspection が実行されていることを確認するため、Keycloak の Event Log を ON に設定します。

![event-log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273221/90e52a40-8a01-cc62-d3d9-12cc0832aec3.png)


Saved Type に`INTROSPECT_TOKEN`が入っていれば、Token Introspection 実行時にログが残ります。

# 動作確認

まずは、下記のコマンドを利用して Spring Boot アプリケーションを起動します。

```sh
$ mvn spring-boot:run
```

この際、`oauth2-oidc-sdk`の依存関係が抜けていると、下記のような`ClassNotFoundException`が発生して起動に失敗します。エラーが発生した際は依存関係を見直してみてください。

```sh
Caused by: java.lang.ClassNotFoundException: com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse
```

アプリケーションが起動したら、適当な REST クライアントなどを利用して Keycloak からアクセストークンを取得してリクエストを送信します。リクエストが成功した後に Keycloak の Event Log を確認すると、下記のように`INTROSPECT_TOKEN`イベントが発生していることが確認できます。

![result-log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273221/8c6c7943-b43f-5135-3046-4aaf4237ba15.png)

また、Scope を減らしたアクセストークンを利用した場合は、`403`エラーレスポンスとともに下記の`WWW-Authenticate`ヘッダが返ってきて、Scope によるアクセス制御が成功していることも確認できます。

```http
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
```

# 最後に

Keycloak は Token Introspection などの OAuth 関連仕様が実装されています。そのため、OAuth ライブラリを利用すれば簡単に Keycloak と連携させることができます。Keycloak を利用してシステムを構築する場合、「Keycloak の使い方」だけでは済まない場合が数多くあるので、こういった他のライブラリとの連携方法に関しても情報を発信していきたいと思っています。