CLOVER🍀

That was when it all began.

Terraform Keycloak ProviderでKeycloakのリソース定義を試す

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

KeycloakでRealmやユーザーを定義するのにWeb UIを使えますが、何度も同じことを操作するのはやや面倒ですし、やり方を忘れます。

管理CLIでも操作できますが、できればリソース定義のような形でやれたらと思うものです。

Keycloak 19.0の管理CLIを使ってみる - CLOVER🍀

Terraformで調べてみると、Keycloak Providerがあるようなので試してみることにしました。

Keycloak Provider

Terraform Keycloak Provider

TerraformのKeycloak Providerのドキュメントはこちら。

Keycloak Provider

GitHubリポジトリーはこちらです。

GitHub - mrparkers/terraform-provider-keycloak: Terraform provider for Keycloak

ただ、メンテナンスの状態はちょっと微妙なところがあったりします…。

The health of this repo, an open discussion. · Issue #964 · mrparkers/terraform-provider-keycloak · GitHub

基本的な機能を使う分には問題なさそうなので、そのまま進めることにします。

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のリソースを作成します。

Keycloak Provider

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_idadmin-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
  }
}

keycloak_user Resource

ロール定義。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"
}

keycloak_role Resource

ユーザーとロールの紐付け。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
  ]
}

keycloak_user_roles Resource

これでリソースを作成します。

$ 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-methodOIDCになっているところがポイントですね。

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"
}

WildFly Admin Guide / Subsystem configuration / Elytron OpenID Connect Client Subsystem Configuration / Configuration / Deployment Configuration

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のデフォルトの設定が
理由です。

ここでemailfirstNamelastNameの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はそもそも推奨されていないので、動作確認の用途などで使うようにしましょう。

アクセス方法はこちらに書かれています。

Securing Applications and Services Guide / Supported Grant Types / 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でリソース作成をしていってもいいのですが、コードで定義できるとやっぱり便利です。

開発が停滞しているところは気になりますが、基本的なリソース作成はできるのでふだんはこちらを使おうかなと思います。