適用於 WildFly 和 EAP 的 Keycloak SAML Galleon 功能套件

使用 Keycloak SAML Galleon 功能套件保護 WildFly 和 EAP 中的應用程式

SAML 轉接器以 Galleon 功能套件的形式發佈,適用於 WildFly 29 或更新版本。有關此主題的更多詳細資訊,請參閱 WildFly 文件。相同的選項也適用於 JBoss EAP 8 GA

如需了解如何將 Keycloak 與在最新 WildFly/EAP 上執行的 JakartaEE 應用程式整合的範例,請參閱 Keycloak Quickstart GitHub 儲存庫中的 servlet-saml-service-provider Jakarta 資料夾。

安裝

功能套件的佈建是分別使用 wildfly-maven-pluginwildfly-jar-maven-plugineap-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.0.5</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.0.5</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.xml 中將 auth-method 設定為 KEYCLOAK-SAML。您也必須使用標準 servlet 安全性來指定 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 設定之外的所有標準 servlet 設定。

使用 Keycloak SAML 子系統保護 WAR 的安全

您不必開啟 WAR 即可使用 Keycloak 保護其安全。或者,您可以透過 Keycloak SAML 轉接器子系統從外部保護其安全。雖然您不必指定 KEYCLOAK-SAML 作為 auth-method,但您仍然必須在 web.xml 中定義 security-constraints。但是,您不必建立 WEB-INF/keycloak-saml.xml 檔案。此中繼資料改為在您伺服器的 domain.xmlstandalone.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 Cookie 的 SameSite 值

瀏覽器計畫將 Cookie 的 SameSite 屬性的預設值設定為 Lax。此設定表示 Cookie 只會在請求來自相同網域時才會傳送至應用程式。此行為可能會影響 SAML POST 繫結,而該繫結可能會變得無法運作。為了保持 SAML 轉接器的完整功能,我們建議將 JSESSIONID Cookie 的 SameSite 值設定為 None,該 Cookie 是由您的容器所建立。如果未執行此操作,可能會導致每次請求 Keycloak 時重設容器的工作階段。

若要避免將 SameSite 屬性設定為 None,請考慮切換至 REDIRECT 繫結 (如果可以接受),或切換至不需要此因應措施的 OIDC 通訊協定。

若要在 WildFly/EAP 中將 JSESSIONID Cookie 的 SameSite 值設定為 None,請將具有以下內容的檔案 undertow-handlers.conf 新增至您應用程式的 WEB-INF 目錄。

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

WildFly 版本 19.1.0 起支援此設定。

向身分識別提供者註冊

對於每個基於 servlet 的轉接器,您為斷言取用者服務 URL 和單一登出服務註冊的端點必須是您的 servlet 應用程式的基本 URL,並附加 /saml,也就是 https://example.com/contextPath/saml

登出

有多種方法可以從 Web 應用程式登出。對於 Jakarta EE servlet 容器,您可以呼叫 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() 會傳回一個 Principal 物件,您可以將其類型轉換為名為 org.keycloak.adapters.saml.SamlPrincipal 的 Keycloak 特定類別。此物件可讓您查看原始斷言,並且也具有可查閱屬性值的便利函式。

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 針對基於 servlet 的用戶端轉接器有一些錯誤處理功能。當驗證中發生錯誤時,用戶端轉接器會呼叫 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 提供多租戶,這表示可以使用多個 Keycloak 領域保護單一目標應用程式 (WAR)。這些領域可以位於相同的 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.xml 中使用哪個 SamlConfigResolver 實作,並使用 keycloak.config.resolver 內容參數

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

當使用者已通過驗證並具有 SSO 工作階段時,若目前瀏覽器分頁中的驗證工作階段過期,Keycloak 就會發送此錯誤。此時 Keycloak 伺服器無法自動對使用者執行 SSO 重新驗證,並以成功回應重新導向回用戶端。當用戶端應用程式收到此類錯誤時,最好立即重試驗證,並向 Keycloak 伺服器發送新的 SAML 請求,由於 SSO 工作階段,這通常應會始終驗證使用者並重新導向回用戶端。如果伺服器返回註解的狀態,SAML 配接器會自動執行該重試。更多詳細資訊請參閱伺服器管理指南

本頁內容