これは、なにをしたくて書いたもの?
KeycloakでRealmやユーザーを定義するのにWeb UIを使えますが、何度も同じことを操作するのはやや面倒ですし、やり方を忘れます。
管理CLIでも操作できますが、できればリソース定義のような形でやれたらと思うものです。
Keycloak 19.0の管理CLIを使ってみる - CLOVER🍀
Terraformで調べてみると、Keycloak Providerがあるようなので試してみることにしました。
Terraform Keycloak Provider
TerraformのKeycloak Providerのドキュメントはこちら。
GitHub - mrparkers/terraform-provider-keycloak: Terraform provider for Keycloak
ただ、メンテナンスの状態はちょっと微妙なところがあったりします…。
基本的な機能を使う分には問題なさそうなので、そのまま進めることにします。
Keycloak ProviderからKeycloakを操作する方法としては、Client CredentialsとPassword Grantの2種類があるようです。
推奨はClient Credentialsのようなのですが、今回はまずは簡単に使えそうなPassword Grantの方で扱ってみたいと思います。
Keycloak Provider / Keycloak Setup
リソース定義するお題は、こちらを焼き直したいと思います。
Keycloak+WildFlyのElytron OpenID Connect ClientサブシステムでOpenID Connect - CLOVER🍀
Reamをひとつ作成し、その中にユーザーを2人作成して、それぞれ専用のロールを割り当てます。
環境
今回の環境はこちら。
Terraform。
$ terraform version Terraform v1.9.5 on linux_amd64
Keycloak。
$ bin/kc.sh --version Keycloak 25.0.4 JVM: 21.0.4 (Eclipse Adoptium OpenJDK 64-Bit Server VM 21.0.4+7-LTS) OS: Linux 5.15.0-119-generic amd64
Keycloakは172.17.0.2で動作しているものとし、以下のコマンドで起動させておきます。
$ KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password bin/kc.sh start-dev
管理ユーザーはadmin / passwordですね。
動作確認に使うJavaアプリケーションを作成、実行する環境。
$ java --version openjdk 21.0.4 2024-07-16 OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04) OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-119-generic", arch: "amd64", family: "unix"
TerraformでKeycloakのリソースを作成する
まずはKeycloak Providerを使って、TerraformでKeycloakのリソースを作成します。
TerraformとKeycloak Providerのバージョンを固定。
versions.tf
terraform { required_version = "1.9.5" required_providers { keycloak = { source = "mrparkers/keycloak" version = "4.4.0" } } }
リソース定義。
main.tf
provider "keycloak" { client_id = "admin-cli" username = "admin" password = "password" url = "http://172.17.0.2:8080" } ## Realm resource "keycloak_realm" "sample_realm" { realm = "sample-realm" display_name = "Sample Realm" enabled = true } ## Client resource "keycloak_openid_client" "sample_client" { realm_id = keycloak_realm.sample_realm.id client_id = "sample-client" name = "Sample Client" enabled = true access_type = "CONFIDENTIAL" standard_flow_enabled = true direct_access_grants_enabled = true # テスト用 root_url = "http://localhost:8080" valid_redirect_uris = ["http://localhost:8080/*"] } ## User resource "keycloak_user" "my_user" { realm_id = keycloak_realm.sample_realm.id username = "my-user" enabled = true initial_password { value = "password" temporary = false } } resource "keycloak_user" "test_user" { realm_id = keycloak_realm.sample_realm.id username = "test-user" enabled = true initial_password { value = "password" temporary = false } } # Role resource "keycloak_role" "my_role" { realm_id = keycloak_realm.sample_realm.id name = "my-role" } resource "keycloak_role" "test_role" { realm_id = keycloak_realm.sample_realm.id name = "test-role" } # Assign Role resource "keycloak_user_roles" "my_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.my_user.id role_ids = [ keycloak_role.my_role.id ] } resource "keycloak_user_roles" "test_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.test_user.id role_ids = [ keycloak_role.test_role.id ] }
少し説明していきましょう。
Providerを使うための設定は、Password Grantを使っています。KeycloakのURLおよび、管理ユーザーのアカウントの情報を設定します。
provider "keycloak" { client_id = "admin-cli" username = "admin" password = "password" url = "http://172.17.0.2:8080" }
Keycloak Provider / Keycloak Setup
client_id
はadmin-cli
を使用します。こちらのことですね。
Realmの定義。
## Realm resource "keycloak_realm" "sample_realm" { realm = "sample-realm" display_name = "Sample Realm" enabled = true }
今回は変わったものはありませんが、他のリソースを作成する時にはこのRealmのidが必要になります。
クライアントの定義。access_type
やどのフローを有効にするかは明示的に指定する必要があります。
## Client resource "keycloak_openid_client" "sample_client" { realm_id = keycloak_realm.sample_realm.id client_id = "sample-client" name = "Sample Client" enabled = true access_type = "CONFIDENTIAL" standard_flow_enabled = true direct_access_grants_enabled = true # テスト用 root_url = "http://localhost:8080" valid_redirect_uris = ["http://localhost:8080/*"] }
keycloak_openid_client Resource
direct_access_grants_enabled
は今回は動作確認用にtrue
にしました。
ユーザー定義。my-userとtest-userの2人を作成しています。
## User resource "keycloak_user" "my_user" { realm_id = keycloak_realm.sample_realm.id username = "my-user" enabled = true initial_password { value = "password" temporary = false } } resource "keycloak_user" "test_user" { realm_id = keycloak_realm.sample_realm.id username = "test-user" enabled = true initial_password { value = "password" temporary = false } }
ロール定義。my-roleとtest-roleの2つを作成しています。
# Role resource "keycloak_role" "my_role" { realm_id = keycloak_realm.sample_realm.id name = "my-role" } resource "keycloak_role" "test_role" { realm_id = keycloak_realm.sample_realm.id name = "test-role" }
ユーザーとロールの紐付け。my-roleをmy-userに、test-roleをtest-userにそれぞれ割り当てています。
# Assign Role resource "keycloak_user_roles" "my_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.my_user.id role_ids = [ keycloak_role.my_role.id ] } resource "keycloak_user_roles" "test_user_roles" { realm_id = keycloak_realm.sample_realm.id user_id = keycloak_user.test_user.id role_ids = [ keycloak_role.test_role.id ] }
これでリソースを作成します。
$ terraform init $ terraform apply
確認すると、Realmおよび関連するリソースが作成されています。
OKですね。
動作確認用のアプリケーションを作成する
作成したリソースを使った動作確認を行います。確認用のアプリケーションを作成しましょう。
アプリケーションも、こちらの焼き直しです。
Keycloak+WildFlyのElytron OpenID Connect ClientサブシステムでOpenID Connect - CLOVER🍀
作った時はJakarta EE 8だったので、Jakarta EE 10向けに少し書き直します。アプリケーションはWildFlyを使いますが、WildFly Maven Pluginで
Bootable JARを作成することにします。
pom.xml
。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>wildfly33-elytron-oidc-example</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-maven-plugin</artifactId> <version>5.0.0.Final</version> <executions> <execution> <goals> <goal>package</goal> </goals> </execution> </executions> <configuration> <bootable-jar>true</bootable-jar> <bootable-jar-name>${project.artifactId}-${project.version}-server-bootable.jar</bootable-jar-name> <overwrite-provisioned-server>true</overwrite-provisioned-server> <discover-provisioning-info> <version>33.0.1.Final</version> </discover-provisioning-info> </configuration> </plugin> </plugins> </build> </project>
Jakarta RESTful Web Services(JAX-RS)の有効化。
src/main/java/org/littlewings/keycloak/wildfly/JaxrsActivator.java
package org.littlewings.keycloak.wildfly; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
リソースクラスの定義。
src/main/java/org/littlewings/keycloak/wildfly/SampleResource.java
package org.littlewings.keycloak.wildfly; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import java.security.Principal; @Path("/") @ApplicationScoped public class SampleResource { @Inject private SecurityContext securityContext; @GET @Path("hello") @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello Keycloak and WildFly!!"; } @GET @Path("my-role/principal-name") @Produces(MediaType.TEXT_PLAIN) public String principal() { Principal principal = securityContext.getUserPrincipal(); return principal.getName(); } @GET @Path("test-role/message") @Produces(MediaType.TEXT_PLAIN) public String message() { return "Authenticated!!"; } }
上から順に、以下の用途で使います。
- 認証なしでアクセスできるエンドポイント
- ユーザーがロールmy-roleを保持している場合にアクセスできるエンドポイント
- ユーザーがロールtest-roleを保持している場合にアクセスできるエンドポイント
その認可設定となるように、web.xml
を設定します。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <security-constraint> <web-resource-collection> <web-resource-name>my-role resource collection</web-resource-name> <url-pattern>/my-role/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>my-role</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>test-role resource collection</web-resource-name> <url-pattern>/test-role/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>test-role</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>OIDC</auth-method> </login-config> <security-role> <role-name>my-role</role-name> </security-role> <security-role> <role-name>test-role</role-name> </security-role> </web-app>
auth-method
がOIDC
になっているところがポイントですね。
OpenID Connectを使うにあたって今回使用するのは、WildFly Elytron OpenID Connect Clientサブシステムです。
WEB-INF
配下にoidc.json
というファイルを作成する必要があります。
src/main/webapp/WEB-INF/oidc.json
{ "client-id" : "sample-client", "credentials" : { "secret" : "VX5D3RmlA9ziwA5tbKqQi1w1S7K6g24m" }, "provider-url" : "http://172.17.0.2:8080/realms/sample-realm", "principal-attribute" : "preferred_username", "ssl-required" : "external" }
client-id
はTerraformで作成したクライアントのid、provider-url
は同じくTerraformで作成したKeycloakのRealmへのURLを指定します。
secret
(Client Secret)はKeycloakの画面で確認して設定しましょう。
もしくは管理CLIで確認してもよいと思います。
$ bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password password $ bin/kcadm.sh get clients -r sample-realm -q 'clientId=sample-client' -F secret
Terraformのoutput
に指定してもいいのですが、sensitive扱いになっているので値の確認には使わない方が良いでしょう。
パッケージング。
$ mvn package
WildFly Glowが選択したレイヤー。
[INFO] --- wildfly:5.0.0.Final:package (default) @ wildfly33-elytron-oidc-example --- [INFO] Glow is scanning... [INFO] Glow scanning DONE. [INFO] context: bare-metal [INFO] enabled profile: none [INFO] galleon discovery [INFO] - feature-packs org.wildfly:wildfly-galleon-pack:33.0.1.Final - layers ee-core-profile-server jaxrs elytron-oidc-client [INFO] Some suggestions have been found. You could enable suggestions with the <suggest>true</suggest> option.
ビルドできたので、起動します。
$ java -jar target/wildfly33-elytron-oidc-example-0.0.1-SNAPSHOT-server-bootable.jar
まずは認証不要なエンドポイントにアクセス。
$ curl localhost:8080/hello Hello Keycloak and WildFly!!
次に、ブラウザでhttp://localhost:8080/my-role/principal-name
にアクセスします。
すると、Keycloakのログイン画面にリダイレクトされるので、ユーザー名とパスワードを入力します。
すると、メールアドレスと名前を入力するように言われるので入力します…。
すると、my-roleを保持しているユーザー向けのエンドポイントにアクセスできました。
test-roleを必要とするhttp://localhost:8080/test-role/message
にはアクセスできません。
ところで、最初にログインしようとした時にメールアドレスなどを求められたことについてですが、これはUser Profileのデフォルトの設定が
理由です。
ここでemail
、firstName
、lastName
の3つが必須のフィールドとして定義されているからです。
以前はなかったようですがKeycloak 23でプレビューに、Keycloak 24で機能として正式に含まれるようになったみたいです。
Keycloak 23.0.0 released - Keycloak
Keycloak 24.0.0 released - Keycloak
ドキュメントはこちら。
Server Administration Guide / Managing users / Managing user attributes
Keycloak Providerで必須ではないようにしたかったのですが、今は難しそうです(後述します)。
初回ログイン時に値を設定する、Terraformでリソースを定義する時に値を一緒に入れてしまう、Web UIで各フィールドを必須ではないようにする
といった対応がありますが、今回は各フィールドの必須定義を外すことにしました。
話を戻して。次は現在ログインしているmy-userをKeycloakからログアウトさせてブラウザ上のCookieを削除したうえで、アクセスするのに
test-roleが必要なhttp://localhost:8080/test-role/message
にアクセスしてみます。
ログアウトする機能は作っていないので、Keycloakからログアウトさせるにはこちらから。
test-userでログイン。
対象のエンドポイントにアクセスできました。
アクセスするのにmy-roleが必要なhttp://localhost:8080/my-role/principal-name
にはアクセスできません。
OKですね。
アクセストークンをResource Owner Password Credentialsで取得する
Keycloak ProviderでDirect Access Grantsを有効にしているので、こちらの動作確認もしておきましょう。
Resource Owner Password Credentialsはそもそも推奨されていないので、動作確認の用途などで使うようにしましょう。
アクセス方法はこちらに書かれています。
こんな感じでしょうか。
$ CLIENT_SECRET=..... $ curl 172.17.0.2:8080/realms/sample-realm/protocol/openid-connect/token \ -d client_id=sample-client \ -d client_secret=${CLIENT_SECRET} \ -d username=my-user \ -d password=password \ -d grant_type=password | jq
結果の例。
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDYm9aNFJNNXRtcHlFbnFFMjBXR2diRVh1czBIbDdfZV9EanFHODV0bUd3In0.eyJleHAiOjE3MjUxNjEyMDYsImlhdCI6MTcyNTE2MDkwNiwianRpIjoiMDBjNGM1YTctNDJmNi00M2Q3LWE2MWItNzM2Njg1Y2Q3ZDA4IiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiYTcwN2M3YzQtMjhmNi00MDA2LWEyMjAtYzI5YzYxMmFkYjE4IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic2FtcGxlLWNsaWVudCIsInNpZCI6ImVhZWU2NTA5LWY3NWItNGY0Zi04ZDZkLTcxMjI5NjRiY2U0NSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsibXktcm9sZSJdfSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoibXkgbXkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJteS11c2VyIiwiZ2l2ZW5fbmFtZSI6Im15IiwiZmFtaWx5X25hbWUiOiJteSIsImVtYWlsIjoibXktdXNlckBleGFtcGxlLmNvbSJ9.lV1d4-uV7FVFKSE4TXWt3OnnSssFc38yOJV5rJ7orAM0-DNJQ614X0vvq_Zr76i-jz7PZ0PJM9J4b0xZ_24WvWWuaG8-f_yoZAu9k73ApRQTvq6Di8OT30I_MM1oyrOfICYV3asaMHKCZWiryRyp58P8rL9g0nJudGno-LtVS6ziouKte2FIWRzHeV941P_gucxHL5x0INXZ_HHT3veIdGCRCctG0A0kc4TlWbZHzEuiKdvAet1-prPUvYGftqEZNCLa2ajxjoTGGqEVDyvPPPeSY56GIa62iH6ZEKtWK2q_QuPbYNt7KJ8fHkxvUkLVakk-H8zfND51pRN2rpuodw", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyODNkYWM1OS1mOWUwLTQ1YmUtYTNjYi1mMWNlNTJmZDgxM2EifQ.eyJleHAiOjE3MjUxNjI3MDYsImlhdCI6MTcyNTE2MDkwNiwianRpIjoiNTQ1ZGMwMmQtZWY2ZC00ODhmLTlhYzItYTlkN2QyMDUzZjRhIiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwiYXVkIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiYTcwN2M3YzQtMjhmNi00MDA2LWEyMjAtYzI5YzYxMmFkYjE4IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6InNhbXBsZS1jbGllbnQiLCJzaWQiOiJlYWVlNjUwOS1mNzViLTRmNGYtOGQ2ZC03MTIyOTY0YmNlNDUiLCJzY29wZSI6InByb2ZpbGUgYmFzaWMgZW1haWwgcm9sZXMgYWNyIHdlYi1vcmlnaW5zIn0.-1HIIHoqS2h82ANB9V3Jz3Fn1gimZeEUt8sU5MdyZj9mnI8CQjrJflhHXuKT7fhdoQN2Yn6G-X4QvYZiA1PK4A", "token_type": "Bearer", "not-before-policy": 0, "session_state": "eaee6509-f75b-4f4f-8d6d-7122964bce45", "scope": "profile email" }
OKですね。
Keycloak ProviderがUser Profileをうまく扱えないという話
Keycloak Providerは、Keycloak 23から追加されたUser Profileをうまく扱えません。
Keycloak version >= 24 support? · Issue #944 · mrparkers/terraform-provider-keycloak · GitHub
User Profileに対応するkeycloak_realm_user_profile
というリソースはあるのですが、実際にこれを使おうとするとusername
またはemail
は
削除できないと言われます。
こんな感じですね。
│ Error: error sending PUT request to /admin/realms/sample-realm/users/profile: 400 Bad Request. Response body: {"errorMessage":"[The attribute 'username' can not be removed, The attribute 'email' can not be removed]"}
厄介なことに1度これを起こしてしまうと抜けられなくなります…。
新規リソースの追加ではなく既存のUser Profileを更新できないといけないようなのですが、それが現状ではできません。
おわりに
TerraformでKeycloakのリソース定義を行える、Keycloak Providerを試してみました。
管理CLIでリソース作成をしていってもいいのですが、コードで定義できるとやっぱり便利です。
開発が停滞しているところは気になりますが、基本的なリソース作成はできるのでふだんはこちらを使おうかなと思います。