CLOVER🍀

That was when it all began.

Infinispan Serverの認可設定をKeycloak(OAuth 2.0)で行う

これは、なにをしたくて書いたもの?

Infinispan Serverの認証・認可設定を、Keycloak(というか、OAuth 2.0)で行えるみたいなので、こちらを試してみようかなと。

Defining Infinispan Server Security Realms / Token Realms

Token Realm

Token Realmは、OAuth 2.0のToken Introspection Endpointと連携するRealmです。

Defining Infinispan Server Security Realms / Token Realms

このエンドポイントを使用すると、トークンがアクティブかどうか(利用可能かどうか)を検証することができます。

Keycloakだと、こちらですね。

Introspection Endpoint

Infinispan ServerのToken Realmでは、Infinispan Serverへのアクセス時にアクセストークンを渡すことで、Infinispan Serverが
OAuth 2.0サーバーのIntrospection Endpointを呼び出します。

この時、少なくともKeycloakの場合はロールの情報がレスポンスに含まれているので、ここで認可を行うことができます。

環境

今回の環境です。

Keycloak。IPアドレスは、172.17.0.2とします。

$ java -version
openjdk version "1.8.0_262"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_262-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.262-b10, mixed mode)


2020-08-02 10:43:24,814 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 11.0.0 (WildFly Core 12.0.3.Final) started in 3484ms - Started 588 of 886 services (601 services are lazy, passive or on-demand)

Infinispan Server。IPアドレスは、172.17.0.3とします。

$ java --version
openjdk 11.0.8 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.8+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.8+10, mixed mode)


08:14:04,509 INFO  (main) [org.infinispan.SERVER] ISPN080001: Infinispan Server 11.0.1.Final started in 13597ms

お題

Keycloak上にRealmを作成し、以下を作成します。

  • Infinispan Server用のクライアント
  • Read-Onlyなことを表すロールおよび、ロールに属するユーザー
  • Read-Writeが可能なことを表すロールおよび、ロールに属するユーザー

ここで、ロールはあくまで名前にしかすぎないので、実際の権限設定についてはInfinispan Server側で行います。

それでは、確認していきましょう。

Keycloak上でRealm、クライアント、ユーザーを作成する

まずは、Keycloakに対する作業を行います。

以下を作っていきます。

  • 管理ユーザー … keycloak-admin
  • Realm … ispn-realm
  • クライアント … infinispan-server
  • ロール、ユーザー
    • read-only、read-only-user001
    • read-write、read-write-user001

Keycloakの管理ユーザーを作成し、Keycloakを再起動。

$ bin/add-user-keycloak.sh -u keycloak-admin -p keycloak-admin-password
$ bin/jboss-cli.sh -c --command=reload

Keycloakにログイン。

$ bin/kcadm.sh config credentials --server http://localhost:8080/auth --realm master --client admin-cli --user keycloak-admin --password keycloak-admin-password

Realmの作成。

$ bin/kcadm.sh create realms -s realm=ispn-realm -s enabled=true

クライアントを作成し、クライアントシークレットを取得しておきます。

$ bin/kcadm.sh create clients -r ispn-realm -s clientId=infinispan-server -s enabled=true -s directAccessGrantsEnabled=true
$ CID=`bin/kcadm.sh get clients -r ispn-realm -F id -q clientId=infinispan-server | jq '.[].id' -r`
$ bin/kcadm.sh get clients/$CID/client-secret -r ispn-realm
{
  "type" : "secret",
  "value" : "[Client Secret]"
}

directAccessGrantsEnabledをtrueにしているのは、後でcurlで簡単にアクセストークンを取得するためです。

ロールの作成。

$ bin/kcadm.sh create roles -r ispn-realm -s name=read-only
$ bin/kcadm.sh create roles -r ispn-realm -s name=read-write

Read-Onlyなユーザーを作成。

$ bin/kcadm.sh create users -r ispn-realm -s username=read-only-user001 -s enabled=true
$ bin/kcadm.sh add-roles -r ispn-realm --uusername read-only-user001 --rolename read-only
$ bin/kcadm.sh set-password -r ispn-realm --username read-only-user001 --new-password read-only-user001-password

Read-Writeなユーザーを作成。

$ bin/kcadm.sh create users -r ispn-realm -s username=read-write-user001 -s enabled=true
$ bin/kcadm.sh add-roles -r ispn-realm --uusername read-write-user001 --rolename read-write
$ bin/kcadm.sh set-password -r ispn-realm --username read-write-user001 --new-password read-write-user001-password

以上で、Keycloak側の準備は完了です。

Infinispan Serverの設定を行う

次に、Infinispan Serverを設定していきます。

設定するのは、以下の3つです。

  • Security Realm
  • Endpoint
  • Cache

Security Realmに、Token Reamをまずは加えます。

Defining Infinispan Server Security Realms / Token Realms

今回は、デフォルトのSecurity Realm(default)はそのまま残し、新しくtoken-realmという名前のSecurity Realmを追加しました。

      <security>
         <security-realms>
            <security-realm name="default">
               <!-- Uncomment to enable TLS on the realm -->
               <!-- server-identities>
                  <ssl>
                     <keystore path="application.keystore" relative-to="infinispan.server.config.path"
                               keystore-password="password" alias="server" key-password="password"
                               generate-self-signed-certificate-host="localhost"/>
                  </ssl>
               </server-identities-->
               <properties-realm groups-attribute="Roles">
                  <user-properties path="users.properties" relative-to="infinispan.server.config.path" plain-text="true"/>
                  <group-properties path="groups.properties" relative-to="infinispan.server.config.path" />
               </properties-realm>
            </security-realm>

        <security-realm name="token-realm">
           <token-realm name="token"
                            client-id="dummy" 
                            auth-server-url="http://172.17.0.2:8080/auth/"> 
                   <oauth2-introspection
               introspection-url="http://172.17.0.2:8080/auth/realms/ispn-realm/protocol/openid-connect/token/introspect" 
                           client-id="infinispan-server"
                           client-secret="[クライアントシークレット]"/> 
               </token-realm>
        </security-realm>
         </security-realms>
      </security>

これですね。

     <security-realm name="token-realm">
           <token-realm name="token"
                            client-id="dummy" 
                            auth-server-url="http://172.17.0.2:8080/auth/"> 
                   <oauth2-introspection
               introspection-url="http://172.17.0.2:8080/auth/realms/ispn-realm/protocol/openid-connect/token/introspect" 
                           client-id="infinispan-server"
                           client-secret="[クライアントシークレット]"/> 
               </token-realm>
        </security-realm>

oauth2-introspectionが、主に設定が必要な箇所です。

token-realmにもclient-idとauth-server-urlを指定する必要があるのですが、これがどう使われているかまでは確認していません…。

テストコードおよび設定を見ていると、Web UIの認証に使いそうな感じがありますね。

https://github.com/infinispan/infinispan/blob/11.0.1.Final/server/tests/src/test/resources/configuration/security/keycloak-oauth2.xml

https://github.com/infinispan/infinispan/blob/11.0.1.Final/server/tests/src/test/resources/keycloak/infinispan-keycloak-realm.json

続いて、Endpointの設定を行います。

Configuring Endpoint Authentication Mechanisms

Token Realmを使う場合は、Hot RodとRESTで使うセキュリティメカニズムが異なります。

Hot Rodの場合は、OAUTHBEARERを使用します。

Hot Rod Authentication Configuration

      <endpoints socket-binding="default" security-realm="token-realm">
         <hotrod-connector name="hotrod">
             <authentication>
                <sasl mechanisms="OAUTHBEARER"
                      server-name="infinispan-server-001"
                      qop="auth"/>
             </authentication>
         </hotrod-connector>
         <rest-connector name="rest"/>
      </endpoints>

最後に、Cacheの設定を行います。

Declaratively Configuring Authorization

   <cache-container name="default" statistics="true">
      <transport cluster="${infinispan.cluster.name}" stack="${infinispan.cluster.stack:tcp}" node-name="${infinispan.node.name:}"/>

      <security>
         <authorization> 
            <identity-role-mapper /> 
            <role name="read-only" permissions="READ" />
            <role name="read-write" permissions="READ WRITE" />
         </authorization>
      </security>

      <distributed-cache mode="SYNC" name="secureCache" statistics="true">
          <security>
              <authorization enabled="true" roles="read-write read-only"/>
          </security>
      </distributed-cache>
   </cache-container>

Cache内にロールの設定を、

          <security>
              <authorization enabled="true" roles="read-write read-only"/>
          </security>

Cache Container内に、各ロールで使える権限を定義します。

      <security>
         <authorization> 
            <identity-role-mapper /> 
            <role name="read-only" permissions="READ" />
            <role name="read-write" permissions="READ WRITE" />
         </authorization>
      </security>

この時のロール名は、Keycloakで定義したロールと同じ名前です。

指定できる権限については、こちらで確認することができます。

Permissions

これで、Infinispan Serverの準備は完了です。ここまでできたら、Infinispan Serverを再起動します。

Infinispan Serverの設定は、こちらも参考になるでしょう。

urn:infinispan:server:11.0

確認する

それでは、確認していきましょう。確認は、Hot Rod Clientを使ったテストコードで行うことにします。

環境は、こちら。

$ java --version
openjdk 11.0.8 2020-07-14
OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.8, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-112-generic", arch: "amd64", family: "unix"

Maven依存関係や、プラグインなど。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>11.0.1.Final</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.16.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

テストコード。
src/test/java/org/littlewings/infinispan/authentication/token/TokenAuthnAuthzTest.java

package org.littlewings.infinispan.authentication.token;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class TokenAuthnAuthzTest {
    @Test
    public void authnAndAuthz() {
        String readWriteUserToken = "[read-writeロールを持ったユーザーのアクセストークン]";

        try (RemoteCacheManager cacheManager = new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("172.17.0.3:11222")
                        .security()
                        .authentication()
                        .saslMechanism("OAUTHBEARER")
                        .token(readWriteUserToken)
                        .build())) {
            RemoteCache<String, String> cache = cacheManager.getCache("secureCache");

            cache.put("key1", "value1");
            assertThat(cache.get("key1")).isEqualTo("value1");
        }

        String readOnlyUserToken = "[read-onlyロールを持ったユーザーのアクセストークン]";

        try (RemoteCacheManager cacheManager = new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("172.17.0.3:11222")
                        .security()
                        .authentication()
                        .saslMechanism("OAUTHBEARER")
                        .token(readOnlyUserToken)
                        .build())) {
            RemoteCache<String, String> cache = cacheManager.getCache("secureCache");

            assertThat(cache.get("key1")).isEqualTo("value1");

            assertThatThrownBy(() -> cache.put("key2", "value2"))
                    .isInstanceOf(HotRodClientException.class)
                    .hasMessageContainingAll("Unauthorized access", "lacks 'WRITE' permission");
        }
    }

    @Test
    public void noAuth() {
        try (RemoteCacheManager cacheManager = new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("172.17.0.3:11222")
                        .build())) {
            RemoteCache<String, String> cache = cacheManager.getCache("secureCache");

            assertThatThrownBy(() -> cache.put("key1", "value1"))
                    .isInstanceOf(HotRodClientException.class)
                    .hasMessageContaining("Unauthorized 'PUT' operation");
        }
    }
}

該当のCacheには、認証・認可を有効にしたので、アクセス時に認証情報を渡す必要があります。

Token Realmの場合は、アクセストークンです。

設定は、こちらを参考に行います。

Configuring Authentication Mechanisms for Hot Rod Clients

ケースとしては、read-writeロールに属するユーザーのアクセストークンを設定することで、読み書きができること、

        try (RemoteCacheManager cacheManager = new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("172.17.0.3:11222")
                        .security()
                        .authentication()
                        .saslMechanism("OAUTHBEARER")
                        .token(readWriteUserToken)
                        .build())) {
            RemoteCache<String, String> cache = cacheManager.getCache("secureCache");

            cache.put("key1", "value1");
            assertThat(cache.get("key1")).isEqualTo("value1");
        }

read-onlyロールに属するユーザーのアクセストークンを設定することで、読み込みはできるものの、書き込みは失敗すること、

        try (RemoteCacheManager cacheManager = new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("172.17.0.3:11222")
                        .security()
                        .authentication()
                        .saslMechanism("OAUTHBEARER")
                        .token(readOnlyUserToken)
                        .build())) {
            RemoteCache<String, String> cache = cacheManager.getCache("secureCache");

            assertThat(cache.get("key1")).isEqualTo("value1");

            assertThatThrownBy(() -> cache.put("key2", "value2"))
                    .isInstanceOf(HotRodClientException.class)
                    .hasMessageContainingAll("Unauthorized access", "lacks 'WRITE' permission");
        }

また、認証設定なしだと操作ができないことを確認します。

    @Test
    public void noAuth() {
        try (RemoteCacheManager cacheManager = new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers("172.17.0.3:11222")
                        .build())) {
            RemoteCache<String, String> cache = cacheManager.getCache("secureCache");

            assertThatThrownBy(() -> cache.put("key1", "value1"))
                    .isInstanceOf(HotRodClientException.class)
                    .hasMessageContaining("Unauthorized 'PUT' operation");
        }
    }

確認にあたってはアクセストークンが必要になるわけですが、これはInfinispan Serverが稼働するホストからcurlで行いました。

## read-write
$ curl -s -d 'client_id=infinispan-server' -d 'client_secret=[クライアントシークレット]' -d 'username=read-write-user001' -d 'password=read-write-user001-password' -d 'grant_type=password' 'http://172.17.0.2:8080/auth/realms/ispn-realm/protocol/openid-connect/token' | jq .access_token -r

## read-only
$ curl -s -d 'client_id=infinispan-server' -d 'client_secret=[クライアントシークレット]' -d 'username=read-only-user001' -d 'password=read-only-user001-password' -d 'grant_type=password' 'http://172.17.0.2:8080/auth/realms/ispn-realm/protocol/openid-connect/token' | jq .access_token -r

最初、Keycloakが動作するホストでコマンドを実行して取得していたら、Token Introspectionの結果が

{"active":false}

となり、これに気づくまでにけっこう時間がかかりました…。

oauth 2.0 - keycloak token introspection always fails with {"active":false} - Stack Overflow

で、これで得られたアクセストークンを使ってテストを行えば、認可設定が行えていることが確認できます。

read-onlyロールで書き込みを行った時のメッセージ全文は、

            assertThatThrownBy(() -> cache.put("key2", "value2"))
                    .isInstanceOf(HotRodClientException.class)
                    .hasMessageContainingAll("Unauthorized access", "lacks 'WRITE' permission");

こんな感じですね。

org.infinispan.client.hotrod.exceptions.HotRodClientException:Request for messageId=14 returned server error (status=0x85): java.lang.SecurityException: ISPN000287: Unauthorized access: subject 'Subject with principal(s): [read-only-user001, RolePrincipal{name='manage-account'}, RolePrincipal{name='offline_access'}, RolePrincipal{name='manage-account-links'}, RolePrincipal{name='uma_authorization'}, RolePrincipal{name='read-only'}, RolePrincipal{name='view-profile'}, InetAddressPrincipal [address=172.17.0.1/172.17.0.1]]' lacks 'WRITE' permission

これで、確認OKです、と。

わからなかったこと

Token Realmに切り替えた時に、Infinispan ServerのCLIが使えなくなって困りました…。

Endpointにはsecurity-realmが設定できるので、こんな感じにSecurity Realmの指定を分ければいいのかな?と思ったのですが、
CLIの時もtoken-realmを使ってしまいます。

      <endpoints socket-binding="default" security-realm="token-realm">
         <hotrod-connector name="hotrod" security-realm="token-ream">
             <authentication>
                <sasl mechanisms="OAUTHBEARER"
                      server-name="infinispan-server-001"
                      qop="auth"/>
             </authentication>
         </hotrod-connector>
         <rest-connector name="rest" security-realm="default"/>
      </endpoints>

かといって、ここのsecurity-realmをdefaultにすると、どうもHot Rod Connectorの方もdefaultを向いてしまうようで、
実質Connectorへの指定は効いていないような…?

      <endpoints socket-binding="default" security-realm="token-realm">

この状態だと、CLIの方も「token-realm」を使おうとします。

      <endpoints socket-binding="default" security-realm="token-realm">

それに、アクセストークンが使えるとして、CLIでどう指定したら?

$ bin/cli.sh -c ???

「-c」を使わずに起動後に「connect」しても、ユーザー名とパスワードを求められますしね。

どうなんでしょう?

まとめ

Infinispan ServerのToken Realmを使って、認可に関するロールをKeycloakから取得するように設定してみました。

いろいろてこずりましたし、最初は認証の通し方もわからなくてだいぶ困っていましたが、なんとかなりました…。

なんか、endpoints要素のsecurity-realm属性とConnectorのsecurity-realm属性で苦労した感が…。とりあえず、通せたので
よしとしましょうか。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-authn-authz-token