6
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

UL Systems (ウルシステムズ)Advent Calendar 2019

Day 6

KeycloakのSpring Bootアダプターでクライアントクレデンシャルグラントを実装する

Last updated at Posted at 2019-12-17

はじめに

OpenID Connect/OAuth2の認可サーバーとして広く使われているKeycloakですが、次のようなケースではどうしたら良いでしょうか?

  • ユーザーがログインする前やログアウトした後に、ユーザーの状態に関係なく、クライアント自身に与えられた権限でアクセストークンで保護されたリソースサーバーを利用したい
  • WebAPIやバッチなど、サービスプロセス自体で認証をかけてリソースサーバーを利用したい

このような場合、OAuth2のクライアントクレデンシャルグラントというグラントタイプを使って、クライアント認証によるアクセストークンを取得することができます。

今回は、KeycloakのJavaアダプターの一つであるKeycloak Spring Boot Adapterを使って、Keycloak Serverに対するクライアント認証を簡単に実装する方法を示します。
このようなケースの情報は少なく、最初は実装に苦労したので、誰かの参考になれば幸いです。

構成

手軽さを重視して、WindowsのローカルPC上に全ての構成を準備することにします。

kcadapter2.png

役割 アプリケーション
認可クライアント Spring Boot 2.2.2.RELEASE
Spring WebMVC/Security 5.2.2.RELEASE
Keycloak Spring Boot Adapter 8.0.1
認可サーバー Keycloak Server 8.0.1
認可リバースプロキシ Keycloak Gatekeeper 8.0.1
バックエンドAPI/ネットワークキャプチャ Fiddler Web Debugger v5.0.20194.41348
ブラウザ Google Chrome 79.0.3945.79
認可クライアント/サーバー用JDK OpenJDK 11

認可クライアント

今回のターゲットである、Spring BootによるRest APIです。
起動時にクライアントクレデンシャルグラントを使ったクライアント認証を認可サーバーへリクエストし、アクセストークンを取得します。
ブラウザからリクエストを受けると、アクセストークンを付加して認可リバースプロキシにリクエストし、バックエンドAPIからのレスポンスをそのまま出力します。

なお、アクセストークンには有効期限がありますが、クライアントクレデンシャルグラントではリフレッシュトークンは発行されないため、期限切れになる前にアクセストークンの再取得が必要になります。このために、クライアント認証のリクエストを定期的に行うようにスケジュールタスクを設けます。

認可サーバー

もちろん、Keycloak Serverを使います。

認可リバースプロキシ

昨年の UL Systems Advent Calendar 2018 の Keycloak GateKeeperの記事 でも取り上げた Keycloak GateKeeper を使って、バックエンドAPIをアクセストークンで保護します。
Keycloak GatekeeperはGo言語で実装されたアプリケーションで、APIの手前で OpenID Connect / OAuth2 に対応する認可リバースプロキシとして動作します。

ネットワークキャプチャ

HTTPリクエストとレスポンスを可視化するネットワークキャプチャとして、Fiddler Web Debugger(以下、Fiddler)を使います。

バックエンドAPI

FiddlerのAutoResponder機能を使って、リクエストに応じた固定のレスポンスを設定することにより、疑似的にバックエンドAPIの振る舞いをさせます。

認可クライアントの実装と起動

それでは、今回のターゲットである認可クライアントをSpring Bootをベースに実装します。

Spring Bootプロジェクトを生成する

最初に、Spring Initializr で以下の設定によりプロジェクトを生成します。

項目 設定
Project Gradle Project
Language Java
Spring Boot 2.2.2
Group com.example
Artifact keycloakadapter
Java 11
Dependencies Web, Security

build.gradleにKeycloak Spring Bootアダプターを設定する

次に、build.gradleのdependenciesに以下の行を加えて、ビルドを行います。

build.gradle
	implementation 'org.keycloak:keycloak-spring-boot-starter:8.0.1'

起動クラスにスケジュール利用を設定する

起動クラスであるKeycloakadapterApplicationに、@EnableSchedulingを設定します。

KeycloakadapterApplication.java
@EnableScheduling
@SpringBootApplication
public class KeycloakadapterApplication {
   ...
}

スケジュールタスクを実装する

(1) 認可サービスクライアント機能をセットアップする

認可クライアント機能で必要となる以下のクラスのインスタンスをセットアップします。

クラス名 説明
KeycloakDeployment KeycloakのエンドポイントURLやアクセス設定を保持する
AuthzClient Keycloakの認可サービスに対するクライアント機能を提供する

KeycloakDeploymentは、Spring Bootプロパティを保持するKeycloakSpringBootPropertiesから生成できます。
AuthzClientの生成についてはインタフェースが提供されておらず一筋縄では行かないため、Keycloakアダプターに含まれているPolicyEnforcerという認可サービス利用時のクラスのコードを参考に実装します。

※Keycloakの認可サービスそのものは今回は使用しませんが、詳細については Keycloak 日本語ドキュメント - Authorization Services Guide を参照してください。

ClientAuthenticationTask.java
@Component
public class ClientAuthenticationTask {

    private KeycloakDeployment deployment;

    private AuthzClient authzClient;

    private AccessToken token;

    private String tokenString;

    public ClientAuthenticationTask(KeycloakSpringBootProperties properties) {
        this.deployment = KeycloakDeploymentBuilder.build(properties);
        Configuration configuration = new Configuration(
            properties.getAuthServerUrl(), properties.getRealm(), properties.getResource(), properties.getCredentials(),
            this.deployment.getClient());
        ClientAuthenticator authenticator = new ClientAuthenticator() {
            @Override
            public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
                Map<String, String> formparams = new HashMap<>();
                ClientCredentialsProviderUtils.setClientCredentials(ClientAuthenticationTask.this.deployment, requestHeaders, formparams);
                for (Entry<String, String> param : formparams.entrySet()) {
                    requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
                }
            }
        };
        this.authzClient = AuthzClient.create(configuration, authenticator);
    }
    ...
}

(2) 認可サービスクライアント機能を使ってアクセストークンを取得する

ClientAuthenticationTaskに、クライアント認証のリクエストを定期的に行うスケジュールタスクを@Scheduledで指定し、fixedDelayStringでタスクの間隔をプロパティで指定するようにします。

上記の(1)でセットアップしたクライアント機能の呼び出しは、KeycloakアダプターのThrowables#retryAndWrapExceptionIfNecessary とCallableを使って行います。
このあたりの実装も、Keycloakアダプターに含まれているAuthorizationResourceという認可サービス利用時のクラスのコードを参考に実装します。
AuthzClient#obtainAccessToken が実際にクライアント認証のリクエストを発行しているメソッドで、これを使うことが今回の実装の主眼となっています。

クライアント機能のレスポンスからアクセストークンを取得し、AdapterTokenVerifier#verifyToken を使って検証を行います。

ClientAuthenticationTask.java
    @Scheduled(initialDelay = 0L, fixedDelayString = "${com.example.authentication.task.interval}")
    public void authenticate() {

        AuthorizationResponse response = null;
        Callable<AuthorizationResponse> callable = new Callable<AuthorizationResponse>() {
            @Override
            public AuthorizationResponse call() throws Exception {
                return new AuthorizationResponse(ClientAuthenticationTask.this.authzClient.obtainAccessToken(), false);
            }
        };
        try {
            response = callable.call();
        } catch (Exception cause) {
            response = Throwables.retryAndWrapExceptionIfNecessary(callable, null, "Failed to obtain access token", cause);
        }
        if (response == null) {
            return;
        }

        try {
            this.token = AdapterTokenVerifier.verifyToken(response.getToken(), this.deployment);
        } catch (VerificationException e) {
            return;
        }
    	this.tokenString = response.getToken();
    }

    public AccessToken getToken() {
		return this.token;
	}

    public String getTokenString() {
		return this.tokenString;
	}

クライアントのRest APIを実装する

上記のクライアント機能で取得したアクセストークンをHTTPヘッダに付加して、認可リバースプロキシのURLにリクエストします。

ここでは実装を示しませんが、クライアント機能が取得したAccessTokenからスコープやロール等の様々な情報を取得することもできます。

ClientController.java
@RestController
public class ClientController {

	@Autowired
	private ClientAuthenticationTask task;

	@GetMapping("/client")
	public String client() {
		HttpHeaders headers = new HttpHeaders();
		headers.setBearerAuth(task.getTokenString());

		ResponseEntity<String> response = new RestTemplate().exchange(
				"http://localhost:3000/secured",
				HttpMethod.GET,
				new HttpEntity<String>(headers),
				String.class);
		return response.getBody();
	}
}

Webセキュリティを設定する

Spring SecurityのWebSecurityConfigurerAdapterを継承して、全てのアクセスを許可するように設定します。

SecurityConfiguration.java
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests().anyRequest().permitAll();
    }
}

Keycloakの設定方法を指定する

KeycloakSpringBootConfigResolverのBean定義を行うことによって、KeycloakをSpring Bootプロパティで設定することとします。

KeycloakConfigResolverConfiguration.java
@Configuration
public class KeycloakConfigResolverConfiguration  {
    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
}

Spring Bootプロパティの設定を行う

KeycloakのレルムとサーバーURL、クライアントID、下記のKeycloak Server管理画面に表示されるクライアントシークレットを設定します。
また、上記のクライアント認証のスケジュールタスクの間隔に、アクセストークンの有効期間である5分よりも少し短い値として4分(PT4M)を指定します。

application.properties
keycloak.realm = example
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.resource = client
keycloak.credentials.secret = 50c39441-e45d-46da-9069-2f04c3c0d3fa

com.example.authentication.task.interval = PT4M
server.port = 8000

これで認可クライアントの実装は完了です。

プロジェクトを起動する

順番が前後しますが、下記の認可サーバーの導入と設定が終わってから、認可クライアントのGradleプロジェクトを起動します。

C:\example\keycloakadapter>gradle run

認可サーバーの導入と設定

Keycloak Serverのダウンロードと起動

Keycloakのダウンロードページで、ServerのZIPをダウンロード、解凍します。
解凍したフォルダ直下のbin/standalone.bat を起動します。

C:\example\keycloak-8.0.1\bin>standalone.bat

起動後に http://localhost:8080/auth にアクセスし、『Administration Console』(以下、管理画面)から管理ユーザを作成してログイン後、以下の手順で日本語化しておきます。

  1. Realm Settings で masterレルムの『Themes』タブを開く
  2. 『Internationalization Enabled』をONにする。
  3. 『Default Locale』で『ja』を選択し、saveする。
  4. 右上の管理ユーザを選択し、『Sign out』で一度サインアウトする。

Keycloak Serverの設定

今回の構成のために、Keycloak Serverの管理画面で以下の設定を行います。

対象 項目 設定内容
レルム レルム名 example
ロール ロール名 CLIENT
クライアント(1) クライアントID gatekeeper
アクセスタイプ confidential
有効なリダイレクト URI http://localhost:3000/*
クライアント(2) クライアントID client
アクセスタイプ confidential
有効なリダイレクト URI http://localhost:8000/*
サービスアカウントの有効 オン
サービスアカウントロール レルムロール=CLIENT

Keycloak Serverにaudクレームの設定を追加する

昨年の Keycloak GateKeeperの記事 で取り上げた、アクセストークン内の"aud"クレームを自身のクライアントIDと一致していることを確認する問題について、執筆時点では未だに issueが解決していない ため、同様の回避設定を行います。

  1. Keycloakの管理画面で、clientの『マッパー』タブで、『作成』する。
  2. 名前を付けて、マッパータイプで『Audience』を選択する。
  3. 『Included Client Audience』で『gatekeeper』を選択する。
  4. 『アクセストークンに追加』のみONにして、『保存』する。

設定後はこのようになります。

kcadapter_aud_mapper.png

認可リバースプロキシの導入と設定

これも昨年の Keycloak GateKeeperの記事 と同様に設定していきます。

Keycloak Gatekeeperのダウンロード

上記のKeycloakのダウンロードページから GatekeeperのWindows版をダウンロードします。
ダウンロードしたファイル keycloak-gatekeeper-windows-amd64.tar.gz を解凍して、実行ファイルである keycloak-gatekeeper.exe を配置します。
この配置したフォルダを、Keycloak Gatekeeperの実行フォルダとします。

Keycloak Gatekeeperの設定

Keycloak Gatekeeperの実行フォルダに、次の内容の設定ファイルconfig.yamlを新規作成します。
/securedのURLに対してはCLIENTロールが必要としています。

config.yaml
listen: localhost:3000 #自身のポートを指定する
discovery-url: http://localhost:8080/auth/realms/example #Keycloak ServerのレルムのURL
client-id: gatekeeper #KeycloakのgatekeeperクライアントID
client-secret: 76238c91-0615-4825-8ec1-64c1439feda1 #Keycloak Serverのgatekeeperクライアントのシークレットを設定する
redirection-url: http://localhost:3000 #Keycloak Serverからリダイレクトで戻るエンドポイントURL
upstream-url: http://localhost:8888 #バックエンドAPIのURL
resources:
- uri: /secured
  roles:
  - CLIENT #/securedはCLIENTロールが必要
no-redirects: true # 認証が必要な場合にKeycloak Serverにリダイレクトしないようにする
enable-logging: true # ログ出力設定
verbose: true # デバッグログ出力設定

Keycloak Gatekeeperの起動

設定ができたら、設定ファイルを指定して起動します。

C:\example\keycloak-gatekeeper>keycloak-gatekeeper.exe --config config.yaml

バックエンドAPI/ネットワークキャプチャの導入と設定

Fiddlerのダウンロードと起動

Fiddlerのダウンロードページで、求められる項目を入力してWindows版のインストーラー FiddlerSetup.exe をダウンロードし、インストールを行います。
(参考:『HTTP通信のキャプチャをとるツールFiddlerをWindowsにインストールする』)

インストール後、Fiddlerを起動します。

Fiddlerは初期状態では、起動直後からローカルPC上のネットワークキャプチャを自動的に開始しますが、今回の構成で行うリクエスト以外は不要なので、以下の手順でlocalhostをホストとするリクエストのみを表示する設定を行います。

  1. Filtersタブを表示します。
  2. 『Use Filters』をONにします。
  3. 『Hosts』の2つ目のプルダウンから『Show only the following Hosts』を選択します。
  4. 上記プルダウンの直後の入力項目に『localhost』を入力します。
  5. 最後に『Actions』→『Run Filterset now』を押して設定を有効化します。

kcgk_fiddler_filter.png

Fiddlerの設定

Fiddlerの 『AutoResponder』タブで『Add Rule』として、以下の内容のようにsecuredの疑似API設定を行います。

  • リクエスト(下段の『Request URL Pattern』に入力する)

    EXACT:http://localhost:8888/secured
    
  • レスポンス(『Edit Response...』で『Raw』タブに入力する)

    HTTP/1.1 200 OK
    Content-Type: application/json
    Content-Length: 21
    
    {"message":"secured"}
    

最後に『Enable rules』をONにすると、AutoResponderが有効になります。

動作確認

Chromeで認可クライアントのURL http://localhost:8000/client にアクセスすると、バックエンドAPIのレスポンスが表示されます。
これによって、クライアント認証によるアクセストークンを使ったリクエストが成功していることが分かります。

kcadapter_client2.png

ここでFiddlerでバックエンドAPIへのリクエストを見ると、ロールをはじめとしてアクセストークンが表していた情報がバックエンドAPIにHTTPヘッダで渡されていることが確認できます。

X-Auth-Audience: gatekeeper
X-Auth-Email: 
X-Auth-Expiresin: 2019-12-16 16:03:58 +0000 UTC
X-Auth-Groups: 
X-Auth-Roles: CLIENT
X-Auth-Subject: ae609fe7-80bd-461c-a5c3-3ce95e7321cc
X-Auth-Token: eyJ...
X-Auth-Userid: service-account-client
X-Auth-Username: service-account-client

おわりに

今回はクライアント認証のみの実装例を示しましたが、Webアプリではユーザーがログインして得られるアクセストークンと、クライアント認証によるアクセストークンを使い分けるといったユースケースが考えられます。
KeycloakのSpring Boot/Securityアダプターはまだまだ使いこなすと面白そうなので、何かあれば情報共有をお待ちしています!

6
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?