Keycloak SAML Galleon 機能パック (WildFly および EAP 向け)

Keycloak SAML Galleon 機能パックを使用して WildFly および EAP でアプリケーションを保護する

SAML アダプターは、WildFly 29 以降向けの Galleon 機能パックとして配布されています。この件に関する詳細は、WildFly のドキュメントを参照してください。同様のオプションは、JBoss EAP 8 GA でも利用可能です。

最新の Wildfly/EAP で実行されている JakartaEE アプリケーションと Keycloak を統合する方法の例については、Keycloak Quickstart GitHub リポジトリservlet-saml-service-provider Jakarta フォルダーを参照してください。

インストール

機能パックのプロビジョニングは、それぞれ wildfly-maven-pluginwildfly-jar-maven-plugin、または eap-maven-plugin を使用して行われます。

wildfly maven plugin を使用したプロビジョニングの例

<plugin>
    <groupId>org.wildfly.plugins</groupId>
    <artifactId>wildfly-maven-plugin</artifactId>
    <version>5.0.0.Final</version>
    <configuration>
        <feature-packs>
            <feature-pack>
                <location>wildfly@maven(org.jboss.universe:community-universe)#32.0.1.Final</location>
            </feature-pack>
            <feature-pack>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-saml-adapter-galleon-pack</artifactId>
                <version>26.2.0</version>
            </feature-pack>
        </feature-packs>
        <layers>
            <layer>core-server</layer>
            <layer>web-server</layer>
            <layer>jaxrs-server</layer>
            <layer>datasources-web-server</layer>
            <layer>webservices</layer>
            <layer>keycloak-saml</layer>
            <layer>keycloak-client-saml</layer>
            <layer>keycloak-client-saml-ejb</layer>
        </layers>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>package</goal>
            </goals>
        </execution>
    </executions>
</plugin>

wildfly jar maven plugin を使用したプロビジョニングの例

<plugin>
    <groupId>org.wildfly.plugins</groupId>
    <artifactId>wildfly-jar-maven-plugin</artifactId>
    <version>11.0.2.Final</version>
    <configuration>
        <feature-packs>
            <feature-pack>
                <location>wildfly@maven(org.jboss.universe:community-universe)#32.0.1.Final</location>
            </feature-pack>
            <feature-pack>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-saml-adapter-galleon-pack</artifactId>
                <version>26.2.0</version>
            </feature-pack>
        </feature-packs>
        <layers>
            <layer>core-server</layer>
            <layer>web-server</layer>
            <layer>jaxrs-server</layer>
            <layer>datasources-web-server</layer>
            <layer>webservices</layer>
            <layer>keycloak-saml</layer>
            <layer>keycloak-client-saml</layer>
            <layer>keycloak-client-saml-ejb</layer>
        </layers>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>package</goal>
            </goals>
        </execution>
    </executions>
</plugin>

EAP maven plugin を使用したプロビジョニングの例

<plugin>
    <groupId>org.jboss.eap.plugins</groupId>
    <artifactId>eap-maven-plugin</artifactId>
    <version>1.0.0.Final-redhat-00014</version>
    <configuration>
        <channels>
            <channel>
                <manifest>
                    <groupId>org.jboss.eap.channels</groupId>
                    <artifactId>eap-8.0</artifactId>
                </manifest>
            </channel>
        </channels>
        <feature-packs>
            <feature-pack>
                <location>org.keycloak:keycloak-saml-adapter-galleon-pack</location>
            </feature-pack>
        </feature-packs>
        <layers>
            <layer>core-server</layer>
            <layer>web-server</layer>
            <layer>jaxrs-server</layer>
            <layer>datasources-web-server</layer>
            <layer>webservices</layer>
            <layer>keycloak-saml</layer>
            <layer>keycloak-client-saml</layer>
            <layer>keycloak-client-saml-ejb</layer>
        </layers>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>package</goal>
            </goals>
        </execution>
    </executions>
</plugin>

設定

SAML クライアントアダプターは、WAR デプロイメント内に配置された XML ファイル /WEB-INF/keycloak-saml.xml によって構成されます。構成は次のようになります。

<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
                       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                       xsi:schemaLocation="urn:keycloak:saml:adapter {saml_adapter_xsd_urn}">
    <SP entityID="https://127.0.0.1:8081/sales-post-sig/"
        sslPolicy="EXTERNAL"
        nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
        logoutPage="/logout.jsp"
        forceAuthentication="false"
        isPassive="false"
        turnOffChangeSessionIdOnLogin="false"
        autodetectBearerOnly="false">
        <Keys>
            <Key signing="true" >
                <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
                    <PrivateKey alias="https://127.0.0.1:8080/sales-post-sig/" password="test123"/>
                    <Certificate alias="https://127.0.0.1:8080/sales-post-sig/"/>
                </KeyStore>
            </Key>
        </Keys>
        <PrincipalNameMapping policy="FROM_NAME_ID"/>
        <RoleIdentifiers>
            <Attribute name="Role"/>
        </RoleIdentifiers>
        <RoleMappingsProvider id="properties-based-role-mapper">
            <Property name="properties.resource.location" value="/WEB-INF/role-mappings.properties"/>
        </RoleMappingsProvider>
        <IDP entityID="idp"
             signaturesRequired="true">
        <SingleSignOnService requestBinding="POST"
                             bindingUrl="https://127.0.0.1:8081/realms/demo/protocol/saml"
                    />

            <SingleLogoutService
                    requestBinding="POST"
                    responseBinding="POST"
                    postBindingUrl="https://127.0.0.1:8081/realms/demo/protocol/saml"
                    redirectBindingUrl="https://127.0.0.1:8081/realms/demo/protocol/saml"
                    />
            <Keys>
                <Key signing="true">
                    <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
                        <Certificate alias="demo"/>
                    </KeyStore>
                </Key>
            </Keys>
        </IDP>
     </SP>
</keycloak-saml-adapter>

${…​} エンクロージャーをシステムプロパティの置換として使用できます。たとえば、${jboss.server.config.dir} などです。XML 構成ファイルのさまざまな要素の詳細については、Keycloak SAML Galleon 機能パックの詳細な構成を参照してください。

WAR の保護

このセクションでは、WAR パッケージ内に構成を追加およびファイルを編集して WAR を直接保護する方法について説明します。

keycloak-saml.xml が作成され、WAR の WEB-INF ディレクトリにある場合、web.xmlauth-methodKEYCLOAK-SAML に設定する必要があります。また、標準のサーブレットセキュリティを使用して、URL に対するロールベースの制約を指定する必要があります。以下は、web.xml ファイルの例です。

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

	<module-name>customer-portal</module-name>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Admins</web-resource-name>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Customers</web-resource-name>
            <url-pattern>/customers/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <login-config>
        <auth-method>KEYCLOAK-SAML</auth-method>
        <realm-name>this is ignored currently</realm-name>
    </login-config>

    <security-role>
        <role-name>admin</role-name>
    </security-role>
    <security-role>
        <role-name>user</role-name>
    </security-role>
</web-app>

auth-method 設定を除くすべての標準サーブレット設定。

Keycloak SAML サブシステムを使用した WAR の保護

Keycloak で保護するために WAR を開く必要はありません。または、Keycloak SAML アダプターサブシステムを介して外部から保護することもできます。auth-method として KEYCLOAK-SAML を指定する必要はありませんが、web.xmlsecurity-constraints を定義する必要があります。ただし、WEB-INF/keycloak-saml.xml ファイルを作成する必要はありません。このメタデータは、代わりにサーバーの domain.xml または standalone.xml サブシステム構成セクションの XML 内で定義されます。

<extensions>
  <extension module="org.keycloak.keycloak-saml-adapter-subsystem"/>
</extensions>

<profile>
  <subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
    <secure-deployment name="WAR MODULE NAME.war">
      <SP entityID="APPLICATION URL">
        ...
      </SP>
    </secure-deployment>
  </subsystem>
</profile>

secure-deployment name 属性は、保護する WAR を識別します。その値は、web.xml で定義された module-name.war が付加されたものです。構成の残りの部分は、一般的なアダプター構成で定義されている keycloak-saml.xml 構成と同じ XML 構文を使用します。

構成例

<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
  <secure-deployment name="saml-post-encryption.war">
    <SP entityID="https://127.0.0.1:8080/sales-post-enc/"
        sslPolicy="EXTERNAL"
        nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
        logoutPage="/logout.jsp"
        forceAuthentication="false">
      <Keys>
        <Key signing="true" encryption="true">
          <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
            <PrivateKey alias="https://127.0.0.1:8080/sales-post-enc/" password="test123"/>
            <Certificate alias="https://127.0.0.1:8080/sales-post-enc/"/>
          </KeyStore>
        </Key>
      </Keys>
      <PrincipalNameMapping policy="FROM_NAME_ID"/>
      <RoleIdentifiers>
        <Attribute name="Role"/>
      </RoleIdentifiers>
      <IDP entityID="idp">
        <SingleSignOnService signRequest="true"
            validateResponseSignature="true"
            requestBinding="POST"
            bindingUrl="https://127.0.0.1:8080/realms/saml-demo/protocol/saml"/>

        <SingleLogoutService
            validateRequestSignature="true"
            validateResponseSignature="true"
            signRequest="true"
            signResponse="true"
            requestBinding="POST"
            responseBinding="POST"
            postBindingUrl="https://127.0.0.1:8080/realms/saml-demo/protocol/saml"
            redirectBindingUrl="https://127.0.0.1:8080/realms/saml-demo/protocol/saml"/>
        <Keys>
          <Key signing="true" >
            <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
              <Certificate alias="saml-demo"/>
            </KeyStore>
          </Key>
        </Keys>
      </IDP>
    </SP>
   </secure-deployment>
</subsystem>

JSESSIONID クッキーの SameSite 値の設定

ブラウザーは、クッキーの SameSite 属性のデフォルト値を Lax に設定することを計画しています。この設定は、リクエストが同じドメインから発信された場合にのみ、クッキーがアプリケーションに送信されることを意味します。この動作は、機能しなくなる可能性がある SAML POST バインディングに影響を与える可能性があります。SAML アダプターの完全な機能を維持するには、コンテナによって作成された JSESSIONID クッキーの SameSite 値を None に設定することをお勧めします。そうしないと、Keycloak へのリクエストごとにコンテナのセッションがリセットされる可能性があります。

SameSite 属性を None に設定することを避けるには、REDIRECT バインディングが許容される場合は REDIRECT バインディングに切り替えるか、この回避策が不要な OIDC プロトコルに切り替えることを検討してください。

Wildfly/EAP で JSESSIONID クッキーの SameSite 値を None に設定するには、次の内容を含む undertow-handlers.conf ファイルをアプリケーションの WEB-INF ディレクトリに追加します。

samesite-cookie(mode=None, cookie-pattern=JSESSIONID)

この構成のサポートは、Wildfly バージョン 19.1.0 以降で利用可能です。

ID プロバイダーへの登録

サーブレットベースのアダプターごとに、アサートコンシューマーサービス URL およびシングルログアウトサービスに登録するエンドポイントは、サーブレットアプリケーションのベース URL に /saml を付加したものである必要があります。つまり、https://example.com/contextPath/saml です。

ログアウト

Web アプリケーションからログアウトする方法は複数あります。Jakarta EE サーブレットコンテナの場合、HttpServletRequest.logout() を呼び出すことができます。その他のブラウザーアプリケーションの場合は、セキュリティ制約のある Web アプリケーションの任意の URL をブラウザーで指定し、クエリパラメーター GLO (例: http://myapp?GLO=true) を渡すことができます。これにより、ブラウザーで SSO セッションがある場合はログアウトされます。

クラスター環境でのログアウト

内部的には、SAML アダプターは、SAML セッションインデックス、プリンシパル名 (既知の場合)、および HTTP セッション ID の間のマッピングを保存します。このマッピングは、JBoss アプリケーションサーバーファミリー (WildFly 10/11、EAP 6/7) で、分散アプリケーションのクラスター間で維持できます。前提条件として、HTTP セッションはクラスター間で分散されている必要があります (つまり、アプリケーションはアプリケーションの web.xml<distributable/> タグでマークされています)。

この機能を有効にするには、次のセクションを /WEB_INF/web.xml ファイルに追加します。

<context-param>
    <param-name>keycloak.sessionIdMapperUpdater.classes</param-name>
    <param-value>org.keycloak.adapters.saml.wildfly.infinispan.InfinispanSessionCacheIdMapperUpdater</param-value>
</context-param>

デプロイメントのセッションキャッシュの名前が deployment-cache の場合、SAML マッピングに使用されるキャッシュの名前は deployment-cache.ssoCache になります。キャッシュの名前は、コンテキストパラメーター keycloak.sessionIdMapperUpdater.infinispan.cacheName でオーバーライドできます。キャッシュを含むキャッシュコンテナは、デプロイメントセッションキャッシュを含むものと同じになりますが、コンテキストパラメーター keycloak.sessionIdMapperUpdater.infinispan.containerName でオーバーライドできます。

デフォルトでは、SAML マッピングキャッシュの構成はセッションキャッシュから派生します。構成は、サーバーのキャッシュ構成セクションで、他のキャッシュと同様に手動でオーバーライドできます。

現在、信頼性の高いサービスを提供するために、SAML セッションキャッシュにはレプリケートされたキャッシュを使用することをお勧めします。分散キャッシュを使用すると、SAML ログアウトリクエストが SAML セッションインデックスから HTTP セッションマッピングへのアクセス権を持たないノードに到達し、ログアウトが失敗する結果になる可能性があります。

クロスサイトシナリオでのログアウト

複数のデータセンターにまたがるセッションを処理するには、特別な処理が必要です。次のシナリオを想像してください。

  1. ログインリクエストは、データセンター 1 のクラスター内で処理されます。

  2. 管理者が特定の SAML セッションのログアウトリクエストを発行し、リクエストはデータセンター 2 に到達します。

データセンター 2 は、データセンター 1 (および HTTP セッションを共有する他のすべてのデータセンター) に存在するすべてのセッションをログアウトする必要があります。

このケースに対応するために、上記で説明した SAML セッションキャッシュは、個々のクラスター内だけでなく、たとえば スタンドアロン Infinispan/JDG サーバー経由 など、すべてのデータセンター間でレプリケートする必要があります。

  1. キャッシュをスタンドアロン Infinispan/JDG サーバーに追加する必要があります。

  2. 前の項目のキャッシュは、それぞれの SAML セッションキャッシュのリモートストアとして追加する必要があります。

デプロイメント中に SAML セッションキャッシュにリモートストアが存在することが判明すると、変更が監視され、ローカル SAML セッションキャッシュがそれに応じて更新されます。

アサーション属性の取得

SAML ログインが成功した後、アプリケーションコードは SAML アサーションで渡された属性値を取得したい場合があります。HttpServletRequest.getUserPrincipal() は、org.keycloak.adapters.saml.SamlPrincipal という Keycloak 固有のクラスに型キャストできる Principal オブジェクトを返します。このオブジェクトを使用すると、生のアサーションを確認したり、属性値を検索するための便利な関数を使用したりできます。

package org.keycloak.adapters.saml;

public class SamlPrincipal implements Serializable, Principal {
    /**
     * Get full saml assertion
     *
     * @return
     */
    public AssertionType getAssertion() {
       ...
    }

    /**
     * Get SAML subject sent in assertion
     *
     * @return
     */
    public String getSamlSubject() {
        ...
    }

    /**
     * Subject nameID format
     *
     * @return
     */
    public String getNameIDFormat() {
        ...
    }

    @Override
    public String getName() {
        ...
    }

    /**
     * Convenience function that gets Attribute value by attribute name
     *
     * @param name
     * @return
     */
    public List<String> getAttributes(String name) {
        ...

    }

    /**
     * Convenience function that gets Attribute value by attribute friendly name
     *
     * @param friendlyName
     * @return
     */
    public List<String> getFriendlyAttributes(String friendlyName) {
        ...
    }

    /**
     * Convenience function that gets first  value of an attribute by attribute name
     *
     * @param name
     * @return
     */
    public String getAttribute(String name) {
        ...
    }

    /**
     * Convenience function that gets first  value of an attribute by attribute name
     *
     *
     * @param friendlyName
     * @return
     */
    public String getFriendlyAttribute(String friendlyName) {
        ...
    }

    /**
     * Get set of all assertion attribute names
     *
     * @return
     */
    public Set<String> getAttributeNames() {
        ...
    }

    /**
     * Get set of all assertion friendly attribute names
     *
     * @return
     */
    public Set<String> getFriendlyNames() {
        ...
    }
}

エラー処理

Keycloak には、サーブレットベースのクライアントアダプターのエラー処理機能がいくつかあります。認証でエラーが発生した場合、クライアントアダプターは HttpServletResponse.sendError() を呼び出します。web.xml ファイル内に error-page を設定して、エラーを任意の方法で処理できます。クライアントアダプターは、400、401、403、および 500 エラーをスローできます。

<error-page>
    <error-code>403</error-code>
    <location>/ErrorHandler</location>
</error-page>

クライアントアダプターは、取得できる HttpServletRequest 属性も設定します。属性名は org.keycloak.adapters.spi.AuthenticationError です。このオブジェクトを org.keycloak.adapters.saml.SamlAuthenticationError に型キャストします。このクラスは、何が起こったかを正確に教えてくれます。この属性が設定されていない場合、アダプターはエラーコードの原因ではありません。

public class SamlAuthenticationError implements AuthenticationError {
    public static enum Reason {
        EXTRACTION_FAILURE,
        INVALID_SIGNATURE,
        ERROR_STATUS
    }

    public Reason getReason() {
        return reason;
    }
    public StatusResponseType getStatus() {
        return status;
    }
}

トラブルシューティング

問題をトラブルシューティングする最良の方法は、クライアントアダプターと Keycloak サーバーの両方で SAML のデバッグをオンにすることです。ロギングフレームワークを使用して、org.keycloak.saml パッケージのログレベルを DEBUG に設定します。これをオンにすると、サーバーとの間で送受信される SAML リクエストとレスポンスドキュメントを確認できます。

マルチテナンシー

SAML はマルチテナンシーを提供します。つまり、単一のターゲットアプリケーション (WAR) を複数の Keycloak レルムで保護できます。レルムは、同じ Keycloak インスタンスまたは異なるインスタンスに配置できます。

これを行うには、アプリケーションに複数の keycloak-saml.xml アダプター構成ファイルが必要です。

異なるアダプター構成ファイルを持つ WAR の複数のインスタンスを異なるコンテキストパスにデプロイできますが、これは不便な場合があり、コンテキストパス以外の何かに基づいてレルムを選択したい場合もあります。

Keycloak を使用すると、カスタム構成リゾルバーを持つことができるため、リクエストごとに使用するアダプター構成を選択できます。SAML では、構成はログイン処理でのみ重要です。ユーザーがログインすると、セッションが認証され、返された keycloak-saml.xml が異なっていても問題ありません。そのため、同じセッションに対して同じ構成を返すのが正しい方法です。

これを実現するには、org.keycloak.adapters.saml.SamlConfigResolver の実装を作成します。次の例では、Host ヘッダーを使用して適切な構成を見つけてロードし、アプリケーションの Java クラスパスから関連する要素をロードします。

package example;

import java.io.InputStream;
import org.keycloak.adapters.saml.SamlConfigResolver;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.saml.common.exceptions.ParsingException;

public class SamlMultiTenantResolver implements SamlConfigResolver {

    @Override
    public SamlDeployment resolve(HttpFacade.Request request) {
        String host = request.getHeader("Host");
        String realm = null;
        if (host.contains("tenant1")) {
            realm = "tenant1";
        } else if (host.contains("tenant2")) {
            realm = "tenant2";
        } else {
            throw new IllegalStateException("Not able to guess the keycloak-saml.xml to load");
        }

        InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak-saml.xml");
        if (is == null) {
            throw new IllegalStateException("Not able to find the file /" + realm + "-keycloak-saml.xml");
        }

        ResourceLoader loader = new ResourceLoader() {
            @Override
            public InputStream getResourceAsStream(String path) {
                return getClass().getResourceAsStream(path);
            }
        };

        try {
            return new DeploymentBuilder().build(is, loader);
        } catch (ParsingException e) {
            throw new IllegalStateException("Cannot load SAML deployment", e);
        }
    }
}

web.xmlkeycloak.config.resolver コンテキストパラメーターを使用して、使用する SamlConfigResolver 実装も構成する必要があります。

<web-app>
    ...
    <context-param>
        <param-name>keycloak.config.resolver</param-name>
        <param-value>example.SamlMultiTenantResolver</param-value>
    </context-param>
</web-app>

Keycloak 固有のエラー

Keycloak サーバーは、SAML レスポンスでクライアントアプリケーションにエラーを送信できます。これには、次のような SAML ステータスが含まれる場合があります。

<samlp:Status>
  <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder">
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"/>
  </samlp:StatusCode>
  <samlp:StatusMessage>authentication_expired</samlp:StatusMessage>
</samlp:Status>

Keycloak は、ユーザーが認証され、SSO セッションを持っているが、現在のブラウザータブで認証セッションが期限切れになった場合にこのエラーを送信します。そのため、Keycloak サーバーはユーザーの SSO 再認証を自動的に実行して、成功したレスポンスでクライアントにリダイレクトバックできません。クライアントアプリケーションがこのタイプのエラーを受信した場合、すぐに認証を再試行し、新しい SAML リクエストを Keycloak サーバーに送信するのが理想的です。これにより、SSO セッションのために通常は常にユーザーが認証され、リダイレクトバックされるはずです。SAML アダプターは、コメントされたステータスがサーバーから返された場合、自動的に再試行を実行します。詳細については、サーバー管理ガイドを参照してください。

このページの内容