CLOVER🍀

That was when it all began.

WildFly 41とKeycloak 26でJakarta SecurityのOpenID Connectを使った認証と認可設定を試す

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

以前、WildFly 34(Jakarta EE 10)とKeycloakでOpenID Connectを使った認証、認可を試しました。

WildFly 34とKeycloak 26でJakarta SecurityのOpenID Connectを使った認証と認可設定を試す - CLOVER🍀

今回は、こちらをWildFly 41(Jakarta EE 11)でやってみたいと思います。バージョンアップ後の確認ですね。

結果としてほぼ変更せずに動作したので、簡単に済ませていきたいと思います。

Jakarta Security 4.0

Jakarta EE 11におけるJakarta Securityのバージョンは4.0です。

Jakarta Security 4.0 | Jakarta EE | The Eclipse Foundation

仕様書を見てもJakarta Security 3.0(Jakarta EE 10)からの変更点が書かれていなかったのですが、仕様のページに
まとめられていました。

変更はこのあたりのようです。

  • Basic API (handler) for Multiple authentication mechanisms … 複数の認証メカニズムのための基本API(ハンドラー)
  • Qualifiers for build-in authentication mechanisms … 組み込み認証メカニズムのQualifier
  • In-memory identity store … インメモリーIDストアー

削除や互換性のない変更はこちら。

  • Remove references to the SecurityManager … SecurityManagerへの参照の削除
  • Build-in authentication mechanisms now have a qualifier by default, whereas before they were unqualified … 組み込み認証メカニズムにはQualifierがなかったので、デフォルトで付与するように変更

Jakarta Security 4.0 | Jakarta EE | The Eclipse Foundation

というわけで、そう大きくは変わっていなさそうですね。

Jakarta Securityの用語は、

  • Authentication Mechanism(認証メカニズム)
    • 認証を実行するメカニズムで、呼び出し元と対話して資格情報を取得し、アイデンティティーストアを呼び出して指定された資格情報をユーザーのアイデンティティーと照合する
    • 一致した場合、認証メカニズムは見つかったアイデンティティーを使用して属性(プリンシパル)を設定し、認証されたサブジェクトを構築する
    • 一致しなかった場合はログイン失敗となる
  • Caller(呼び出し元)、Caller Principal
    • アプリケーションにリクエストを送信したり、アプリケーションAPIを呼び出したりするユーザー
    • Caller Principal(呼び出し元プリンシパル)とは、そのユーザーを表すプリンシパルオブジェクト
  • HAM
    • Jakarta Securityで定義されているインターフェースである、HttpAuthenticationMechanismの略称
  • Identity Store(アイデンティティーストア)
    • ユーザー、グループ、ロール、権限などのアプリケーション固有のセキュリティデータにアクセスできるコンポーネント
    • 通常、RDBMS、LDAP、ファイルシステム、その他類似のリソースなどのデータソースと1対1で関連付けられている
    • 同義語: セキュリティプロバイダー、リポジトリー、ストア、ログインモジュール(JAAS)、アイデンティティーマネージャー、サービスプロバイダー、Relying Party、Authenticator、ユーザーサービス
  • SAM
    • Jakarta Authenticationで定義されているインターフェースであるServerAuthModuleの略称

Jakarta Securityによる認証メカニズムは、Jakarta Servlet上で動作することを想定しています。

このあたり、詳しくは以前のエントリーを参照してください。

WildFly 34とKeycloak 26でJakarta SecurityのOpenID Connectを使った認証と認可設定を試す - CLOVER🍀

Jakarta Securityがサポートしている認証方法は、以下の4つです。

  • Basic認証
  • Form認証
  • カスタムForm認証
  • OpenID Connect認証

WildFly 41が使用しているJakarta Securityの実装は、Soteriaです。バージョンは4.0.2ですね。

GitHub - eclipse-ee4j/soteria: Soteria, a Jakarta Security implementation · GitHub

では、簡単に試してみましょう。

お題

お題は、前回のエントリーと同じです。以下の構成で、Jakarta SecurityのOpenID Connectを使った認証を試してみたいと
思います。

flowchart LR
    A[Browser] --> |HTTP| B[WildFly]
    A --> |Redirect / HTTP| C[Keycloak]
    B --> |Get Token / Verify Token| C

OpenID Connectの用語の合わせると、こうなりますね。

flowchart LR
    A[Browser] --> |HTTP| B[Relying Party/WildFly]
    A --> |Redirect / HTTP| C[OpenID Provider/Keycloak]
    B --> |Get Token / Verify Token| C

アクセスするユーザーとしては、リーダーと一般ユーザーの2種類(のロール)を用意することにします。

Keycloadk上のリソースはTerraformで作成するものとします。

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.3 2026-04-21
OpenJDK Runtime Environment (build 25.0.3+9-2-24.04.2-Ubuntu)
OpenJDK 64-Bit Server VM (build 25.0.3+9-2-24.04.2-Ubuntu, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 25.0.3, vendor: Ubuntu, runtime: /usr/lib/jvm/java-25-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-137-generic", arch: "amd64", family: "unix"

Keycloak。

$ bin/kc.sh --version
Keycloak 26.7.1
JVM: 25.0.3 (Eclipse Adoptium OpenJDK 64-Bit Server VM 25.0.3+9-LTS)
OS: Linux 6.8.0-137-generic amd64

Keycloakは172.17.0.2で動作しているものとし、以下のコマンドで起動させておきます。

bin/kc.sh start-dev

Keycloakの設定はこちら。

grep -vE '^#|^$' conf/keycloak.conf
bootstrap-admin-username=keycloak-admin
bootstrap-admin-password=password
http-host=0.0.0.0
http-access-log-enabled=true

Terraform。

$ terraform version
Terraform v1.15.8
on linux_amd64

TerraformでKeycloakのリソースを作成する

まずはTerraformでKeycloak上のリソースを定義します。

versions.tf

terraform {
  required_version = "1.15.8"

  required_providers {
    keycloak = {
      source  = "keycloak/keycloak"
      version = "5.9.0"
    }
  }
}

あとはクライアント、クライアントプロトコルマッパー、ユーザー、ロール、ロールのユーザーへのアサインを定義。

main.tf

provider "keycloak" {
  client_id = "admin-cli"
  username  = "keycloak-admin"
  password  = "password"
  url       = "http://172.17.0.2:8080"
}

## Realm
resource "keycloak_realm" "sample_realm" {
  realm        = "sample-realm"
  display_name = " Realm"
  enabled      = true
}

## Client
resource "keycloak_openid_client" "ee_client" {
  realm_id  = keycloak_realm.sample_realm.id
  client_id = "ee-client"

  name    = "Jakarta Security Client"
  enabled = true

  access_type                  = "CONFIDENTIAL"
  standard_flow_enabled        = true
  direct_access_grants_enabled = true # テスト用

  # pkce_code_challenge_method = "S256" # PKCE

  root_url            = "http://localhost:8080"
  valid_redirect_uris = ["http://localhost:8080/*"]
}

## Client Protocol Mapper
data "keycloak_openid_client_scope" "roles" { # client scopeのmapperとして使う場合
  realm_id = keycloak_realm.sample_realm.id
  name     = "roles"
}

resource "keycloak_openid_user_realm_role_protocol_mapper" "role_mapper" {
  realm_id  = keycloak_realm.sample_realm.id
  name      = "roles as top"
  client_id = keycloak_openid_client.ee_client.id # dedicated mapperとして使う場合
  # client_scope_id = data.keycloak_openid_client_scope.roles.id # client scopeのmapperとして使う場合

  claim_name       = "roles"
  claim_value_type = "String"
  multivalued      = true

  add_to_access_token = true
  add_to_id_token     = false
}

## User
resource "keycloak_user" "leader_user" {
  realm_id   = keycloak_realm.sample_realm.id
  username   = "leader-user"
  email      = "leader-user@example.com"
  first_name = "Leader"
  last_name  = "User"

  enabled = true

  initial_password {
    value     = "password"
    temporary = false
  }
}

resource "keycloak_user" "general_user" {
  realm_id   = keycloak_realm.sample_realm.id
  username   = "general-user"
  email      = "general-user@example.com"
  first_name = "General"
  last_name  = "User"

  enabled = true

  initial_password {
    value     = "password"
    temporary = false
  }
}

## Role
resource "keycloak_role" "leader_role" {
  realm_id = keycloak_realm.sample_realm.id
  name     = "leader-role"
}

resource "keycloak_role" "general_role" {
  realm_id = keycloak_realm.sample_realm.id
  name     = "general-role"
}

## Assign Role
resource "keycloak_user_roles" "leader_user_assign_leader_role" {
  realm_id = keycloak_realm.sample_realm.id
  user_id  = keycloak_user.leader_user.id

  role_ids = [
    keycloak_role.leader_role.id
  ]
}

resource "keycloak_user_roles" "general_user_assign_general_role" {
  realm_id = keycloak_realm.sample_realm.id
  user_id  = keycloak_user.general_user.id

  role_ids = [
    keycloak_role.general_role.id
  ]
}

output "ee_client_client_secret" {
  value     = keycloak_openid_client.ee_client.client_secret
  sensitive = true
}

Jakarta Securityで認証した後にユーザーに割り当てたロールを認識させるために、クライアントプロトコルマッパーを
定義しています。

## Client Protocol Mapper
data "keycloak_openid_client_scope" "roles" { # client scopeのmapperとして使う場合
  realm_id = keycloak_realm.sample_realm.id
  name     = "roles"
}

resource "keycloak_openid_user_realm_role_protocol_mapper" "role_mapper" {
  realm_id  = keycloak_realm.sample_realm.id
  name      = "roles as top"
  client_id = keycloak_openid_client.ee_client.id # dedicated mapperとして使う場合
  # client_scope_id = data.keycloak_openid_client_scope.roles.id # client scopeのmapperとして使う場合

  claim_name       = "roles"
  claim_value_type = "String"
  multivalued      = true

  add_to_access_token = true
  add_to_id_token     = false
}

またPKCEも必須にしてみたのですが、Jakarta Securityが対応していなかったので使うのを諦めました。

## Client
resource "keycloak_openid_client" "ee_client" {
  realm_id  = keycloak_realm.sample_realm.id
  client_id = "ee-client"

  name    = "Jakarta Security Client"
  enabled = true

  access_type                  = "CONFIDENTIAL"
  standard_flow_enabled        = true
  direct_access_grants_enabled = true # テスト用

  # pkce_code_challenge_method = "S256" # PKCE

  root_url            = "http://localhost:8080"
  valid_redirect_uris = ["http://localhost:8080/*"]
}

リソースを作成。

$ terraform init
$ terraform apply

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

$ terraform output ee_client_client_secret
"g1brJ8rSsoHXb6V3oO0RCqstk7mgXv8uQqKf3hiSBHlzPNSQdBx8igOxY6QWOMcu7Ev5OBKojQZJtFfrdThzC3"

ソースコードを作成する

それでは、ソースコードを作成していきます。

Maven依存関係など。

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>wildfly-ee-with-tools</artifactId>
                <version>41.0.0.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>wildfly-expansion-with-tools</artifactId>
                <version>41.0.0.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.inject</groupId>
            <artifactId>jakarta.inject-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.security.enterprise</groupId>
            <artifactId>jakarta.security.enterprise-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
          <finalName>ROOT</finalName>
          <plugins>
              <plugin>
                  <groupId>org.wildfly.plugins</groupId>
                  <artifactId>wildfly-maven-plugin</artifactId>
                  <version>6.0.1.Final</version>
                  <executions>
                      <execution>
                          <id>package</id>
                          <goals>
                              <goal>package</goal>
                          </goals>
                      </execution>
                  </executions>
                  <configuration>
                      <overwrite-provisioned-server>true</overwrite-provisioned-server>
                      <discover-provisioning-info>
                          <version>41.0.0.Final</version>
                          <suggest>true</suggest>
                      </discover-provisioning-info>
                      <commands>
                          <command>/subsystem=undertow/application-security-domain=other:write-attribute(name=integrated-jaspi, value=false)</command>
                      </commands>
                  </configuration>
              </plugin>
          </plugins>
      </build>

Jakarta Securityを使った、OpenID Connectの設定。

src/main/java/org/littlewings/security/oidc/OidcConfig.java

package org.littlewings.security.oidc;

import jakarta.annotation.security.DeclareRoles;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.security.enterprise.authentication.mechanism.http.OpenIdAuthenticationMechanismDefinition;
import jakarta.security.enterprise.authentication.mechanism.http.openid.ClaimsDefinition;
import jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant;
import org.eclipse.microprofile.config.inject.ConfigProperty;

@OpenIdAuthenticationMechanismDefinition(
        providerURI = "${oidcConfig.providerUri}",
        clientId = "${oidcConfig.clientId}",
        clientSecret = "${oidcConfig.clientSecret}",
        scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, // default
        redirectURI = "${baseURL}/callback",
        // redirectToOriginalResource = true, // trueにすると、アクセスしたページに戻ろうとする
        tokenAutoRefresh = true,
        claimsDefinition = @ClaimsDefinition(callerGroupsClaim = "roles"))
@DeclareRoles({"leader-role", "general-role"})
@ApplicationScoped
@Named("oidcConfig") // ELで参照するのに必要
public class OidcConfig {
    @Inject
    @ConfigProperty(name = "security.oidc.provider-uri")
    private String providerUri;

    @Inject
    @ConfigProperty(name = "security.oidc.client-id")
    private String clientId;

    @Inject
    @ConfigProperty(name = "security.oidc.client-secret")
    private String clientSecret;

    // getter/setterは省略
}

@OpenIdAuthenticationMechanismDefinitionアノテーションの各プロパティーは、設定で変更できるようにEL式にしています。

値の取得元は、このBean自身ですが。

定義はMicroProfile Configで行っています。

src/main/resources/META-INF/microprofile-config.properties

security.oidc.provider-uri=http://172.17.0.2:8080/realms/sample-realm
security.oidc.client-id=ee-client
security.oidc.client-secret=g1brJ8rSsoHXb6V3oO0RCqstk7mgXv8uQqKf3hiSBHlzPNSQdBx8igOxY6QWOMcu7Ev5OBKojQZJtFfrdThzC3

ロールの宣言もここに入れています。

@DeclareRoles({"leader-role", "general-role"})

アクセス制御を行うServletを書いていきましょう。前回同様、今回も認証・認可における動作確認をServletで行って
いきます。

まずはベースから。

src/main/java/org/littlewings/security/oidc/OidcAccessTestServlet.java

package org.littlewings.security.oidc;

import jakarta.inject.Inject;
import jakarta.security.enterprise.SecurityContext;
import jakarta.security.enterprise.identitystore.openid.AccessToken;
import jakarta.security.enterprise.identitystore.openid.IdentityToken;
import jakarta.security.enterprise.identitystore.openid.OpenIdContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.Principal;
import java.util.Map;

public abstract class OidcAccessTestServlet extends HttpServlet {
    @Inject
    private SecurityContext securityContext;

    @Inject
    private OpenIdContext openIdContext;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        PrintWriter writer = response.getWriter();

        Principal principal = securityContext.getCallerPrincipal();

        writer.printf("=============== Principal ===============%n");
        writer.printf("name = %s%n", principal.getName());
        writer.printf("=============== Principal ===============%n");

        writer.println();

        if (!"anonymous".equals(principal.getName())) {
            writer.printf("=============== OpenIdContext ===============%n");

            writer.printf("subject = %s%n", openIdContext.getSubject());

            writer.println();

            writer.printf(
                    "claims preferred_username = %s%n",
                    openIdContext.getClaims().getPreferredUsername().get());
            writer.printf("claims roles = %s%n", openIdContext.getClaims().getArrayStringClaim("roles"));

            writer.println();

            AccessToken accessToken = openIdContext.getAccessToken();
            writer.printf("access token string = %s%n", accessToken.getToken());
            writer.println();
            writer.printf("access tokens claims%n");

            for (Map.Entry<String, Object> claim : accessToken.getClaims().entrySet()) {
                writer.printf("  %s = %s%n", claim.getKey(), claim.getValue());
            }

            writer.println();

            IdentityToken idToken = openIdContext.getIdentityToken();
            writer.printf("id token string = %s%n", idToken.getToken());
            writer.println();
            writer.printf("id tokens claims%n");

            for (Map.Entry<String, Object> claim : idToken.getClaims().entrySet()) {
                writer.printf("  %s = %s%n", claim.getKey(), claim.getValue());
            }

            writer.printf("=============== OpenIdContext ===============%n");
        }
    }
}

こちらを継承して、アクセス制御を行うServletを書いていきます。

リーダーと一般ユーザーの両方がアクセスできるServletです。

sc/main/java/org/littlewings/security/oidc/LeaderOrGeneralAccessAccessTestServlet.java

package org.littlewings.security.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.annotation.HttpConstraint;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebServlet;

@WebServlet("/leader-or-general")
@ServletSecurity(@HttpConstraint(rolesAllowed = {"leader-role", "general-role"}))
@ApplicationScoped
public class LeaderOrGeneralAccessAccessTestServlet extends OidcAccessTestServlet {}

リーダーだけがアクセスできるServlet。

src/main/java/org/littlewings/security/oidc/LeaderOnlyAccessAccessTestServlet.java

package org.littlewings.security.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.annotation.HttpConstraint;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebServlet;

@WebServlet("/leader-only")
@ServletSecurity(@HttpConstraint(rolesAllowed = "leader-role"))
@ApplicationScoped
public class LeaderOnlyAccessAccessTestServlet extends OidcAccessTestServlet {}

一般ユーザーだけがアクセスできるServlet。

src/main/java/org/littlewings/security/oidc/GeneralOnlyAccessAccessTestServlet.java

package org.littlewings.security.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.annotation.HttpConstraint;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebServlet;

@WebServlet("/general-only")
@ServletSecurity(@HttpConstraint(rolesAllowed = "general-role"))
@ApplicationScoped
public class GeneralOnlyAccessAccessTestServlet extends OidcAccessTestServlet {}

どのロールでもアクセスできないServlet。

src/main/java/org/littlewings/security/oidc/AccessDenyAccessAccessTestServlet.java

package org.littlewings.security.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.annotation.HttpConstraint;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebServlet;

@WebServlet("/deny")
@ServletSecurity(@HttpConstraint(ServletSecurity.EmptyRoleSemantic.DENY))
@ApplicationScoped
public class AccessDenyAccessAccessTestServlet extends OidcAccessTestServlet {}

アクセス制限をつけないServlet。

package org.littlewings.security.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.annotation.WebServlet;

@WebServlet("/plain")
@ApplicationScoped
public class PlainServlet extends OidcAccessTestServlet {}

ログイン時のコールバック用のServlet。

src/main/java/org/littlewings/security/oidc/CallbackServlet.java

package org.littlewings.security.oidc;

import jakarta.inject.Inject;
import jakarta.security.enterprise.SecurityContext;
import jakarta.security.enterprise.authentication.mechanism.http.openid.OpenIdConstant;
import jakarta.security.enterprise.identitystore.openid.OpenIdContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.util.Optional;

@WebServlet("/callback")
public class CallbackServlet extends HttpServlet {
    @Inject
    private SecurityContext securityContext;

    @Inject
    private OpenIdContext context;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Principal principal = securityContext.getCallerPrincipal();

        if (!"anonymous".equals(principal.getName())) {
            Optional<String> originalRequest =
                    context.getStoredValue(request, response, OpenIdConstant.ORIGINAL_REQUEST);
            String originalRequestString = originalRequest.get();
            response.sendRedirect(originalRequestString);
        }
    }
}

redirectURIに割り当てていたのはこちらですね。

@OpenIdAuthenticationMechanismDefinition(
        providerURI = "${oidcConfig.providerUri}",
        clientId = "${oidcConfig.clientId}",
        clientSecret = "${oidcConfig.clientSecret}",
        scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE},
        redirectURI = "${baseURL}/callback",
        // redirectToOriginalResource = true,  // trueにすると、アクセスしたページに戻ろうとする
        tokenAutoRefresh = true,
        claimsDefinition = @ClaimsDefinition(callerGroupsClaim = "roles")
)

このServletではOpenIdConstantのORIGINAL_REQUESTを使ってOpenIdContextから値を取得することで、
未ログイン時にアクセスしたURLにリダイレクトさせています。

近いことはredirectToOriginalResourcetrueにすることでもできるのですが、こちらはOpenID Connectのフローで使われる
大量のQueryStringも付いてきます…。

これで準備は完了です。

動作確認する

それでは動作確認していきましょう。やることは前のエントリーと同じです。

WildFlyを起動。




ちなみに、今回のWildFly Maven Pluginの設定だとパッケージングして起動する方法ではJASPIが無効にならずうまくいきません。
※JASPIは後で

まずは未ログイン状態でhttp://localhost:8080/plainにアクセスしてみます。

結果。

=============== Principal ===============
name = anonymous
=============== Principal ===============

次にhttp://localhost:8080/leader-or-generalにアクセスすると、Keycloakのログイン画面にリダイレクトされます。

まずはリーダーとしてログイン。

ログインすると、コールバック用のSerlvetに1度戻ってきてから、見ていたページにリダイレクトします。

テキストでも。

=============== Principal ===============
name = leader-user
=============== Principal ===============

=============== OpenIdContext ===============
subject = 02139241-52ef-409c-8388-454536d4a5cf

claims preferred_username = leader-user
claims roles = [leader-role]

access token string = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWSDkxSlJURXR5bDZUN1Bkb2Zib0l6STBGREJ1LVlZYl9GU1dObFlzNWhzIn0.eyJleHAiOjE3ODY4NzEyODQsImlhdCI6MTc4Njg3MDk4NCwiYXV0aF90aW1lIjoxNzg2ODcwOTg0LCJqdGkiOiJvbnJ0YWM6MDA2Y2YzMzUtMzExMS0yMmQ5LTEyNDUtYmZlOTk3ZWUzNWY0IiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiMDIxMzkyNDEtNTJlZi00MDljLTgzODgtNDU0NTM2ZDRhNWNmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZWUtY2xpZW50Iiwic2lkIjoiR1U0Ykstd0xYN2NWSFc5bUUtTGM2cEhlIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJsZWFkZXItcm9sZSJdfSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicm9sZXMiOlsibGVhZGVyLXJvbGUiXSwibmFtZSI6IkxlYWRlciBVc2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibGVhZGVyLXVzZXIiLCJnaXZlbl9uYW1lIjoiTGVhZGVyIiwiZmFtaWx5X25hbWUiOiJVc2VyIiwiZW1haWwiOiJsZWFkZXItdXNlckBleGFtcGxlLmNvbSJ9.VQJi9k8NAbxhOkNMjKn-_klSF4axdklo_Op65JJ3ReFqtXHBO_2QHdhOyIxmJV7plPuPDQ1CAwYfEavDJ1y1JCZSk7I34Y2YeOCxqXvoeZ6bO9sKSH90mXmZAsqQ_VrsUxVv2uz6yhABCBYIYTSt2BbJEOO-3fA9nf6xcXMUkUYWwjgoBUXHVTVYBMV_oSTOgNgPk645oB5Qj673hXqR5IybRk-7fGnFPwWFpXG4a-sb4XOEnYJkdda_uSdpJ2_e1DS7a4VXgzvtiMn1Vv2q8AWuaSzsZ2gj87t_OO53a2o62X0euAt8n49oJDCFdgkYPTOw5L3TDJkeslnSQUyxEw

access tokens claims
  exp = Sun Aug 16 18:08:04 JST 2026
  iat = Sun Aug 16 18:03:04 JST 2026
  auth_time = 1786870984
  jti = onrtac:006cf335-3111-22d9-1245-bfe997ee35f4
  iss = http://172.17.0.2:8080/realms/sample-realm
  sub = 02139241-52ef-409c-8388-454536d4a5cf
  typ = Bearer
  azp = ee-client
  sid = GU4bK-wLX7cVHW9mE-Lc6pHe
  acr = 1
  realm_access = {roles=[leader-role]}
  scope = openid profile email
  email_verified = false
  roles = [leader-role]
  name = Leader User
  preferred_username = leader-user
  given_name = Leader
  family_name = User
  email = leader-user@example.com

id token string = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWSDkxSlJURXR5bDZUN1Bkb2Zib0l6STBGREJ1LVlZYl9GU1dObFlzNWhzIn0.eyJleHAiOjE3ODY4NzEyODQsImlhdCI6MTc4Njg3MDk4NCwiYXV0aF90aW1lIjoxNzg2ODcwOTg0LCJqdGkiOiJmYTM3Yzk4Yi0xOTQwLTQ2YTktMzUxOC0wMDkzMWViOTJhMDYiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJlZS1jbGllbnQiLCJzdWIiOiIwMjEzOTI0MS01MmVmLTQwOWMtODM4OC00NTQ1MzZkNGE1Y2YiLCJ0eXAiOiJJRCIsImF6cCI6ImVlLWNsaWVudCIsIm5vbmNlIjoiai1rcnc3NEFmSktFYm0zNVhBMmI2LVVoY1V6cWZVVU0yd1VoRWVDd3Z2MCIsInNpZCI6IkdVNGJLLXdMWDdjVkhXOW1FLUxjNnBIZSIsImF0X2hhc2giOiJsdXRyZ3h6WGF1SWx3WGw0QzN1ZlZnIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkxlYWRlciBVc2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibGVhZGVyLXVzZXIiLCJnaXZlbl9uYW1lIjoiTGVhZGVyIiwiZmFtaWx5X25hbWUiOiJVc2VyIiwiZW1haWwiOiJsZWFkZXItdXNlckBleGFtcGxlLmNvbSJ9.vGz7tiHWtweSNVjn6DMPQ1GduvQX0vtmUy1xhDwnQqsAOP8jIWTmbpPYpojIDAFQpncQ79D_XwSJQkSIhHdRBg-1MaPbAF0u-W8NHe8dcsTPQSmp6in0RC5X-PPWPsjGlwsJwR2tRhvGx-rUPWfH0qQYyK8qsfObVurQTY00fHdMF0m1uKOvp0LjHL36g6dhO7GvLJSX8ZqnkJWlXcExTW_ESnmWADJ4P5I3a0WZKCzy1JEGhaAFE7ZJpT2D_Sj8usyMP-zSp58PJTaY1GlQkClMVH9OMA_lGLYAdnHs7KWL5Gay_uTdjQ0jGDHJe9n1K22_lUjEMwyesOGm8uQrpg

id tokens claims
  exp = Sun Aug 16 18:08:04 JST 2026
  iat = Sun Aug 16 18:03:04 JST 2026
  auth_time = 1786870984
  jti = fa37c98b-1940-46a9-3518-00931eb92a06
  iss = http://172.17.0.2:8080/realms/sample-realm
  aud = [ee-client]
  sub = 02139241-52ef-409c-8388-454536d4a5cf
  typ = ID
  azp = ee-client
  nonce = j-krw74AfJKEbm35XA2b6-UhcUzqfUUM2wUhEeCwvv0
  sid = GU4bK-wLX7cVHW9mE-Lc6pHe
  at_hash = lutrgxzXauIlwXl4C3ufVg
  acr = 1
  email_verified = false
  name = Leader User
  preferred_username = leader-user
  given_name = Leader
  family_name = User
  email = leader-user@example.com
=============== OpenIdContext ===============

次に一般ユーザーのみがアクセスできるhttp://localhost:8080/general-onlyを表示してみると、Forbiddenになります。

あとは表示内容は記載しませんが、アクセス結果を書いておきます。

  • /leader-only … アクセス可能、/leader-or-generalと同じ結果が表示される
  • /plain … アクセス可能、/leader-or-generalと同じ結果が表示される
  • /deny … Forbidden

次は一般ユーザーでアクセスします。

未ログイン状態でhttp://localhost:8080/leader-or-generalへアクセスして、Keycloakへログイン。

結果。

=============== Principal ===============
name = general-user
=============== Principal ===============

=============== OpenIdContext ===============
subject = 099146a0-ee62-42bc-81d3-5c1611f14a58

claims preferred_username = general-user
claims roles = [general-role]

access token string = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWSDkxSlJURXR5bDZUN1Bkb2Zib0l6STBGREJ1LVlZYl9GU1dObFlzNWhzIn0.eyJleHAiOjE3ODY4NzE0NjcsImlhdCI6MTc4Njg3MTE2NywiYXV0aF90aW1lIjoxNzg2ODcxMTY3LCJqdGkiOiJvbnJ0YWM6MmZlMzU2NTEtNGQzNi0zMDU5LTFmMjEtZjczYzVlMzkzOTM5IiwiaXNzIjoiaHR0cDovLzE3Mi4xNy4wLjI6ODA4MC9yZWFsbXMvc2FtcGxlLXJlYWxtIiwic3ViIjoiMDk5MTQ2YTAtZWU2Mi00MmJjLTgxZDMtNWMxNjExZjE0YTU4IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiZWUtY2xpZW50Iiwic2lkIjoiV05HbTRXcmJnYUJVZ19UMkRRSXpPWjc1IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJnZW5lcmFsLXJvbGUiXX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInJvbGVzIjpbImdlbmVyYWwtcm9sZSJdLCJuYW1lIjoiR2VuZXJhbCBVc2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZ2VuZXJhbC11c2VyIiwiZ2l2ZW5fbmFtZSI6IkdlbmVyYWwiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJlbWFpbCI6ImdlbmVyYWwtdXNlckBleGFtcGxlLmNvbSJ9.ALL49UBfRui5lVokApy7myqt08TO6VfOspwmoULMu3zfzu9FEFzGjc0oQeg_GII0CchaZix9glKmZTCN13xtMarpjsb1o-O-ReUhKvkGZ_0p0oTm_r51HHd7HKoLjfT1RjsG-unVgxPM_VAgVarRMToIsIwfjNgzy5-927VgqBn25E-Dc0Mx-cjIRs8XWFwgfGiAb_6ncyQBixul3LSohGFJsRblFvwpFUfsJqiLFku8BfML_Xx4HdiuK91gVukQa3uoGQPEsbrcLix5zwpLi-5pAZ0nCWRjsftQ05Nhr2S335N6XaF4g5T13BpFFCKSPIf0lpFwEz7Nr_cjbtpP7A

access tokens claims
  exp = Sun Aug 16 18:11:07 JST 2026
  iat = Sun Aug 16 18:06:07 JST 2026
  auth_time = 1786871167
  jti = onrtac:2fe35651-4d36-3059-1f21-f73c5e393939
  iss = http://172.17.0.2:8080/realms/sample-realm
  sub = 099146a0-ee62-42bc-81d3-5c1611f14a58
  typ = Bearer
  azp = ee-client
  sid = WNGm4WrbgaBUg_T2DQIzOZ75
  acr = 1
  realm_access = {roles=[general-role]}
  scope = openid profile email
  email_verified = false
  roles = [general-role]
  name = General User
  preferred_username = general-user
  given_name = General
  family_name = User
  email = general-user@example.com

id token string = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWSDkxSlJURXR5bDZUN1Bkb2Zib0l6STBGREJ1LVlZYl9GU1dObFlzNWhzIn0.eyJleHAiOjE3ODY4NzE0NjcsImlhdCI6MTc4Njg3MTE2NywiYXV0aF90aW1lIjoxNzg2ODcxMTY3LCJqdGkiOiIzMmJlYjA4Mi1hMjEyLWM5MTctOGE4MC02NjllYTliODdlMmMiLCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMjo4MDgwL3JlYWxtcy9zYW1wbGUtcmVhbG0iLCJhdWQiOiJlZS1jbGllbnQiLCJzdWIiOiIwOTkxNDZhMC1lZTYyLTQyYmMtODFkMy01YzE2MTFmMTRhNTgiLCJ0eXAiOiJJRCIsImF6cCI6ImVlLWNsaWVudCIsIm5vbmNlIjoiMnA5QVdSbG9ySVYxZVEtR2VzUi1hb0owMkFkeWY5Zjc5TXNJY01FYnEzTSIsInNpZCI6IldOR200V3JiZ2FCVWdfVDJEUUl6T1o3NSIsImF0X2hhc2giOiI1SFl1eEhqUVhRSHFsQjd0YUQ1eUh3IiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkdlbmVyYWwgVXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImdlbmVyYWwtdXNlciIsImdpdmVuX25hbWUiOiJHZW5lcmFsIiwiZmFtaWx5X25hbWUiOiJVc2VyIiwiZW1haWwiOiJnZW5lcmFsLXVzZXJAZXhhbXBsZS5jb20ifQ.OmviauOCHScXA3-hF3v67ms4NyYrJgIlGBK0tpIhV_cx520tq4q1-wj9YH-WlbsYoP2bLA0Yfm9Y1Y9dUNY12UoYkEGG8eG8IN4KJ8Z1RohtKNLNuhpcvYWDVtru8U9qoVkDPd3H5jjkk7uG5T9f03KVVtL8e8RzdyBotQPg7QjOtfRMAy-p5aDe7de-OJuYwg3lxtt0aoeV0u3QJdeLOVaFYDzr7fK2R2F1rAAiWUQTIybjfo4Uzi_gblL415VljzpCx1t7S9ASkvCj-LjMN02z9ZIDPoTpmpoucf53yElhjZLODFdpLMD5uMhhNL1vbgg-Xo7rOsRqp2q5d6klKg

id tokens claims
  exp = Sun Aug 16 18:11:07 JST 2026
  iat = Sun Aug 16 18:06:07 JST 2026
  auth_time = 1786871167
  jti = 32beb082-a212-c917-8a80-669ea9b87e2c
  iss = http://172.17.0.2:8080/realms/sample-realm
  aud = [ee-client]
  sub = 099146a0-ee62-42bc-81d3-5c1611f14a58
  typ = ID
  azp = ee-client
  nonce = 2p9AWRlorIV1eQ-GesR-aoJ02Adyf9f79MsIcMEbq3M
  sid = WNGm4WrbgaBUg_T2DQIzOZ75
  at_hash = 5HYuxHjQXQHqlB7taD5yHw
  acr = 1
  email_verified = false
  name = General User
  preferred_username = general-user
  given_name = General
  family_name = User
  email = general-user@example.com
=============== OpenIdContext ===============

他のパスへのアクセス結果も記載しておきます。

  • /leader-only … Forbidden
  • /general-only … アクセス可能、/leader-or-generalと同じ結果が表示される
  • /plain … アクセス可能、/leader-or-generalと同じ結果が表示される
  • /deny … Forbidden

ちなみに、未ログイン状態だと以下のようになります。

  • /plain … 最初に書いたように名前だけ表示される
  • /deny … Forbidden
  • その他のページはKeycloakへ

というわけで、設定どおり動きました。

オマケ

WildFlyではJASPIとのインテグレーションを無効にする

WildFly Maven Pluginで設定していた以下の部分ですが、こちらはJASPIを無効にして資格情報の検証を無効にする設定です。

                      <commands>
                          <command>/subsystem=undertow/application-security-domain=other:write-attribute(name=integrated-jaspi, value=false)</command>
                      </commands>

やっておかないと、Keycloakにログインしてアプリケーション側に戻ってきた時にこういう例外を見ることになります。

16:06:44,573 ERROR [io.undertow.request] (default task-2) UT005023: Exception handling request to /callback: java.lang.IllegalStateException: java.io.IOException: java.io.IOException: ELY01177: Authorization failed.
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.jaspic.Jaspic.handleCallbacks(Jaspic.java:172)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.jaspic.Jaspic.notifyContainerAboutLogin(Jaspic.java:145)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.HttpMessageContextImpl.notifyContainerAboutLogin(HttpMessageContextImpl.java:220)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.HttpMessageContextImpl.notifyContainerAboutLogin(HttpMessageContextImpl.java:198)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.OpenIdAuthenticationMechanism.validateAuthorizationCode(OpenIdAuthenticationMechanism.java:379)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.OpenIdAuthenticationMechanism.authenticate(OpenIdAuthenticationMechanism.java:287)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.OpenIdAuthenticationMechanism.validateRequest(OpenIdAuthenticationMechanism.java:175)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.OpenIdAuthenticationMechanism$Proxy$_$$_WeldClientProxy.validateRequest(Unknown Source)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
        at java.base/java.lang.reflect.Method.invoke(Method.java:565)
        at org.jboss.weld.core@6.0.4.Final//org.jboss.weld.bean.proxy.AbstractBeanInstance.invoke(AbstractBeanInstance.java:39)
        at org.jboss.weld.core@6.0.4.Final//org.jboss.weld.bean.proxy.ProxyMethodHandler.invoke(ProxyMethodHandler.java:109)
        at org.glassfish.soteria@4.0.2//org.jboss.weld.generated.proxies.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism$1008466176$Proxy$_$$_WeldClientProxy.validateRequest(Unknown Source)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.DefaultHttpAuthenticationMechanismHandler.validateRequest(DefaultHttpAuthenticationMechanismHandler.java:39)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
        at java.base/java.lang.reflect.Method.invoke(Method.java:565)
        at org.jboss.weld.core@6.0.4.Final//org.jboss.weld.bean.proxy.AbstractBeanInstance.invoke(AbstractBeanInstance.java:39)
        at org.jboss.weld.core@6.0.4.Final//org.jboss.weld.bean.proxy.ProxyMethodHandler.invoke(ProxyMethodHandler.java:109)
        at org.glassfish.soteria@4.0.2//org.jboss.weld.generated.proxies.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanismHandler$421417595$Proxy$_$$_WeldClientProxy.validateRequest(Unknown Source)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.jaspic.HttpBridgeServerAuthModule.validateRequest(HttpBridgeServerAuthModule.java:87)
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.ElytronServerAuthContext.validateRequest(ElytronServerAuthContext.java:85)
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.WrappingServerAuthContext.lambda$validateRequest$0(WrappingServerAuthContext.java:50)
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.ThreadLocalCallbackHandler.get(ThreadLocalCallbackHandler.java:56)
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.WrappingServerAuthContext.validateRequest(WrappingServerAuthContext.java:50)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.2.1.Final//org.wildfly.elytron.web.undertow.server.servlet.ServletSecurityContextImpl.authenticate(ServletSecurityContextImpl.java:180)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.2.1.Final//org.wildfly.elytron.web.undertow.server.servlet.ServletSecurityContextImpl.authenticate(ServletSecurityContextImpl.java:102)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:55)
        at io.undertow.core@2.4.2.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.core@2.4.2.Final//io.undertow.security.handlers.AuthenticationConstraintHandler.handleRequest(AuthenticationConstraintHandler.java:53)
        at io.undertow.core@2.4.2.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.security.ServletSecurityConstraintHandler.handleRequest(ServletSecurityConstraintHandler.java:60)
        at io.undertow.core@2.4.2.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.2.1.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
        at io.undertow.core@2.4.2.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@41.0.0.Final//org.wildfly.extension.undertow.deployment.ControlPointDeploymentInfoConfigurator$ControlPointRequestAttributeHandler.handleRequest(ControlPointDeploymentInfoConfigurator.java:175)
        at org.wildfly.extension.undertow@41.0.0.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler$1$1.handleRequest(JACCContextIdHandler.java:74)
        at io.undertow.core@2.4.2.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
        at io.undertow.core@2.4.2.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:278)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.call(ServletInitialHandler.java:131)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.call(ServletInitialHandler.java:128)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
        at org.wildfly.extension.undertow@41.0.0.Final//org.wildfly.extension.undertow.deployment.ControlPointDeploymentInfoConfigurator$1.call(ControlPointDeploymentInfoConfigurator.java:100)
        at org.wildfly.extension.undertow@41.0.0.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler$2$1.call(JACCContextIdHandler.java:103)
        at org.wildfly.extension.undertow@41.0.0.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1428)
        at org.wildfly.extension.undertow@41.0.0.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1428)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:258)
        at io.undertow.servlet@2.0.1.Final//io.undertow.servlet.handlers.ServletInitialHandler.lambda$new$1(ServletInitialHandler.java:100)
        at io.undertow.core@2.4.2.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:368)
        at io.undertow.core@2.4.2.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:935)
        at org.jboss.threads@3.9.2//org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
        at org.jboss.threads@3.9.2//org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2651)
        at org.jboss.threads@3.9.2//org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2630)
        at org.jboss.threads@3.9.2//org.jboss.threads.EnhancedQueueExecutor.runThreadBody(EnhancedQueueExecutor.java:1622)
        at org.jboss.threads@3.9.2//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1589)
        at org.jboss.xnio@3.8.16.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
        at java.base/java.lang.Thread.run(Thread.java:1474)
Caused by: java.io.IOException: java.io.IOException: ELY01177: Authorization failed.
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.JaspiAuthenticationContext$1.handle(JaspiAuthenticationContext.java:143)
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.ThreadLocalCallbackHandler.handle(ThreadLocalCallbackHandler.java:49)
        at org.glassfish.soteria@4.0.2//org.glassfish.soteria.mechanisms.jaspic.Jaspic.handleCallbacks(Jaspic.java:166)
        ... 59 more
Caused by: java.io.IOException: ELY01177: Authorization failed.
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.JaspiAuthenticationContext$1.handleOne(JaspiAuthenticationContext.java:255)
        at org.wildfly.security.jakarta.authentication@4.0.0.Final//org.wildfly.security.auth.jaspi.impl.JaspiAuthenticationContext$1.handle(JaspiAuthenticationContext.java:134)
        ... 61 more

出典はAuth0のブログです。

Use Jakarta EE 10 with OpenID Connect Authentication

PKCE

Keycloakでは、PKCEを強制できるように設定できます。

resource "keycloak_openid_client" "ee_client" {
  realm_id  = keycloak_realm.sample_realm.id
  client_id = "ee-client"

  name    = "Jakarta Security Client"
  enabled = true

  access_type                  = "CONFIDENTIAL"
  standard_flow_enabled        = true
  direct_access_grants_enabled = true # テスト用

  pkce_code_challenge_method = "S256" # PKCE

  root_url            = "http://localhost:8080"
  valid_redirect_uris = ["http://localhost:8080/*"]
}

Jakarta Securityでは特にPKCEに言及がなく、Soteriaとしてもどうなっているのだろうとちょっと試してみました。

これをやるとどうなるかですが、ログインが必要なページにアクセスしてKeycloakにリダイレクトした時に失敗します。
つまり、Keycloakのログイン画面が見えなくなります。

Keycloak側のログ。

2026-08-16 09:13:28,920 INFO  [org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker] (executor-thread-63) PKCE enforced Client without code challenge method.
2026-08-16 09:13:28,920 WARN  [org.keycloak.events] (executor-thread-63) type="LOGIN_ERROR", realmId="sample-realm", realmName="sample-realm", clientId="ee-client", userId="null", ipAddress="172.17.0.1", error="invalid_request", reason="Missing parameter: code_challenge_method", response_type="code", redirect_uri="http://localhost:8080/callback", response_mode="query"
2026-08-16 09:13:28,920 INFO  [org.keycloak.http.access-log] (executor-thread-63) 172.17.0.1 - - [16/Aug/2026:09:13:28 +0000] "GET /realms/sample-realm/protocol/openid-connect/auth?scope=openid+email+profile&response_type=code&client_id=ee-client&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&state=93da40f0-6d37-4166-9f6a-7bfac292033b&nonce=JYVAuEsWO7n78lDPOyxIGMpvrqbqEutOg-7KkjvtMrU&display=page HTTP/1.1" 302 -

まあ、仕様書に記載がないのでうまくいかないであろうことと、Soteriaのソースコードも対応指定なさそうな雰囲気を
感じたので、そうでしょうね、というところではありますが。

クライアントプロトコルマッパー

こちらは前回のエントリーを参照…。

WildFly 34とKeycloak 26でJakarta SecurityのOpenID Connectを使った認証と認可設定を試す - CLOVER🍀

おわりに

Jakarta Securityを使ったOpenID Connectを使った認証、それから認可設定を試してみました。

前回はとてもとても苦労しましたが、1回やっておいたのでなんとかなりましたね…。ほとんどそのまま使える情報で
よかったです…。

Keycloak 26(26.4.0以降)でアクセスログを有効にする

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

以前、こんなエントリーを書きました。

Keycloak 25でアクセスログを有効にする(Quarkusのプロパティを指定する) - CLOVER🍀

久しぶりに設定を見返してみると、Keycloak自体にHTTPアクセスログに関する設定が増えていたのでこちらを試してみましょう。

Keycloakでアクセスログを有効にする

以前のエントリーでは、「Keycloakにアクセスログに関する設定はない」、「有効にしたい場合はQuarkusのプロパティーを使う」と
書いていました。

Keycloak 25でアクセスログを有効にする(Quarkusのプロパティを指定する) - CLOVER🍀

今、ドキュメントを見るとアクセスログに関する記述が増えています。

Configuring logging / HTTP Access Logging

起動時のオプションで--http-access-log-enabled=trueを指定すればよさそうです。

こちらが追加されたのはKeycloak 26.4.0のようです。2025年9月のリリースなので、1年前ですね。

HTTP access logging of incoming HTTP requests

Keycloak 26.4.0 released - Keycloak

動作しているのはQuarkusのHTTPアクセスログの機能であることには代わりはないみたいなので、設定したい場合は
QuarkusのHTTPアクセスログに関するプロパティーを見ることになります。

少し試してみましょう。

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.3 2026-04-21
OpenJDK Runtime Environment (build 25.0.3+9-2-24.04.2-Ubuntu)
OpenJDK 64-Bit Server VM (build 25.0.3+9-2-24.04.2-Ubuntu, mixed mode, sharing)

Keycloak。

$ bin/kc.sh --version
Keycloak 26.7.1
JVM: 25.0.3 (Ubuntu OpenJDK 64-Bit Server VM 25.0.3+9-2-24.04.2-Ubuntu)
OS: Linux 6.8.0-137-generic amd64

Keycloakのアクセスログを有効にする

Keycloakのアクセスログを有効にするには、起動時に--http-access-log-enabled=trueオプションを付与します。

$ KC_BOOTSTRAP_ADMIN_USERNAME=keycloak-admin KC_BOOTSTRAP_ADMIN_PASSWORD=password bin/kc.sh start-dev --http-access-log-enabled=true

確認。

$ curl localhost:8080/admin/master/console/

アクセスログが記録されるようになりました。

2026-08-09 21:36:36,710 INFO  [org.keycloak.http.access-log] (executor-thread-1) 127.0.0.1 - - [09/Aug/2026:21:36:36 +0900] "GET /admin/master/console/ HTTP/1.1" 200 3690

設定ファイルに書くパターンも試してみましょう。こちらではフォーマットをcombinedにしてみます。

$ grep -vE '^#|^$' conf/keycloak.conf
http-access-log-enabled=true
http-access-log-pattern=combined

--http-access-log-enabled=trueオプションを外して起動。

$ KC_BOOTSTRAP_ADMIN_USERNAME=keycloak-admin KC_BOOTSTRAP_ADMIN_PASSWORD=password bin/kc.sh start-dev

この状態でアクセスすると、以下のようなアクセスログが記録されます。

2026-08-09 21:38:37,856 INFO  [org.keycloak.http.access-log] (executor-thread-1) 127.0.0.1 - - [09/Aug/2026:21:38:37 +0900] "GET /admin/master/console/ HTTP/1.1" 200 3690 "-" "curl/8.5.0"

OKですね。

おわりに

Keycloakのアクセスログを有効にしてみました。

以前調べた時はQuarkusのプロパティーファイルを使うしか方法がありませんでしたが、それから1年後にはKeycloak自身で
設定できるようになっていたんですね。

今後はこちらを使いましょう。