これは、なにをしたくて書いたもの?
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だと、こちらですね。
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の認証に使いそうな感じがありますね。
続いて、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で定義したロールと同じ名前です。
指定できる権限については、こちらで確認することができます。
これで、Infinispan Serverの準備は完了です。ここまでできたら、Infinispan Serverを再起動します。
Infinispan Serverの設定は、こちらも参考になるでしょう。
確認する
それでは、確認していきましょう。確認は、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"
<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