keycloak-memoizeit
Merge pull request #3472 from hmlnarik/KEYCLOAK-1881-saml-key-rotation Keycloak …
Changes
adapters/saml/core/pom.xml 6(+6 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java 75(+75 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java 32(+32 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java 11(+11 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java 47(+26 -21)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/IDPXmlParser.java 78(+60 -18)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java 7(+7 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java 101(+101 -0)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java 122(+113 -9)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java 8(+6 -2)
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java 175(+175 -0)
adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java 180(+180 -0)
adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml 62(+62 -0)
adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml.xml 0(+0 -0)
adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-invalid.xml 0(+0 -0)
adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-multiple-signing-keys.xml 81(+81 -0)
adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-wth-http-client-settings.xml 81(+81 -0)
core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java 75(+75 -0)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml 1(+1 -0)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml 1(+1 -0)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml 1(+1 -0)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-subsystem/main/module.xml 1(+1 -0)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml 1(+1 -0)
distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml 1(+1 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml 1(+1 -0)
saml-core/pom.xml 12(+12 -0)
saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java 23(+11 -12)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResolveParser.java 2(+2 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResponseParser.java 3(+3 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAttributeQueryParser.java 2(+2 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAuthNRequestParser.java 2(+2 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLExtensionsParser.java 82(+82 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLResponseParser.java 3(+3 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloRequestParser.java 2(+2 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloResponseParser.java 3(+3 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/BaseWriter.java 26(+26 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java 20(+20 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java 18(+18 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java 75(+75 -0)
saml-core/src/main/java/org/keycloak/saml/processing/core/util/SignatureUtilTransferObject.java 11(+11 -0)
saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java 107(+107 -0)
saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response.xml 29(+29 -0)
saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml 30(+30 -0)
saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-signed-logout-request.xml 32(+32 -0)
services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java 1(+1 -0)
services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java 16(+3 -13)
services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlSubsystemInstallation.java 2(+1 -1)
services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java 89(+53 -36)
services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java 4(+3 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigPostNoIdpKeyServlet.java 39(+39 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirNoIdpKeyServlet.java 39(+39 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirOptNoIdpKeyServlet.java 39(+39 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java 98(+98 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java 44(+21 -23)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java 101(+90 -11)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keycloak-saml.xml 54(+54 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keystore.jks 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keycloak-saml.xml 54(+54 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keystore.jks 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keycloak-saml.xml 54(+54 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keystore.jks 0(+0 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json 55(+55 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml 1(+1 -0)
testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml 39(+39 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
index 63cdab8..e6d6588 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
@@ -38,7 +38,7 @@ import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.keycloak.common.util.EnvUtil;
import org.keycloak.common.util.KeystoreUtil;
-import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.representations.adapters.config.AdapterHttpClientConfig;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
@@ -333,7 +333,7 @@ public class HttpClientBuilder {
}
}
- public HttpClient build(AdapterConfig adapterConfig) {
+ public HttpClient build(AdapterHttpClientConfig adapterConfig) {
disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
String truststorePath = adapterConfig.getTruststore();
@@ -379,13 +379,13 @@ public class HttpClientBuilder {
/**
* Configures a the proxy to use for auth-server requests if provided.
* <p>
- * If the given {@link AdapterConfig} contains the attribute {@code proxy-url} we use the
+ * If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
* given URL as a proxy server, otherwise the proxy configuration is ignored.
* </p>
*
* @param adapterConfig
*/
- private void configureProxyForAuthServerIfProvided(AdapterConfig adapterConfig) {
+ private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
return;
diff --git a/adapters/saml/core/nbproject/project.properties b/adapters/saml/core/nbproject/project.properties
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/adapters/saml/core/nbproject/project.properties
adapters/saml/core/pom.xml 6(+6 -0)
diff --git a/adapters/saml/core/pom.xml b/adapters/saml/core/pom.xml
index 16dce33..b01061b 100755
--- a/adapters/saml/core/pom.xml
+++ b/adapters/saml/core/pom.xml
@@ -34,6 +34,7 @@
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
</properties>
+
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
@@ -70,6 +71,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java
new file mode 100644
index 0000000..5c94fdb
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/AdapterHttpClientConfig.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.cloned;
+
+/**
+ * Configuration options relevant for configuring http client that can be used by adapter.
+ *
+ * NOTE: keep in sync with core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java until unified.
+ *
+ * @author hmlnarik
+ */
+public interface AdapterHttpClientConfig {
+
+ /**
+ * Returns truststore filename.
+ */
+ public String getTruststore();
+
+ /**
+ * Returns truststore password.
+ */
+ public String getTruststorePassword();
+
+ /**
+ * Returns keystore with client keys.
+ */
+ public String getClientKeystore();
+
+ /**
+ * Returns keystore password.
+ */
+ public String getClientKeystorePassword();
+
+ /**
+ * Returns boolean flag whether any hostname verification is done on the server's
+ * certificate, {@code true} means that verification is not done.
+ * @return
+ */
+ public boolean isAllowAnyHostname();
+
+ /**
+ * Returns boolean flag whether any trust management and hostname verification is done.
+ * <p>
+ * <i>NOTE</i> Disabling trust manager is a security hole, so only set this option
+ * if you cannot or do not want to verify the identity of the
+ * host you are communicating with.
+ */
+ public boolean isDisableTrustManager();
+
+ /**
+ * Returns size of connection pool.
+ */
+ public int getConnectionPoolSize();
+
+ /**
+ * Returns URL of HTTP proxy.
+ */
+ public String getProxyUrl();
+
+}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpAdapterUtils.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpAdapterUtils.java
new file mode 100644
index 0000000..0fa330e
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpAdapterUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.cloned;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+
+import java.io.IOException;
+import java.io.InputStream;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.util.EntityUtils;
+import org.keycloak.adapters.saml.descriptor.parsers.SamlDescriptorIDPKeysExtractor;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.saml.common.exceptions.ParsingException;
+
+/**
+ * @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
+ */
+public class HttpAdapterUtils {
+
+ public static MultivaluedHashMap<String, KeyInfo> downloadKeysFromSamlDescriptor(HttpClient client, String descriptorUrl) throws HttpClientAdapterException {
+ try {
+ HttpGet httpRequest = new HttpGet(descriptorUrl);
+ HttpResponse response = client.execute(httpRequest);
+ int status = response.getStatusLine().getStatusCode();
+ if (status != HttpStatus.SC_OK) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ throw new HttpClientAdapterException("Unexpected status = " + status);
+ }
+
+ HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ throw new HttpClientAdapterException("There was no entity.");
+ }
+
+ MultivaluedHashMap<String, KeyInfo> res;
+ try (InputStream is = entity.getContent()) {
+ res = extractKeysFromSamlDescriptor(is);
+ }
+
+ EntityUtils.consumeQuietly(entity);
+
+ return res;
+ } catch (IOException | ParsingException e) {
+ throw new HttpClientAdapterException("IO error", e);
+ }
+ }
+
+ /**
+ * Parses SAML descriptor and extracts keys from it.
+ * @param xmlStream
+ * @return List of KeyInfo objects containing keys from the descriptor.
+ * @throws IOException
+ */
+ public static MultivaluedHashMap<String, KeyInfo> extractKeysFromSamlDescriptor(InputStream xmlStream) throws ParsingException {
+ Object res = new SamlDescriptorIDPKeysExtractor().parse(xmlStream);
+ return (MultivaluedHashMap<String, KeyInfo>) res;
+ }
+
+}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java
new file mode 100644
index 0000000..e0371ad
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientAdapterException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.cloned;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class HttpClientAdapterException extends Exception {
+
+ public HttpClientAdapterException(String message) {
+ super(message);
+ }
+
+ public HttpClientAdapterException(String message, Throwable t) {
+ super(message, t);
+ }
+}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientBuilder.java
new file mode 100644
index 0000000..7e26c01
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/HttpClientBuilder.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.cloned;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.HttpClient;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnRoutePNames;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.SingleClientConnManager;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.keycloak.common.util.EnvUtil;
+import org.keycloak.common.util.KeystoreUtil;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.IOException;
+import java.net.URI;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Abstraction for creating HttpClients. Allows SSL configuration.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class HttpClientBuilder {
+ public static enum HostnameVerificationPolicy {
+ /**
+ * Hostname verification is not done on the server's certificate
+ */
+ ANY,
+ /**
+ * Allows wildcards in subdomain names i.e. *.foo.com
+ */
+ WILDCARD,
+ /**
+ * CN must match hostname connecting to
+ */
+ STRICT
+ }
+
+
+ /**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+ private static class PassthroughTrustManager implements X509TrustManager {
+ public void checkClientTrusted(X509Certificate[] chain,
+ String authType) throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain,
+ String authType) throws CertificateException {
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+
+ protected KeyStore truststore;
+ protected KeyStore clientKeyStore;
+ protected String clientPrivateKeyPassword;
+ protected boolean disableTrustManager;
+ protected boolean disableCookieCache = true;
+ protected HostnameVerificationPolicy policy = HostnameVerificationPolicy.WILDCARD;
+ protected SSLContext sslContext;
+ protected int connectionPoolSize = 100;
+ protected int maxPooledPerRoute = 0;
+ protected long connectionTTL = -1;
+ protected TimeUnit connectionTTLUnit = TimeUnit.MILLISECONDS;
+ protected HostnameVerifier verifier = null;
+ protected long socketTimeout = -1;
+ protected TimeUnit socketTimeoutUnits = TimeUnit.MILLISECONDS;
+ protected long establishConnectionTimeout = -1;
+ protected TimeUnit establishConnectionTimeoutUnits = TimeUnit.MILLISECONDS;
+ protected HttpHost proxyHost;
+
+
+ /**
+ * Socket inactivity timeout
+ *
+ * @param timeout
+ * @param unit
+ * @return
+ */
+ public HttpClientBuilder socketTimeout(long timeout, TimeUnit unit) {
+ this.socketTimeout = timeout;
+ this.socketTimeoutUnits = unit;
+ return this;
+ }
+
+ /**
+ * When trying to make an initial socket connection, what is the timeout?
+ *
+ * @param timeout
+ * @param unit
+ * @return
+ */
+ public HttpClientBuilder establishConnectionTimeout(long timeout, TimeUnit unit) {
+ this.establishConnectionTimeout = timeout;
+ this.establishConnectionTimeoutUnits = unit;
+ return this;
+ }
+
+ public HttpClientBuilder connectionTTL(long ttl, TimeUnit unit) {
+ this.connectionTTL = ttl;
+ this.connectionTTLUnit = unit;
+ return this;
+ }
+
+ public HttpClientBuilder maxPooledPerRoute(int maxPooledPerRoute) {
+ this.maxPooledPerRoute = maxPooledPerRoute;
+ return this;
+ }
+
+ public HttpClientBuilder connectionPoolSize(int connectionPoolSize) {
+ this.connectionPoolSize = connectionPoolSize;
+ return this;
+ }
+
+ /**
+ * Disable trust management and hostname verification. <i>NOTE</i> this is a security
+ * hole, so only set this option if you cannot or do not want to verify the identity of the
+ * host you are communicating with.
+ */
+ public HttpClientBuilder disableTrustManager() {
+ this.disableTrustManager = true;
+ return this;
+ }
+
+ public HttpClientBuilder disableCookieCache() {
+ this.disableCookieCache = true;
+ return this;
+ }
+
+ /**
+ * SSL policy used to verify hostnames
+ *
+ * @param policy
+ * @return
+ */
+ public HttpClientBuilder hostnameVerification(HostnameVerificationPolicy policy) {
+ this.policy = policy;
+ return this;
+ }
+
+
+ public HttpClientBuilder sslContext(SSLContext sslContext) {
+ this.sslContext = sslContext;
+ return this;
+ }
+
+ public HttpClientBuilder trustStore(KeyStore truststore) {
+ this.truststore = truststore;
+ return this;
+ }
+
+ public HttpClientBuilder keyStore(KeyStore keyStore, String password) {
+ this.clientKeyStore = keyStore;
+ this.clientPrivateKeyPassword = password;
+ return this;
+ }
+
+ public HttpClientBuilder keyStore(KeyStore keyStore, char[] password) {
+ this.clientKeyStore = keyStore;
+ this.clientPrivateKeyPassword = new String(password);
+ return this;
+ }
+
+
+ static class VerifierWrapper implements X509HostnameVerifier {
+ protected HostnameVerifier verifier;
+
+ VerifierWrapper(HostnameVerifier verifier) {
+ this.verifier = verifier;
+ }
+
+ @Override
+ public void verify(String host, SSLSocket ssl) throws IOException {
+ if (!verifier.verify(host, ssl.getSession())) throw new SSLException("Hostname verification failure");
+ }
+
+ @Override
+ public void verify(String host, X509Certificate cert) throws SSLException {
+ throw new SSLException("This verification path not implemented");
+ }
+
+ @Override
+ public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
+ throw new SSLException("This verification path not implemented");
+ }
+
+ @Override
+ public boolean verify(String s, SSLSession sslSession) {
+ return verifier.verify(s, sslSession);
+ }
+ }
+
+ public HttpClient build() {
+ X509HostnameVerifier verifier = null;
+ if (this.verifier != null) verifier = new VerifierWrapper(this.verifier);
+ else {
+ switch (policy) {
+ case ANY:
+ verifier = new AllowAllHostnameVerifier();
+ break;
+ case WILDCARD:
+ verifier = new BrowserCompatHostnameVerifier();
+ break;
+ case STRICT:
+ verifier = new StrictHostnameVerifier();
+ break;
+ }
+ }
+ try {
+ SSLSocketFactory sslsf = null;
+ SSLContext theContext = sslContext;
+ if (disableTrustManager) {
+ theContext = SSLContext.getInstance("SSL");
+ theContext.init(null, new TrustManager[]{new PassthroughTrustManager()},
+ new SecureRandom());
+ verifier = new AllowAllHostnameVerifier();
+ sslsf = new SniSSLSocketFactory(theContext, verifier);
+ } else if (theContext != null) {
+ sslsf = new SniSSLSocketFactory(theContext, verifier);
+ } else if (clientKeyStore != null || truststore != null) {
+ sslsf = new SniSSLSocketFactory(SSLSocketFactory.TLS, clientKeyStore, clientPrivateKeyPassword, truststore, null, verifier);
+ } else {
+ final SSLContext tlsContext = SSLContext.getInstance(SSLSocketFactory.TLS);
+ tlsContext.init(null, null, null);
+ sslsf = new SniSSLSocketFactory(tlsContext, verifier);
+ }
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(
+ new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
+ Scheme httpsScheme = new Scheme("https", 443, sslsf);
+ registry.register(httpsScheme);
+ ClientConnectionManager cm = null;
+ if (connectionPoolSize > 0) {
+ ThreadSafeClientConnManager tcm = new ThreadSafeClientConnManager(registry, connectionTTL, connectionTTLUnit);
+ tcm.setMaxTotal(connectionPoolSize);
+ if (maxPooledPerRoute == 0) maxPooledPerRoute = connectionPoolSize;
+ tcm.setDefaultMaxPerRoute(maxPooledPerRoute);
+ cm = tcm;
+
+ } else {
+ cm = new SingleClientConnManager(registry);
+ }
+ BasicHttpParams params = new BasicHttpParams();
+
+ if (proxyHost != null) {
+ params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost);
+ }
+
+ if (socketTimeout > -1) {
+ HttpConnectionParams.setSoTimeout(params, (int) socketTimeoutUnits.toMillis(socketTimeout));
+
+ }
+ if (establishConnectionTimeout > -1) {
+ HttpConnectionParams.setConnectionTimeout(params, (int) establishConnectionTimeoutUnits.toMillis(establishConnectionTimeout));
+ }
+ DefaultHttpClient client = new DefaultHttpClient(cm, params);
+
+ if (disableCookieCache) {
+ client.setCookieStore(new CookieStore() {
+ @Override
+ public void addCookie(Cookie cookie) {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public List<Cookie> getCookies() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean clearExpired(Date date) {
+ return false; //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void clear() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+ });
+
+ }
+ return client;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public HttpClient build(AdapterHttpClientConfig adapterConfig) {
+ disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
+
+ String truststorePath = adapterConfig.getTruststore();
+ if (truststorePath != null) {
+ truststorePath = EnvUtil.replace(truststorePath);
+ String truststorePassword = adapterConfig.getTruststorePassword();
+ try {
+ this.truststore = KeystoreUtil.loadKeyStore(truststorePath, truststorePassword);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load truststore", e);
+ }
+ }
+ String clientKeystore = adapterConfig.getClientKeystore();
+ if (clientKeystore != null) {
+ clientKeystore = EnvUtil.replace(clientKeystore);
+ String clientKeystorePassword = adapterConfig.getClientKeystorePassword();
+ try {
+ KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
+ keyStore(clientCertKeystore, clientKeystorePassword);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load keystore", e);
+ }
+ }
+ int size = 10;
+ if (adapterConfig.getConnectionPoolSize() > 0)
+ size = adapterConfig.getConnectionPoolSize();
+ HttpClientBuilder.HostnameVerificationPolicy policy = HttpClientBuilder.HostnameVerificationPolicy.WILDCARD;
+ if (adapterConfig.isAllowAnyHostname())
+ policy = HttpClientBuilder.HostnameVerificationPolicy.ANY;
+ connectionPoolSize(size);
+ hostnameVerification(policy);
+ if (adapterConfig.isDisableTrustManager()) {
+ disableTrustManager();
+ } else {
+ trustStore(truststore);
+ }
+
+ configureProxyForAuthServerIfProvided(adapterConfig);
+
+ return build();
+ }
+
+ /**
+ * Configures a the proxy to use for auth-server requests if provided.
+ * <p>
+ * If the given {@link AdapterHttpClientConfig} contains the attribute {@code proxy-url} we use the
+ * given URL as a proxy server, otherwise the proxy configuration is ignored.
+ * </p>
+ *
+ * @param adapterConfig
+ */
+ private void configureProxyForAuthServerIfProvided(AdapterHttpClientConfig adapterConfig) {
+
+ if (adapterConfig == null || adapterConfig.getProxyUrl() == null || adapterConfig.getProxyUrl().trim().isEmpty()) {
+ return;
+ }
+
+ URI uri = URI.create(adapterConfig.getProxyUrl());
+ this.proxyHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
+ }
+}
\ No newline at end of file
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/SniSSLSocketFactory.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/SniSSLSocketFactory.java
new file mode 100644
index 0000000..8c064b0
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/cloned/SniSSLSocketFactory.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.cloned;
+
+import org.apache.http.HttpHost;
+import org.apache.http.conn.scheme.HostNameResolver;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.protocol.HttpContext;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.security.AccessController;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * SSLSocketFactory that uses Server Name Indication (SNI) TLS extension.
+ *
+ * <p>
+ * Originally copied from <b>keycloak-adapter-core</b> project.
+ *
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ * @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
+ */
+public class SniSSLSocketFactory extends SSLSocketFactory {
+
+ private static final Logger LOG = Logger.getLogger(SniSSLSocketFactory.class.getName());
+
+ public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, HostNameResolver nameResolver) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(algorithm, keystore, keyPassword, truststore, random, nameResolver);
+ }
+
+ public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(algorithm, keystore, keyPassword, truststore, random, trustStrategy, hostnameVerifier);
+ }
+
+ public SniSSLSocketFactory(String algorithm, KeyStore keystore, String keyPassword, KeyStore truststore, SecureRandom random, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(algorithm, keystore, keyPassword, truststore, random, hostnameVerifier);
+ }
+
+ public SniSSLSocketFactory(KeyStore keystore, String keystorePassword, KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(keystore, keystorePassword, truststore);
+ }
+
+ public SniSSLSocketFactory(KeyStore keystore, String keystorePassword) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(keystore, keystorePassword);
+ }
+
+ public SniSSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(truststore);
+ }
+
+ public SniSSLSocketFactory(TrustStrategy trustStrategy, X509HostnameVerifier hostnameVerifier) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(trustStrategy, hostnameVerifier);
+ }
+
+ public SniSSLSocketFactory(TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
+ super(trustStrategy);
+ }
+
+ public SniSSLSocketFactory(SSLContext sslContext) {
+ super(sslContext);
+ }
+
+ public SniSSLSocketFactory(SSLContext sslContext, HostNameResolver nameResolver) {
+ super(sslContext, nameResolver);
+ }
+
+ public SniSSLSocketFactory(SSLContext sslContext, X509HostnameVerifier hostnameVerifier) {
+ super(sslContext, hostnameVerifier);
+ }
+
+ public SniSSLSocketFactory(SSLContext sslContext, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) {
+ super(sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ }
+
+ public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) {
+ super(socketfactory, hostnameVerifier);
+ }
+
+ public SniSSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory, String[] supportedProtocols, String[] supportedCipherSuites, X509HostnameVerifier hostnameVerifier) {
+ super(socketfactory, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+ }
+
+ @Override
+ public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException {
+ return super.connectSocket(connectTimeout, applySNI(socket, host.getHostName()), host, remoteAddress, localAddress, context);
+ }
+
+ @Override
+ public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) throws IOException {
+ return super.createLayeredSocket(applySNI(socket, target), target, port, context);
+ }
+
+ private Socket applySNI(final Socket socket, String hostname) {
+ if (socket instanceof SSLSocket) {
+ try {
+ Method setHostMethod = AccessController.doPrivileged(new PrivilegedExceptionAction<Method>() {
+ @Override
+ public Method run() throws NoSuchMethodException {
+ return socket.getClass().getMethod("setHost", String.class);
+ }
+ });
+
+ setHostMethod.invoke(socket, hostname);
+ LOG.log(Level.FINEST, "Applied SNI to socket for host {0}", hostname);
+ } catch (PrivilegedActionException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ LOG.log(Level.WARNING, "Failed to apply SNI to SSLSocket", e);
+ }
+ }
+ return socket;
+ }
+}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java
index 305ffeb..6ddf52c 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/AbstractInitiateLogin.java
@@ -79,7 +79,9 @@ public abstract class AbstractInitiateLogin implements AuthChallenge {
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
}
- binding.signWith(keypair);
+ binding.signWith(null, keypair);
+ // TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
+ // <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
binding.signDocument();
}
return binding;
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/IDP.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/IDP.java
index 3960b46..de95d87 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/IDP.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/IDP.java
@@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config;
import java.io.Serializable;
import java.util.List;
+import org.keycloak.adapters.cloned.AdapterHttpClientConfig;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -157,12 +158,97 @@ public class IDP implements Serializable {
}
}
+ public static class HttpClientConfig implements AdapterHttpClientConfig {
+
+ private String truststore;
+ private String truststorePassword;
+ private String clientKeystore;
+ private String clientKeystorePassword;
+ private boolean allowAnyHostname;
+ private boolean disableTrustManager;
+ private int connectionPoolSize;
+ private String proxyUrl;
+
+ @Override
+ public String getTruststore() {
+ return truststore;
+ }
+
+ public void setTruststore(String truststore) {
+ this.truststore = truststore;
+ }
+
+ @Override
+ public String getTruststorePassword() {
+ return truststorePassword;
+ }
+
+ public void setTruststorePassword(String truststorePassword) {
+ this.truststorePassword = truststorePassword;
+ }
+
+ @Override
+ public String getClientKeystore() {
+ return clientKeystore;
+ }
+
+ public void setClientKeystore(String clientKeystore) {
+ this.clientKeystore = clientKeystore;
+ }
+
+ @Override
+ public String getClientKeystorePassword() {
+ return clientKeystorePassword;
+ }
+
+ public void setClientKeystorePassword(String clientKeystorePassword) {
+ this.clientKeystorePassword = clientKeystorePassword;
+ }
+
+ @Override
+ public boolean isAllowAnyHostname() {
+ return allowAnyHostname;
+ }
+
+ public void setAllowAnyHostname(boolean allowAnyHostname) {
+ this.allowAnyHostname = allowAnyHostname;
+ }
+
+ @Override
+ public boolean isDisableTrustManager() {
+ return disableTrustManager;
+ }
+
+ public void setDisableTrustManager(boolean disableTrustManager) {
+ this.disableTrustManager = disableTrustManager;
+ }
+
+ @Override
+ public int getConnectionPoolSize() {
+ return connectionPoolSize;
+ }
+
+ public void setConnectionPoolSize(int connectionPoolSize) {
+ this.connectionPoolSize = connectionPoolSize;
+ }
+
+ @Override
+ public String getProxyUrl() {
+ return proxyUrl;
+ }
+
+ public void setProxyUrl(String proxyUrl) {
+ this.proxyUrl = proxyUrl;
+ }
+ }
+
private String entityID;
private String signatureAlgorithm;
private String signatureCanonicalizationMethod;
private SingleSignOnService singleSignOnService;
private SingleLogoutService singleLogoutService;
private List<Key> keys;
+ private AdapterHttpClientConfig httpClientConfig = new HttpClientConfig();
public String getEntityID() {
return entityID;
@@ -212,4 +298,12 @@ public class IDP implements Serializable {
this.signatureCanonicalizationMethod = signatureCanonicalizationMethod;
}
+ public AdapterHttpClientConfig getHttpClientConfig() {
+ return httpClientConfig;
+ }
+
+ public void setHttpClientConfig(AdapterHttpClientConfig httpClientConfig) {
+ this.httpClientConfig = httpClientConfig;
+ }
+
}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
index 0085a6a..1a3dd04 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
@@ -72,4 +72,15 @@ public class ConfigXmlConstants {
public static final String VALIDATE_REQUEST_SIGNATURE_ATTR = "validateRequestSignature";
public static final String POST_BINDING_URL_ATTR = "postBindingUrl";
public static final String REDIRECT_BINDING_URL_ATTR = "redirectBindingUrl";
+
+ public static final String HTTP_CLIENT_ELEMENT = "HttpClient";
+ public static final String ALLOW_ANY_HOSTNAME_ATTR = "allowAnyHostname";
+ public static final String CLIENT_KEYSTORE_ATTR = "clientKeystore";
+ public static final String CLIENT_KEYSTORE_PASSWORD_ATTR = "clientKeystorePassword";
+ public static final String CONNECTION_POOL_SIZE_ATTR = "connectionPoolSize";
+ public static final String DISABLE_TRUST_MANAGER_ATTR = "disableTrustManager";
+ public static final String PROXY_URL_ATTR = "proxyUrl";
+ public static final String TRUSTSTORE_ATTR = "truststore";
+ public static final String TRUSTSTORE_PASSWORD_ATTR = "truststorePassword";
+
}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
index d6e4bce..7af71ba 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
@@ -40,6 +40,7 @@ import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.HashSet;
import java.util.Set;
+import org.keycloak.adapters.cloned.HttpClientBuilder;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -178,35 +179,39 @@ public class DeploymentBuilder {
if (sp.getIdp().getKeys() != null) {
for (Key key : sp.getIdp().getKeys()) {
if (key.isSigning()) {
- if (key.getKeystore() != null) {
- KeyStore keyStore = loadKeystore(resourceLoader, key);
- Certificate cert = null;
- try {
- cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
- } catch (KeyStoreException e) {
- throw new RuntimeException(e);
- }
- idp.setSignatureValidationKey(cert.getPublicKey());
- } else {
- if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
- throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
- }
- try {
- PublicKey publicKey = getPublicKeyFromPem(key);
- idp.setSignatureValidationKey(publicKey);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
+ processSigningKey(idp, key, resourceLoader);
}
}
}
+ idp.setClient(new HttpClientBuilder().build(sp.getIdp().getHttpClientConfig()));
+ idp.refreshKeyLocatorConfiguration();
return deployment;
}
- protected static PublicKey getPublicKeyFromPem(Key key) throws Exception {
+ private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException {
+ PublicKey publicKey;
+ if (key.getKeystore() != null) {
+ KeyStore keyStore = loadKeystore(resourceLoader, key);
+ Certificate cert = null;
+ try {
+ cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
+ } catch (KeyStoreException e) {
+ throw new RuntimeException(e);
+ }
+ publicKey = cert.getPublicKey();
+ } else {
+ if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
+ throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
+ }
+ publicKey = getPublicKeyFromPem(key);
+ }
+
+ idp.addSignatureValidationKey(publicKey);
+ }
+
+ protected static PublicKey getPublicKeyFromPem(Key key) {
PublicKey publicKey;
if (key.getPublicKeyPem() != null) {
publicKey = PemUtils.decodePublicKey(key.getPublicKeyPem().trim());
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/IDPXmlParser.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/IDPXmlParser.java
index e649d1c..be54223 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/IDPXmlParser.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/IDPXmlParser.java
@@ -29,6 +29,10 @@ import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.util.List;
+import org.keycloak.adapters.saml.config.IDP.HttpClientConfig;
+import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getAttributeValue;
+import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getBooleanAttributeValue;
+import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getIntegerAttributeValue;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -41,16 +45,16 @@ public class IDPXmlParser extends AbstractParser {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
StaxParserUtil.validate(startElement, ConfigXmlConstants.IDP_ELEMENT);
IDP idp = new IDP();
- String entityID = SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
+ String entityID = getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
if (entityID == null) {
throw new ParsingException("entityID must be set on IDP");
}
idp.setEntityID(entityID);
- boolean signaturesRequired = SPXmlParser.getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
- idp.setSignatureCanonicalizationMethod(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
- idp.setSignatureAlgorithm(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
+ boolean signaturesRequired = getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
+ idp.setSignatureCanonicalizationMethod(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
+ idp.setSignatureAlgorithm(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
while (xmlEventReader.hasNext()) {
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
if (xmlEvent == null)
@@ -75,6 +79,10 @@ public class IDPXmlParser extends AbstractParser {
IDP.SingleLogoutService slo = parseSingleLogoutService(xmlEventReader, signaturesRequired);
idp.setSingleLogoutService(slo);
+ } else if (tag.equals(ConfigXmlConstants.HTTP_CLIENT_ELEMENT)) {
+ HttpClientConfig config = parseHttpClientElement(xmlEventReader);
+ idp.setHttpClientConfig(config);
+
} else if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
KeysXmlParser parser = new KeysXmlParser();
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
@@ -90,29 +98,63 @@ public class IDPXmlParser extends AbstractParser {
protected IDP.SingleLogoutService parseSingleLogoutService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
IDP.SingleLogoutService slo = new IDP.SingleLogoutService();
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
- slo.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
- slo.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
- slo.setValidateRequestSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
- slo.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
- slo.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
- slo.setSignResponse(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
- slo.setPostBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
- slo.setRedirectBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
+ slo.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
+ slo.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
+ slo.setValidateRequestSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
+ slo.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
+ slo.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
+ slo.setSignResponse(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
+ slo.setPostBindingUrl(getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
+ slo.setRedirectBindingUrl(getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
return slo;
}
protected IDP.SingleSignOnService parseSingleSignOnService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
IDP.SingleSignOnService sso = new IDP.SingleSignOnService();
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
- sso.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
- sso.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
- sso.setValidateAssertionSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
- sso.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
- sso.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
- sso.setBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
+ sso.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
+ sso.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
+ sso.setValidateAssertionSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
+ sso.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
+ sso.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
+ sso.setBindingUrl(getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
return sso;
}
+ private HttpClientConfig parseHttpClientElement(XMLEventReader xmlEventReader) throws ParsingException {
+ StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
+ StaxParserUtil.validate(startElement, ConfigXmlConstants.HTTP_CLIENT_ELEMENT);
+ HttpClientConfig config = new HttpClientConfig();
+
+ config.setAllowAnyHostname(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
+ config.setClientKeystore(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_ATTR));
+ config.setClientKeystorePassword(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_PASSWORD_ATTR));
+ config.setConnectionPoolSize(getIntegerAttributeValue(startElement, ConfigXmlConstants.CONNECTION_POOL_SIZE_ATTR, 0));
+ config.setDisableTrustManager(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
+ config.setProxyUrl(getAttributeValue(startElement, ConfigXmlConstants.PROXY_URL_ATTR));
+ config.setTruststore(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_ATTR));
+ config.setTruststorePassword(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_PASSWORD_ATTR));
+
+ while (xmlEventReader.hasNext()) {
+ XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
+ if (xmlEvent == null)
+ break;
+ if (xmlEvent instanceof EndElement) {
+ EndElement endElement = (EndElement) StaxParserUtil.getNextEvent(xmlEventReader);
+ String endElementName = StaxParserUtil.getEndElementName(endElement);
+ if (endElementName.equals(ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT))
+ break;
+ else
+ continue;
+ }
+
+ String tag = StaxParserUtil.getStartElementName(startElement);
+ StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
+ }
+
+ return config;
+ }
+
@Override
public boolean supports(QName qname) {
return false;
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
index 3eeb1f7..be6d682 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
@@ -48,6 +48,13 @@ public class SPXmlParser extends AbstractParser {
return str;
}
+ public static int getIntegerAttributeValue(StartElement startElement, String tag, int defaultValue) {
+ String result = getAttributeValue(startElement, tag);
+ if (result == null)
+ return defaultValue;
+ return Integer.valueOf(result);
+ }
+
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
String result = getAttributeValue(startElement, tag);
if (result == null)
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
index ee753ad..a52cdc2 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
@@ -23,7 +23,14 @@ import org.keycloak.saml.SignatureAlgorithm;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Set;
+import org.apache.http.client.HttpClient;
+import org.keycloak.adapters.saml.rotation.SamlDescriptorPublicKeyLocator;
+import org.keycloak.rotation.CompositeKeyLocator;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -179,10 +186,15 @@ public class DefaultSamlDeployment implements SamlDeployment {
public static class DefaultIDP implements IDP {
+ private static final int DEFAULT_CACHE_TTL = 24 * 60 * 60;
+
private String entityID;
- private PublicKey signatureValidationKey;
+ private final CompositeKeyLocator signatureValidationKeyLocator = new CompositeKeyLocator();
private SingleSignOnService singleSignOnService;
private SingleLogoutService singleLogoutService;
+ private final List<PublicKey> signatureValidationKeys = new LinkedList<>();
+ private int minTimeBetweenDescriptorRequests;
+ private HttpClient client;
@Override
public String getEntityID() {
@@ -200,16 +212,25 @@ public class DefaultSamlDeployment implements SamlDeployment {
}
@Override
- public PublicKey getSignatureValidationKey() {
- return signatureValidationKey;
+ public KeyLocator getSignatureValidationKeyLocator() {
+ return this.signatureValidationKeyLocator;
+ }
+
+ @Override
+ public int getMinTimeBetweenDescriptorRequests() {
+ return minTimeBetweenDescriptorRequests;
+ }
+
+ public void setMinTimeBetweenDescriptorRequests(int minTimeBetweenDescriptorRequests) {
+ this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests;
}
public void setEntityID(String entityID) {
this.entityID = entityID;
}
- public void setSignatureValidationKey(PublicKey signatureValidationKey) {
- this.signatureValidationKey = signatureValidationKey;
+ public void addSignatureValidationKey(PublicKey signatureValidationKey) {
+ this.signatureValidationKeys.add(signatureValidationKey);
}
public void setSingleSignOnService(SingleSignOnService singleSignOnService) {
@@ -219,6 +240,31 @@ public class DefaultSamlDeployment implements SamlDeployment {
public void setSingleLogoutService(SingleLogoutService singleLogoutService) {
this.singleLogoutService = singleLogoutService;
}
+
+ public void refreshKeyLocatorConfiguration() {
+ this.signatureValidationKeyLocator.clear();
+
+ // When key is set, use that (and only that), otherwise configure dynamic key locator
+ if (! this.signatureValidationKeys.isEmpty()) {
+ this.signatureValidationKeyLocator.add(new HardcodedKeyLocator(this.signatureValidationKeys));
+ } else if (this.singleSignOnService != null) {
+ String samlDescriptorUrl = singleSignOnService.getRequestBindingUrl() + "/descriptor";
+ HttpClient httpClient = getClient();
+ SamlDescriptorPublicKeyLocator samlDescriptorPublicKeyLocator =
+ new SamlDescriptorPublicKeyLocator(
+ samlDescriptorUrl, this.minTimeBetweenDescriptorRequests, DEFAULT_CACHE_TTL, httpClient);
+ this.signatureValidationKeyLocator.add(samlDescriptorPublicKeyLocator);
+ }
+ }
+
+ @Override
+ public HttpClient getClient() {
+ return this.client;
+ }
+
+ public void setClient(HttpClient client) {
+ this.client = client;
+ }
}
private IDP idp;
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
new file mode 100644
index 0000000..0858675
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/descriptor/parsers/SamlDescriptorIDPKeysExtractor.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.saml.descriptor.parsers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import javax.xml.crypto.MarshalException;
+import javax.xml.crypto.dom.DOMStructure;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.saml.common.constants.JBossSAMLConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.processing.core.util.NamespaceContext;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * Goes through the given XML file and extracts names, certificates and keys from the KeyInfo elements.
+ * @author hmlnarik
+ */
+public class SamlDescriptorIDPKeysExtractor {
+
+ private static final NamespaceContext NS_CONTEXT = new NamespaceContext();
+ static {
+ NS_CONTEXT.addNsUriPair("m", JBossSAMLURIConstants.METADATA_NSURI.get());
+ NS_CONTEXT.addNsUriPair("dsig", JBossSAMLURIConstants.XMLDSIG_NSURI.get());
+ }
+
+ private final KeyInfoFactory kif = KeyInfoFactory.getInstance();
+
+ private final XPathFactory xPathfactory = XPathFactory.newInstance();
+ private final XPath xpath = xPathfactory.newXPath();
+ {
+ xpath.setNamespaceContext(NS_CONTEXT);
+ }
+
+ public MultivaluedHashMap<String, KeyInfo> parse(InputStream stream) throws ParsingException {
+ MultivaluedHashMap<String, KeyInfo> res = new MultivaluedHashMap<>();
+
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document doc = builder.parse(stream);
+
+ XPathExpression expr = xpath.compile("/m:EntitiesDescriptor/m:EntityDescriptor/m:IDPSSODescriptor/m:KeyDescriptor");
+ NodeList keyDescriptors = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
+ for (int i = 0; i < keyDescriptors.getLength(); i ++) {
+ Node keyDescriptor = keyDescriptors.item(i);
+ Element keyDescriptorEl = (Element) keyDescriptor;
+ KeyInfo ki = processKeyDescriptor(keyDescriptorEl);
+ if (ki != null) {
+ String use = keyDescriptorEl.getAttribute(JBossSAMLConstants.USE.get());
+ res.add(use, ki);
+ }
+ }
+ } catch (SAXException | IOException | ParserConfigurationException | MarshalException | XPathExpressionException e) {
+ throw new ParsingException("Error parsing SAML descriptor", e);
+ }
+
+ return res;
+ }
+
+ private KeyInfo processKeyDescriptor(Element keyDescriptor) throws MarshalException {
+ NodeList childNodes = keyDescriptor.getElementsByTagNameNS(JBossSAMLURIConstants.XMLDSIG_NSURI.get(), JBossSAMLConstants.KEY_INFO.get());
+
+ if (childNodes.getLength() == 0) {
+ return null;
+ }
+ Node keyInfoNode = childNodes.item(0);
+ return (keyInfoNode == null) ? null : kif.unmarshalKeyInfo(new DOMStructure(keyInfoNode));
+ }
+
+}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
index e9247b3..429d610 100644
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java
@@ -64,11 +64,20 @@ import org.w3c.dom.Node;
import java.io.IOException;
import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyManagementException;
import java.security.PublicKey;
import java.security.Signature;
+import java.security.SignatureException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.w3c.dom.Element;
/**
*
@@ -257,13 +266,44 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
private void validateSamlSignature(SAMLDocumentHolder holder, boolean postBinding, String paramKey) throws VerificationException {
+ KeyLocator signatureValidationKey = deployment.getIDP().getSignatureValidationKeyLocator();
if (postBinding) {
- verifyPostBindingSignature(holder.getSamlDocument(), deployment.getIDP().getSignatureValidationKey());
+ verifyPostBindingSignature(holder.getSamlDocument(), signatureValidationKey);
} else {
- verifyRedirectBindingSignature(deployment.getIDP().getSignatureValidationKey(), paramKey);
+ String keyId = getMessageSigningKeyId(holder.getSamlObject());
+ verifyRedirectBindingSignature(paramKey, signatureValidationKey, keyId);
}
}
+ private String getMessageSigningKeyId(SAML2Object doc) {
+ final ExtensionsType extensions;
+ if (doc instanceof RequestAbstractType) {
+ extensions = ((RequestAbstractType) doc).getExtensions();
+ } else if (doc instanceof StatusResponseType) {
+ extensions = ((StatusResponseType) doc).getExtensions();
+ } else {
+ return null;
+ }
+
+ if (extensions == null) {
+ return null;
+ }
+
+ for (Object ext : extensions.getAny()) {
+ if (! (ext instanceof Element)) {
+ continue;
+ }
+
+ String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
+
+ if (res != null) {
+ return res;
+ }
+ }
+
+ return null;
+ }
+
private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
if(statusCode != null && statusCode.getValue()!=null){
String v = statusCode.getValue().toString();
@@ -473,10 +513,10 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
return false;
}
- public void verifyPostBindingSignature(Document document, PublicKey publicKey) throws VerificationException {
+ public void verifyPostBindingSignature(Document document, KeyLocator keyLocator) throws VerificationException {
SAML2Signature saml2Signature = new SAML2Signature();
try {
- if (!saml2Signature.validate(document, publicKey)) {
+ if (!saml2Signature.validate(document, keyLocator)) {
throw new VerificationException("Invalid signature on document");
}
} catch (ProcessingException e) {
@@ -484,7 +524,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
}
}
- public void verifyRedirectBindingSignature(PublicKey publicKey, String paramKey) throws VerificationException {
+ private void verifyRedirectBindingSignature(String paramKey, KeyLocator keyLocator, String keyId) throws VerificationException {
String request = facade.getRequest().getQueryParamValue(paramKey);
String algorithm = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
String signature = facade.getRequest().getQueryParamValue(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
@@ -511,16 +551,80 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
try {
//byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
byte[] decodedSignature = Base64.decode(signature);
+ byte[] rawQueryBytes = rawQuery.getBytes("UTF-8");
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
- Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
- validator.initVerify(publicKey);
- validator.update(rawQuery.getBytes("UTF-8"));
- if (!validator.verify(decodedSignature)) {
+
+ if (! validateRedirectBindingSignature(signatureAlgorithm, rawQueryBytes, decodedSignature, keyLocator, keyId)) {
throw new VerificationException("Invalid query param signature");
}
} catch (Exception e) {
throw new VerificationException(e);
}
}
+
+ private boolean validateRedirectBindingSignature(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, KeyLocator locator, String keyId)
+ throws KeyManagementException, VerificationException {
+ try {
+ Key key;
+ try {
+ key = locator.getKey(keyId);
+ boolean keyLocated = key != null;
+
+ if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
+ return true;
+ }
+
+ if (keyLocated) {
+ return false;
+ }
+ } catch (KeyManagementException ex) {
+ }
+ } catch (SignatureException ex) {
+ log.debug("Verification failed for key %s: %s", keyId, ex);
+ log.trace(ex);
+ }
+
+ if (locator instanceof Iterable) {
+ Iterable<Key> availableKeys = (Iterable<Key>) locator;
+
+ log.trace("Trying hard to validate XML signature using all available keys.");
+
+ for (Key key : availableKeys) {
+ try {
+ if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
+ return true;
+ }
+ } catch (SignatureException ex) {
+ log.debug("Verification failed: %s", ex);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private boolean validateRedirectBindingSignatureForKey(SignatureAlgorithm sigAlg, byte[] rawQueryBytes, byte[] decodedSignature, Key key)
+ throws SignatureException {
+ if (key == null) {
+ return false;
+ }
+
+ if (! (key instanceof PublicKey)) {
+ log.warnf("Unusable key for signature validation: %s", key);
+ return false;
+ }
+
+ Signature signature = sigAlg.createSignature(); // todo plugin signature alg
+ try {
+ signature.initVerify((PublicKey) key);
+ } catch (InvalidKeyException ex) {
+ log.warnf(ex, "Unusable key for signature validation: %s", key);
+ return false;
+ }
+
+ signature.update(rawQueryBytes);
+
+ return signature.verify(decodedSignature);
+ }
}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
index 5c1454f..231c425 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/webbrowsersso/WebBrowserSsoAuthenticationHandler.java
@@ -82,8 +82,10 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
if (deployment.getSignatureCanonicalizationMethod() != null)
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
- .signWith(deployment.getSigningKeyPair())
+ .signWith(null, deployment.getSigningKeyPair())
.signDocument();
+ // TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
+ // <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
}
@@ -113,8 +115,10 @@ public class WebBrowserSsoAuthenticationHandler extends AbstractSamlAuthenticati
if (deployment.getSignatureCanonicalizationMethod() != null)
binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
binding.signatureAlgorithm(deployment.getSignatureAlgorithm());
- binding.signWith(deployment.getSigningKeyPair())
+ binding.signWith(null, deployment.getSigningKeyPair())
.signDocument();
+ // TODO: As part of KEYCLOAK-3810, add KeyID to the SAML document
+ // <related DocumentBuilder>.addExtension(new KeycloakKeySamlExtensionGenerator(<key ID>));
}
binding.relayState("logout");
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java
new file mode 100644
index 0000000..7a45fb7
--- /dev/null
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/rotation/SamlDescriptorPublicKeyLocator.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.saml.rotation;
+
+import java.security.Key;
+import java.security.KeyManagementException;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyName;
+import org.apache.http.client.HttpClient;
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.cloned.HttpAdapterUtils;
+import org.keycloak.adapters.cloned.HttpClientAdapterException;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.Time;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.api.util.KeyInfoTools;
+
+/**
+ * This class defines a {@link KeyLocator} that looks up public keys and certificates in IdP's
+ * SAML descriptor (i.e. http://{host}/auth/realms/{realm}/protocol/saml/descriptor).
+ *
+ * Based on {@code JWKPublicKeyLocator}.
+ *
+ * @author hmlnarik
+ */
+public class SamlDescriptorPublicKeyLocator implements KeyLocator, Iterable<PublicKey> {
+
+ private static final Logger LOG = Logger.getLogger(SamlDescriptorPublicKeyLocator.class);
+
+ /**
+ * Time between two subsequent requests (in seconds).
+ */
+ private final int minTimeBetweenDescriptorRequests;
+
+ /**
+ * Time to live for cache entries (in seconds).
+ */
+ private final int cacheEntryTtl;
+
+ /**
+ * Target descriptor URL.
+ */
+ private final String descriptorUrl;
+
+ private final Map<String, PublicKey> publicKeyCache = new ConcurrentHashMap<>();
+
+ private final HttpClient client;
+
+ private volatile int lastRequestTime = 0;
+
+ public SamlDescriptorPublicKeyLocator(String descriptorUrl, int minTimeBetweenDescriptorRequests, int cacheEntryTtl, HttpClient httpClient) {
+ this.minTimeBetweenDescriptorRequests = minTimeBetweenDescriptorRequests <= 0
+ ? 20
+ : minTimeBetweenDescriptorRequests;
+
+ this.descriptorUrl = descriptorUrl;
+ this.cacheEntryTtl = cacheEntryTtl;
+
+ this.client = httpClient;
+ }
+
+ @Override
+ public Key getKey(String kid) throws KeyManagementException {
+ if (kid == null) {
+ LOG.debugf("Invalid key id: %s", kid);
+ return null;
+ }
+
+ LOG.tracef("Requested key id: %s", kid);
+
+ int currentTime = Time.currentTime();
+
+ PublicKey res;
+ if (currentTime > this.lastRequestTime + this.cacheEntryTtl) {
+ LOG.debugf("Performing regular cache cleanup.");
+ res = refreshCertificateCacheAndGet(kid);
+ } else {
+ res = publicKeyCache.get(kid);
+
+ if (res == null) {
+ if (currentTime > this.lastRequestTime + this.minTimeBetweenDescriptorRequests) {
+ res = refreshCertificateCacheAndGet(kid);
+ } else {
+ LOG.debugf("Won't send request to realm SAML descriptor url, timeout not expired. Last request time was %d", lastRequestTime);
+ }
+ }
+ }
+
+ return res;
+ }
+
+ @Override
+ public synchronized void refreshKeyCache() {
+ LOG.info("Forcing key cache cleanup and refresh.");
+ this.publicKeyCache.clear();
+ refreshCertificateCacheAndGet(null);
+ }
+
+ private synchronized PublicKey refreshCertificateCacheAndGet(String kid) {
+ if (this.descriptorUrl == null) {
+ return null;
+ }
+
+ this.lastRequestTime = Time.currentTime();
+
+ LOG.debugf("Refreshing public key cache from %s", this.descriptorUrl);
+ List<KeyInfo> signingCerts;
+ try {
+ MultivaluedHashMap<String, KeyInfo> certs = HttpAdapterUtils.downloadKeysFromSamlDescriptor(client, this.descriptorUrl);
+ signingCerts = certs.get(KeyTypes.SIGNING.value());
+ } catch (HttpClientAdapterException ex) {
+ LOG.error("Could not refresh certificates from the server", ex);
+ return null;
+ }
+
+ if (signingCerts == null) {
+ return null;
+ }
+
+ LOG.debugf("Certificates retrieved from server, filling public key cache");
+
+ // Only clear cache after it is certain that the SAML descriptor has been read successfully
+ this.publicKeyCache.clear();
+
+ for (KeyInfo ki : signingCerts) {
+ KeyName keyName = KeyInfoTools.getKeyName(ki);
+ X509Certificate x509certificate = KeyInfoTools.getX509Certificate(ki);
+ if (x509certificate != null && keyName != null) {
+ LOG.tracef("Registering signing certificate %s", keyName.getName());
+ this.publicKeyCache.put(keyName.getName(), x509certificate.getPublicKey());
+ } else {
+ LOG.tracef("Ignoring certificate %s: %s", keyName, x509certificate);
+ }
+
+ }
+
+ return (kid == null ? null : this.publicKeyCache.get(kid));
+ }
+
+ @Override
+ public String toString() {
+ return "Keys retrieved from SAML descriptor at " + descriptorUrl;
+ }
+
+ @Override
+ public Iterator<PublicKey> iterator() {
+ if (this.publicKeyCache.isEmpty()) {
+ refreshCertificateCacheAndGet(null);
+ }
+
+ return this.publicKeyCache.values().iterator();
+ }
+}
diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
index 0b82ff2..4442177 100755
--- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
+++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
@@ -22,14 +22,18 @@ import org.keycloak.saml.SignatureAlgorithm;
import java.security.KeyPair;
import java.security.PrivateKey;
-import java.security.PublicKey;
import java.util.Set;
+import org.apache.http.client.HttpClient;
+import org.keycloak.rotation.KeyLocator;
/**
+ * Represents SAML deployment configuration.
+ *
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SamlDeployment {
+
enum Binding {
POST,
REDIRECT;
@@ -41,20 +45,68 @@ public interface SamlDeployment {
}
public interface IDP {
+ /**
+ * Returns entity identifier of this IdP.
+ * @return see description.
+ */
String getEntityID();
+ /**
+ * Returns Single sign on service configuration for this IdP.
+ * @return see description.
+ */
SingleSignOnService getSingleSignOnService();
+
+ /**
+ * Returns Single logout service configuration for this IdP.
+ * @return see description.
+ */
SingleLogoutService getSingleLogoutService();
- PublicKey getSignatureValidationKey();
+
+ /**
+ * Returns {@link KeyLocator} looking up public keys used for validation of IdP signatures.
+ * @return see description.
+ */
+ KeyLocator getSignatureValidationKeyLocator();
+
+ /**
+ * Returns minimum time (in seconds) between issuing requests to IdP SAML descriptor.
+ * Used e.g. by {@link KeyLocator} looking up public keys for validation of IdP signatures
+ * to prevent too frequent requests.
+ *
+ * @return see description.
+ */
+ int getMinTimeBetweenDescriptorRequests();
+
+ /**
+ * Returns {@link HttpClient} instance that will be used for http communication with this IdP.
+ * @return see description
+ */
+ HttpClient getClient();
public interface SingleSignOnService {
+ /**
+ * Returns {@code true} if the requests to IdP need to be signed by SP key.
+ * @return see dscription
+ */
boolean signRequest();
+ /**
+ * Returns {@code true} if the complete response message from IdP should
+ * be checked for valid signature.
+ * @return see dscription
+ */
boolean validateResponseSignature();
+ /**
+ * Returns {@code true} if individual assertions in response from IdP should
+ * be checked for valid signature.
+ * @return see dscription
+ */
boolean validateAssertionSignature();
Binding getRequestBinding();
Binding getResponseBinding();
String getRequestBindingUrl();
}
+
public interface SingleLogoutService {
boolean validateRequestSignature();
boolean validateResponseSignature();
@@ -67,10 +119,19 @@ public interface SamlDeployment {
}
}
+ /**
+ * Returns Identity Provider configuration for this SAML deployment.
+ * @return see description.
+ */
public IDP getIDP();
public boolean isConfigured();
SslRequired getSslRequired();
+
+ /**
+ * Returns entity identifier of this SP.
+ * @return see description.
+ */
String getEntityID();
String getNameIDPolicyFormat();
boolean isForceAuthentication();
diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd
new file mode 100644
index 0000000..174ea17
--- /dev/null
+++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_7.xsd
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<xs:schema version="1.0"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns="urn:keycloak:saml:adapter"
+ targetNamespace="urn:keycloak:saml:adapter"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified">
+
+ <xs:element name="keycloak-saml-adapter" type="adapter-type"/>
+ <xs:complexType name="adapter-type">
+ <xs:annotation>
+ <xs:documentation>
+ <![CDATA[
+ The Keycloak SAML Adapter keycloak-saml.xml config file
+ ]]>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:all>
+ <xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type"/>
+ </xs:all>
+ </xs:complexType>
+
+ <xs:complexType name="sp-type">
+ <xs:all>
+ <xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1"/>
+ </xs:all>
+ <xs:attribute name="entityID" type="xs:string" use="required"/>
+ <xs:attribute name="sslPolicy" type="xs:string" use="optional"/>
+ <xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional"/>
+ <xs:attribute name="logoutPage" type="xs:string" use="optional"/>
+ <xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
+ <xs:attribute name="isPassive" type="xs:boolean" use="optional"/>
+ <xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="keys-type">
+ <xs:sequence>
+ <xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="key-type">
+ <xs:all>
+ <xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type"/>
+ <xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1"/>
+ </xs:all>
+ <xs:attribute name="signing" type="xs:boolean" use="optional"/>
+ <xs:attribute name="encryption" type="xs:boolean" use="optional"/>
+ </xs:complexType>
+ <xs:complexType name="key-store-type">
+ <xs:all>
+ <xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type"/>
+ <xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1"/>
+ </xs:all>
+ <xs:attribute name="file" type="xs:string" use="optional"/>
+ <xs:attribute name="resource" type="xs:string" use="optional"/>
+ <xs:attribute name="password" type="xs:string" use="required"/>
+ </xs:complexType>
+ <xs:complexType name="private-key-type">
+ <xs:attribute name="alias" type="xs:string" use="required"/>
+ <xs:attribute name="password" type="xs:string" use="required"/>
+ </xs:complexType>
+ <xs:complexType name="certificate-type">
+ <xs:attribute name="alias" type="xs:string" use="required"/>
+ </xs:complexType>
+ <xs:complexType name="principal-name-mapping-type">
+ <xs:attribute name="policy" type="xs:string" use="required"/>
+ <xs:attribute name="attribute" type="xs:string" use="optional"/>
+ </xs:complexType>
+ <xs:complexType name="role-identifiers-type">
+ <xs:choice minOccurs="0" maxOccurs="unbounded">
+ <xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type"/>
+ </xs:choice>
+ </xs:complexType>
+ <xs:complexType name="attribute-type">
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ </xs:complexType>
+ <xs:complexType name="idp-type">
+ <xs:sequence minOccurs="0" maxOccurs="unbounded">
+ <xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type"/>
+ <xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1"/>
+ <xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1"/>
+ </xs:sequence>
+ <xs:attribute name="entityID" type="xs:string" use="required"/>
+ <xs:attribute name="signaturesRequired" type="xs:boolean" use="required"/>
+ <xs:attribute name="signatureAlgorithm" type="xs:string" use="optional"/>
+ <xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional"/>
+ <xs:attribute name="encryption" type="xs:boolean" use="optional"/>
+ </xs:complexType>
+ <xs:complexType name="sign-on-type">
+ <xs:attribute name="signRequest" type="xs:boolean" use="optional"/>
+ <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional"/>
+ <xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional"/>
+ <xs:attribute name="requestBinding" type="xs:string" use="optional"/>
+ <xs:attribute name="responseBinding" type="xs:string" use="optional"/>
+ <xs:attribute name="bindingUrl" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="logout-type">
+ <xs:attribute name="signRequest" type="xs:boolean" use="optional"/>
+ <xs:attribute name="signResponse" type="xs:boolean" use="optional"/>
+ <xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional"/>
+ <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional"/>
+ <xs:attribute name="requestBinding" type="xs:string" use="optional"/>
+ <xs:attribute name="responseBinding" type="xs:string" use="optional"/>
+ <xs:attribute name="postBindingUrl" type="xs:string" use="optional"/>
+ <xs:attribute name="redirectBindingUrl" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="http-client-type">
+ <xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional"/>
+ <xs:attribute name="clientKeystore" type="xs:string" use="optional"/>
+ <xs:attribute name="clientKeystorePassword" type="xs:string" use="optional"/>
+ <xs:attribute name="connectionPoolSize" type="xs:int" use="optional"/>
+ <xs:attribute name="disableTrustManager" type="xs:boolean" use="optional"/>
+ <xs:attribute name="proxyUrl" type="xs:string" use="optional"/>
+ <xs:attribute name="truststore" type="xs:string" use="optional"/>
+ <xs:attribute name="truststorePassword" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+</xs:schema>
diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/cloned/HttpAdapterUtilsTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/cloned/HttpAdapterUtilsTest.java
new file mode 100644
index 0000000..2c03ef8
--- /dev/null
+++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/cloned/HttpAdapterUtilsTest.java
@@ -0,0 +1,82 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.keycloak.adapters.cloned;
+
+import java.io.InputStream;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyName;
+import javax.xml.crypto.dsig.keyinfo.X509Data;
+import static org.hamcrest.CoreMatchers.*;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.keycloak.adapters.saml.config.parsers.ConfigXmlConstants;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.saml.common.exceptions.ParsingException;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class HttpAdapterUtilsTest {
+
+ private <T> T getContent(List<Object> objects, Class<T> clazz) {
+ for (Object o : objects) {
+ if (clazz.isInstance(o)) {
+ return (T) o;
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void testExtractKeysFromSamlDescriptor() throws ParsingException {
+ InputStream xmlStream = HttpAdapterUtilsTest.class.getResourceAsStream("saml-descriptor-valid.xml");
+ MultivaluedHashMap<String, KeyInfo> res = HttpAdapterUtils.extractKeysFromSamlDescriptor(xmlStream);
+
+ assertThat(res, notNullValue());
+ assertThat(res.keySet(), hasItems(KeyTypes.SIGNING.value()));
+ assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR), notNullValue());
+ assertThat(res.get(ConfigXmlConstants.SIGNING_ATTR).size(), equalTo(2));
+
+ KeyInfo ki;
+ KeyName keyName;
+ X509Data x509data;
+ X509Certificate x509certificate;
+
+ ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(0);
+ assertThat(ki.getContent().size(), equalTo(2));
+ assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(X509Data.class)));
+ assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(KeyName.class)));
+
+ keyName = getContent(ki.getContent(), KeyName.class);
+ assertThat(keyName.getName(), equalTo("rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI"));
+
+ x509data = getContent(ki.getContent(), X509Data.class);
+ assertThat(x509data, notNullValue());
+ x509certificate = getContent(x509data.getContent(), X509Certificate.class);
+ assertThat(x509certificate, notNullValue());
+ assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA"));
+
+ ki = res.get(ConfigXmlConstants.SIGNING_ATTR).get(1);
+ assertThat(ki.getContent().size(), equalTo(2));
+ assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(X509Data.class)));
+ assertThat((List<Object>) ki.getContent(), hasItem(instanceOf(KeyName.class)));
+
+ keyName = getContent(ki.getContent(), KeyName.class);
+ assertThat(keyName.getName(), equalTo("BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4"));
+
+ x509data = getContent(ki.getContent(), X509Data.class);
+ assertThat(x509data, notNullValue());
+ x509certificate = getContent(x509data.getContent(), X509Certificate.class);
+ assertThat(x509certificate, notNullValue());
+ assertThat(x509certificate.getSigAlgName(), equalTo("SHA256withRSA"));
+
+ }
+
+}
diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java
new file mode 100755
index 0000000..10537b3
--- /dev/null
+++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.adapters.saml.config.parsers;
+
+import static org.junit.Assert.*;
+import static org.hamcrest.CoreMatchers.*;
+import org.junit.Test;
+import org.keycloak.adapters.saml.config.IDP;
+import org.keycloak.adapters.saml.config.Key;
+import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
+import org.keycloak.adapters.saml.config.SP;
+import org.keycloak.saml.common.util.StaxParserUtil;
+
+import java.io.InputStream;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.keycloak.saml.common.exceptions.ParsingException;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class KeycloakSamlAdapterXMLParserTest {
+
+ private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_7.xsd";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private void testValidationValid(String fileName) throws Exception {
+ InputStream schema = getClass().getResourceAsStream(CURRENT_XSD_LOCATION);
+ InputStream is = getClass().getResourceAsStream(fileName);
+ assertNotNull(is);
+ assertNotNull(schema);
+ StaxParserUtil.validate(is, schema);
+ }
+
+ @Test
+ public void testValidationSimpleFile() throws Exception {
+ testValidationValid("keycloak-saml.xml");
+ }
+
+ @Test
+ public void testValidationMultipleKeys() throws Exception {
+ testValidationValid("keycloak-saml-multiple-signing-keys.xml");
+ }
+
+ @Test
+ public void testValidationWithHttpClient() throws Exception {
+ testValidationValid("keycloak-saml-wth-http-client-settings.xml");
+ }
+
+ @Test
+ public void testValidationKeyInvalid() throws Exception {
+ InputStream schemaIs = KeycloakSamlAdapterXMLParser.class.getResourceAsStream(CURRENT_XSD_LOCATION);
+ InputStream is = getClass().getResourceAsStream("keycloak-saml-invalid.xml");
+ assertNotNull(is);
+ assertNotNull(schemaIs);
+
+ expectedException.expect(ParsingException.class);
+ StaxParserUtil.validate(is, schemaIs);
+ }
+
+ @Test
+ public void testXmlParser() throws Exception {
+ InputStream is = getClass().getResourceAsStream("keycloak-saml.xml");
+ assertNotNull(is);
+ KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
+
+ KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
+ assertNotNull(config);
+ assertEquals(1, config.getSps().size());
+ SP sp = config.getSps().get(0);
+ assertEquals("sp", sp.getEntityID());
+ assertEquals("ssl", sp.getSslPolicy());
+ assertEquals("format", sp.getNameIDPolicyFormat());
+ assertTrue(sp.isForceAuthentication());
+ assertTrue(sp.isIsPassive());
+ assertEquals(2, sp.getKeys().size());
+ Key signing = sp.getKeys().get(0);
+ assertTrue(signing.isSigning());
+ Key.KeyStoreConfig keystore = signing.getKeystore();
+ assertNotNull(keystore);
+ assertEquals("file", keystore.getFile());
+ assertEquals("cp", keystore.getResource());
+ assertEquals("pw", keystore.getPassword());
+ assertEquals("private alias", keystore.getPrivateKeyAlias());
+ assertEquals("private pw", keystore.getPrivateKeyPassword());
+ assertEquals("cert alias", keystore.getCertificateAlias());
+ Key encryption = sp.getKeys().get(1);
+ assertTrue(encryption.isEncryption());
+ assertEquals("private pem", encryption.getPrivateKeyPem());
+ assertEquals("public pem", encryption.getPublicKeyPem());
+ assertEquals("policy", sp.getPrincipalNameMapping().getPolicy());
+ assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
+ assertTrue(sp.getRoleAttributes().size() == 1);
+ assertTrue(sp.getRoleAttributes().contains("member"));
+
+ IDP idp = sp.getIdp();
+ assertEquals("idp", idp.getEntityID());
+ assertEquals("RSA", idp.getSignatureAlgorithm());
+ assertEquals("canon", idp.getSignatureCanonicalizationMethod());
+ assertTrue(idp.getSingleSignOnService().isSignRequest());
+ assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
+ assertEquals("post", idp.getSingleSignOnService().getRequestBinding());
+ assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
+
+ assertTrue(idp.getSingleLogoutService().isSignRequest());
+ assertTrue(idp.getSingleLogoutService().isSignResponse());
+ assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
+ assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
+ assertEquals("redirect", idp.getSingleLogoutService().getRequestBinding());
+ assertEquals("post", idp.getSingleLogoutService().getResponseBinding());
+ assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
+ assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
+
+ assertTrue(idp.getKeys().size() == 1);
+ assertTrue(idp.getKeys().get(0).isSigning());
+ assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
+ }
+
+
+ @Test
+ public void testXmlParserMultipleSigningKeys() throws Exception {
+ InputStream is = getClass().getResourceAsStream("keycloak-saml-multiple-signing-keys.xml");
+ assertNotNull(is);
+ KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
+
+ KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
+ assertNotNull(config);
+ assertEquals(1, config.getSps().size());
+ SP sp = config.getSps().get(0);
+ IDP idp = sp.getIdp();
+
+ assertTrue(idp.getKeys().size() == 4);
+ for (int i = 0; i < 4; i ++) {
+ Key key = idp.getKeys().get(i);
+ assertTrue(key.isSigning());
+ assertEquals("cert pem " + i, idp.getKeys().get(i).getCertificatePem());
+ }
+ }
+
+ @Test
+ public void testXmlParserHttpClientSettings() throws Exception {
+ InputStream is = getClass().getResourceAsStream("keycloak-saml-wth-http-client-settings.xml");
+ assertNotNull(is);
+ KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
+
+ KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
+ assertNotNull(config);
+ assertEquals(1, config.getSps().size());
+ SP sp = config.getSps().get(0);
+ IDP idp = sp.getIdp();
+
+ assertThat(idp.getHttpClientConfig(), notNullValue());
+ assertThat(idp.getHttpClientConfig().getClientKeystore(), is("ks"));
+ assertThat(idp.getHttpClientConfig().getClientKeystorePassword(), is("ks-pwd"));
+ assertThat(idp.getHttpClientConfig().getProxyUrl(), is("pu"));
+ assertThat(idp.getHttpClientConfig().getTruststore(), is("ts"));
+ assertThat(idp.getHttpClientConfig().getTruststorePassword(), is("tsp"));
+ assertThat(idp.getHttpClientConfig().getConnectionPoolSize(), is(42));
+ assertThat(idp.getHttpClientConfig().isAllowAnyHostname(), is(true));
+ assertThat(idp.getHttpClientConfig().isDisableTrustManager(), is(true));
+ }
+}
diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml
new file mode 100644
index 0000000..8dd3e41
--- /dev/null
+++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/cloned/saml-descriptor-valid.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+
+-->
+<EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak">
+ <EntityDescriptor entityID="http://localhost:8081/auth/realms/master">
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <KeyDescriptor use="signing">
+ <dsig:KeyInfo>
+ <dsig:KeyName>rJkJlvowmv1Id74GznieaAC5jU5QQp_ILzuG-GsweTI</dsig:KeyName>
+ <dsig:X509Data>
+ <dsig:X509Certificate>
+ MIICmzCCAYMCBgFX/9ccIDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMjUwWhcNMjYxMDI2MDgxNDMwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPjDrM890OoFWLIU5xNT+v8B8EkpOGY1y/9Yi/yQd95uG/p5LaywiPsw+lPy4tSn1pH/2SxNDST2zynKPDd1lYDev43m0sC2FfD2H73q3udQRqSOxW1e8FrTrGDIHxb82UNrCPlu+fH+xYSkigrkOvLvPigTwSIcu8vgs0lk9FqJ81ty3Wj2e9lS7JJGAJ3pC7rp39VLdJSKbfyj/v2RYBeG5Pscncl8cjUOHUq5u19hThjkU2jOBzgIK2JS0bNmzSfH1eBTZMoCQBI1UJ1IbA8tqjQwpOXc+JkPBRU8T/JUQoQlSR6DTcPFvDgH2oGZYFHFfUontZqtz8jrIt2pxBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK5VgQp1x1FKgabFI6W/iGuy9ZCRoAixOOEGGkDps6dOEFgTQKTy5D/FZts9KuNxhhiD+NvS0d5BKYa5ITPLVPnGucgYkZhz+/+GhxmbjeQr0eJPaY7ZgLfH3tPA6tfdIkA0iE1En1sKEwt6R6DZjh9jtP9laoUoddTvYaFLJpZ2u1Ik94q6ZqX0fS/RKchaBHjhg6MtqCcHt07CBKHh8XNmKPXVSJC/p0MjyXv+qLaNNqyaAvAw6P6DX1hNjzrdkuaaHGXhu6kkezZUVlDWAm9cd1ppqalSK6ggy7yMW1NWTd/NYOPsFU2TS8DDPzRo14s1Qvw4v+TY6yT0NURJPQA=
+ </dsig:X509Certificate>
+ </dsig:X509Data>
+ </dsig:KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="signing">
+ <dsig:KeyInfo>
+ <dsig:KeyName>BzYc4GwL8HVrAhNyNdp-lTah2DvU9jU03kby9Ynohr4</dsig:KeyName>
+ <dsig:X509Data>
+ <dsig:X509Certificate>
+ MIICmzCCAYMCBgFX/9eK7TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYxMDI2MDcxMzE4WhcNMjYxMDI2MDgxNDU4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCDLT+40/BWzWPSVmpaSaZRs5lBMQ9VP9TCoXkby4PHqxIWRecTPM8fcNkPNPE/tiR2tUIpMXPDzgXNFA/EMoB3V1OEVXPecjKtiZczdR6pi75CBx7PJ2fSXg6xpjhZmHu0k7x591GZdP8Iiu2E6b9QA2p5VXgNgfuP07XzgabnSvIrLG60Imus3u6C2qA/QEuY7EYQWrFooriYLW6B8s3xU8R1a92SLMT8JsfMWXi+1CzAhIbVvdwUwkhVDDhAU6pUek88QQgxodd3FAMksoijCGFN1yrCkovlFhKb3j9AC6Icd9eeJuwYddN/nMeMGEDOeCcAGBACiaUisjUvZDw1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHAHbBI0CRfdw5ZHxHAjgSQvSj41c/4cfwln4Q7X3I5lMBbW3tcgond6Ku9eU46FzG5VpgXIgvEf4u0O9jUnxLlO50+t2SHwQ1RwHdBWQngVSZCRzscq3KrSzx1hx88qLyqcPrr3QtR92fYipDjENxttT/qJtDMrXlwLZEITlHDoneX319USYB9C4zlrCIsQ5XxQTTyCx886Pz15DSVSRxVp61HGk6ROsX/DG5/xwInlzgMZ0r3JWnAjtAaXqUrcwH9FXxco+xkiqKW79bGhWGQI9sXXvQSSNAaENMIUhxtd9uOi1l5e0EkKHE2fHlYyfdUDnFJWwSMXd/NM+hVI4Lw=
+ </dsig:X509Certificate>
+ </dsig:X509Data>
+ </dsig:KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+ </NameIDFormat>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ </NameIDFormat>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+ </NameIDFormat>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+ </NameIDFormat>
+ <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
+ <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
+ <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://localhost:8081/auth/realms/master/protocol/saml"/>
+ </IDPSSODescriptor>
+ </EntityDescriptor>
+</EntitiesDescriptor>
\ No newline at end of file
diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-multiple-signing-keys.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-multiple-signing-keys.xml
new file mode 100644
index 0000000..33b2f73
--- /dev/null
+++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-multiple-signing-keys.xml
@@ -0,0 +1,81 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter">
+ <SP entityID="sp"
+ sslPolicy="ssl"
+ nameIDPolicyFormat="format"
+ forceAuthentication="true"
+ isPassive="true">
+ <Keys>
+ <Key signing="true" >
+ <KeyStore file="file" resource="cp" password="pw">
+ <PrivateKey alias="private alias" password="private pw"/>
+ <Certificate alias="cert alias"/>
+ </KeyStore>
+ </Key>
+ <Key encryption="true">
+ <PrivateKeyPem>
+ private pem
+ </PrivateKeyPem>
+ <PublicKeyPem>
+ public pem
+ </PublicKeyPem>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="policy" attribute="attribute"/>
+ <RoleIdentifiers>
+ <Attribute name="member"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp"
+ signatureAlgorithm="RSA"
+ signatureCanonicalizationMethod="canon"
+ signaturesRequired="true"
+ >
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="post"
+ bindingUrl="url"
+ />
+
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="redirect"
+ responseBinding="post"
+ postBindingUrl="posturl"
+ redirectBindingUrl="redirecturl"
+ />
+ <Keys>
+ <Key signing="true">
+ <CertificatePem>cert pem 0</CertificatePem>
+ </Key>
+ <Key signing="true">
+ <CertificatePem>cert pem 1</CertificatePem>
+ </Key>
+ <Key signing="true">
+ <CertificatePem>cert pem 2</CertificatePem>
+ </Key>
+ <Key signing="true">
+ <CertificatePem>cert pem 3</CertificatePem>
+ </Key>
+ </Keys>
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-wth-http-client-settings.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-wth-http-client-settings.xml
new file mode 100644
index 0000000..36410e5
--- /dev/null
+++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-wth-http-client-settings.xml
@@ -0,0 +1,81 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter">
+ <SP entityID="sp"
+ sslPolicy="ssl"
+ nameIDPolicyFormat="format"
+ forceAuthentication="true"
+ isPassive="true">
+ <Keys>
+ <Key signing="true" >
+ <KeyStore file="file" resource="cp" password="pw">
+ <PrivateKey alias="private alias" password="private pw"/>
+ <Certificate alias="cert alias"/>
+ </KeyStore>
+ </Key>
+ <Key encryption="true">
+ <PrivateKeyPem>
+ private pem
+ </PrivateKeyPem>
+ <PublicKeyPem>
+ public pem
+ </PublicKeyPem>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="policy" attribute="attribute"/>
+ <RoleIdentifiers>
+ <Attribute name="member"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp"
+ signatureAlgorithm="RSA"
+ signatureCanonicalizationMethod="canon"
+ signaturesRequired="true"
+ >
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="post"
+ bindingUrl="url"
+ />
+
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="redirect"
+ responseBinding="post"
+ postBindingUrl="posturl"
+ redirectBindingUrl="redirecturl"
+ />
+ <Keys>
+ <Key signing="true">
+ <CertificatePem>
+ cert pem
+ </CertificatePem>
+ </Key>
+ </Keys>
+ <HttpClient allowAnyHostname="true"
+ clientKeystore="ks" clientKeystorePassword="ks-pwd"
+ connectionPoolSize="42"
+ disableTrustManager="true"
+ proxyUrl="pu"
+ truststore="ts" truststorePassword="tsp"
+ />
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java b/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java
index 3e4839a..283eb3e 100755
--- a/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java
+++ b/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java
@@ -98,7 +98,7 @@ public final class StringPropertyReplacer
public static String replaceProperties(final String string, final Properties props)
{
final char[] chars = string.toCharArray();
- StringBuffer buffer = new StringBuffer();
+ StringBuilder buffer = new StringBuilder();
boolean properties = false;
int state = NORMAL;
int start = 0;
diff --git a/common/src/main/java/org/keycloak/common/util/Time.java b/common/src/main/java/org/keycloak/common/util/Time.java
index ef5d174..54809d8 100644
--- a/common/src/main/java/org/keycloak/common/util/Time.java
+++ b/common/src/main/java/org/keycloak/common/util/Time.java
@@ -26,26 +26,51 @@ public class Time {
private static int offset;
+ /**
+ * Returns current time in seconds adjusted by adding {@link #offset) seconds.
+ * @return see description
+ */
public static int currentTime() {
return ((int) (System.currentTimeMillis() / 1000)) + offset;
}
+ /**
+ * Returns current time in milliseconds adjusted by adding {@link #offset) seconds.
+ * @return see description
+ */
public static long currentTimeMillis() {
return System.currentTimeMillis() + (offset * 1000);
}
+ /**
+ * Returns {@link Date} object, its value set to time
+ * @param time Time in milliseconds since the epoch
+ * @return see description
+ */
public static Date toDate(int time) {
return new Date(((long) time ) * 1000);
}
+ /**
+ * Returns time in milliseconds for a time in seconds. No adjustment is made to the parameter.
+ * @param time Time in seconds since the epoch
+ * @return Time in milliseconds
+ */
public static long toMillis(int time) {
return ((long) time) * 1000;
}
+ /**
+ * @return Time offset in seconds that will be added to {@link #currentTime()} and {@link #currentTimeMillis()}.
+ */
public static int getOffset() {
return offset;
}
+ /**
+ * Sets time offset in seconds that will be added to {@link #currentTime()} and {@link #currentTimeMillis()}.
+ * @param offset Offset (in seconds)
+ */
public static void setOffset(int offset) {
Time.offset = offset;
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index c4818b4..0ba327d 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -39,7 +39,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
"proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests",
"policy-enforcer"
})
-public class AdapterConfig extends BaseAdapterConfig {
+public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClientConfig {
@JsonProperty("allow-any-hostname")
protected boolean allowAnyHostname;
@@ -82,6 +82,7 @@ public class AdapterConfig extends BaseAdapterConfig {
@JsonProperty("proxy-url")
protected String proxyUrl;
+ @Override
public boolean isAllowAnyHostname() {
return allowAnyHostname;
}
@@ -90,6 +91,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.allowAnyHostname = allowAnyHostname;
}
+ @Override
public boolean isDisableTrustManager() {
return disableTrustManager;
}
@@ -98,6 +100,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.disableTrustManager = disableTrustManager;
}
+ @Override
public String getTruststore() {
return truststore;
}
@@ -106,6 +109,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.truststore = truststore;
}
+ @Override
public String getTruststorePassword() {
return truststorePassword;
}
@@ -114,6 +118,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.truststorePassword = truststorePassword;
}
+ @Override
public String getClientKeystore() {
return clientKeystore;
}
@@ -122,6 +127,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.clientKeystore = clientKeystore;
}
+ @Override
public String getClientKeystorePassword() {
return clientKeystorePassword;
}
@@ -138,6 +144,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.clientKeyPassword = clientKeyPassword;
}
+ @Override
public int getConnectionPoolSize() {
return connectionPoolSize;
}
@@ -202,6 +209,7 @@ public class AdapterConfig extends BaseAdapterConfig {
this.policyEnforcerConfig = policyEnforcerConfig;
}
+ @Override
public String getProxyUrl() {
return proxyUrl;
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java
new file mode 100644
index 0000000..fa4c87e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterHttpClientConfig.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.representations.adapters.config;
+
+/**
+ * Configuration options relevant for configuring http client that can be used by adapter.
+ *
+ * NOTE: keep in sync with adapters/saml/core/src/main/java/org/keycloak/adapters/AdapterHttpClientConfig.java until unified.
+ *
+ * @author hmlnarik
+ */
+public interface AdapterHttpClientConfig {
+
+ /**
+ * Returns truststore filename.
+ */
+ public String getTruststore();
+
+ /**
+ * Returns truststore password.
+ */
+ public String getTruststorePassword();
+
+ /**
+ * Returns keystore with client keys.
+ */
+ public String getClientKeystore();
+
+ /**
+ * Returns keystore password.
+ */
+ public String getClientKeystorePassword();
+
+ /**
+ * Returns boolean flag whether any hostname verification is done on the server's
+ * certificate, {@code true} means that verification is not done.
+ * @return
+ */
+ public boolean isAllowAnyHostname();
+
+ /**
+ * Returns boolean flag whether any trust management and hostname verification is done.
+ * <p>
+ * <i>NOTE</i> Disabling trust manager is a security hole, so only set this option
+ * if you cannot or do not want to verify the identity of the
+ * host you are communicating with.
+ */
+ public boolean isDisableTrustManager();
+
+ /**
+ * Returns size of connection pool.
+ */
+ public int getConnectionPoolSize();
+
+ /**
+ * Returns URL of HTTP proxy.
+ */
+ public String getProxyUrl();
+
+}
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml
index 451497f..1438ed9 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml
@@ -30,6 +30,7 @@
<module name="org.keycloak.keycloak-saml-core-public"/>
<module name="org.keycloak.keycloak-saml-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
index 49e14c4..4973aa1 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
@@ -34,6 +34,7 @@
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
<module name="org.keycloak.keycloak-saml-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml
index 1d728f5..b85e56f 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-adapter/main/module.xml
@@ -41,6 +41,7 @@
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
<module name="org.keycloak.keycloak-saml-adapter-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-subsystem/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-subsystem/main/module.xml
index d720b02..9d1a63e 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-subsystem/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-as7-subsystem/main/module.xml
@@ -39,5 +39,6 @@
<module name="org.jboss.logging"/>
<module name="org.jboss.vfs"/>
<module name="org.jboss.metadata"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml
index aac9500..7851fd5 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml
@@ -36,6 +36,7 @@
</imports>
</module>
<module name="javax.api"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml
index 550dac8..35977c7 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml
@@ -32,6 +32,7 @@
</imports>
</module>
<module name="javax.api"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml
index f04205b..40186d7 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml
@@ -29,6 +29,7 @@
<module name="org.picketbox"/>
<module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml
index 451497f..1438ed9 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-api-public/main/module.xml
@@ -30,6 +30,7 @@
<module name="org.keycloak.keycloak-saml-core-public"/>
<module name="org.keycloak.keycloak-saml-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
index 34e6895..e19e0f0 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml
@@ -34,6 +34,7 @@
<module name="org.keycloak.keycloak-saml-core-public"/>
<module name="org.keycloak.keycloak-saml-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml
index aac9500..7851fd5 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml
@@ -36,6 +36,7 @@
</imports>
</module>
<module name="javax.api"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml
index e5e572c..5637635 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core-public/main/module.xml
@@ -35,6 +35,7 @@
</imports>
</module>
<module name="javax.api"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml
index 397901c..d4cefc5 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml
@@ -40,6 +40,7 @@
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
<module name="org.keycloak.keycloak-saml-adapter-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml
index 3027115..ee00fcc 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml
@@ -41,6 +41,7 @@
<module name="org.keycloak.keycloak-saml-adapter-api-public"/>
<module name="org.keycloak.keycloak-saml-adapter-core"/>
<module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml
index cda8970..857a8e3 100755
--- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml
+++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml
@@ -39,5 +39,6 @@
<module name="org.jboss.vfs"/>
<module name="org.jboss.as.web-common"/>
<module name="org.jboss.metadata"/>
+ <module name="org.apache.httpcomponents"/>
</dependencies>
</module>
diff --git a/saml-core/nbproject/project.properties b/saml-core/nbproject/project.properties
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/saml-core/nbproject/project.properties
saml-core/pom.xml 12(+12 -0)
diff --git a/saml-core/pom.xml b/saml-core/pom.xml
index 8483240..8c08b69 100755
--- a/saml-core/pom.xml
+++ b/saml-core/pom.xml
@@ -53,6 +53,18 @@
<groupId>org.apache.santuario</groupId>
<artifactId>xmlsec</artifactId>
</dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.12</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-core</artifactId>
+ <version>1.3</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<resources>
diff --git a/saml-core/src/main/java/org/keycloak/rotation/CompositeKeyLocator.java b/saml-core/src/main/java/org/keycloak/rotation/CompositeKeyLocator.java
new file mode 100644
index 0000000..4b3cb57
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/rotation/CompositeKeyLocator.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.rotation;
+
+import java.security.Key;
+import java.security.KeyManagementException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * {@link KeyLocator} that represents a list of multiple {@link KeyLocator}s. Key is searched
+ * from the first to the last {@link KeyLocator} in the order given by the list. If there are
+ * multiple {@link KeyLocator}s providing key with the same key ID, the first matching key is
+ * returned.
+ *
+ * @author hmlnarik
+ */
+public class CompositeKeyLocator implements KeyLocator, Iterable<Key> {
+
+ private final List<KeyLocator> keyLocators = new LinkedList<>();
+
+ @Override
+ public Key getKey(String kid) throws KeyManagementException {
+ for (KeyLocator keyLocator : keyLocators) {
+ Key k = keyLocator.getKey(kid);
+ if (k != null) {
+ return k;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void refreshKeyCache() {
+ for (KeyLocator keyLocator : keyLocators) {
+ keyLocator.refreshKeyCache();
+ }
+ }
+
+ /**
+ * Registers a given {@link KeyLocator} as the first {@link KeyLocator}.
+ */
+ public void addFirst(KeyLocator keyLocator) {
+ this.keyLocators.add(0, keyLocator);
+ }
+
+ /**
+ * Registers a given {@link KeyLocator} as the last {@link KeyLocator}.
+ */
+ public void add(KeyLocator keyLocator) {
+ this.keyLocators.add(keyLocator);
+ }
+
+ /**
+ * Clears the list of registered {@link KeyLocator}s
+ */
+ public void clear() {
+ this.keyLocators.clear();
+ }
+
+ @Override
+ public String toString() {
+ if (this.keyLocators.size() == 1) {
+ return this.keyLocators.get(0).toString();
+ }
+
+ StringBuilder sb = new StringBuilder("Key locator chain: [");
+ for (Iterator<KeyLocator> it = keyLocators.iterator(); it.hasNext();) {
+ KeyLocator keyLocator = it.next();
+ sb.append(keyLocator.toString());
+ if (it.hasNext()) {
+ sb.append(", ");
+ }
+ }
+ return sb.append("]").toString();
+ }
+
+ @Override
+ public Iterator<Key> iterator() {
+ final Iterator<Iterable<Key>> iterablesIterator = getKeyLocatorIterators().iterator();
+
+ return new JointKeyIterator(iterablesIterator).iterator();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Iterable<Iterable<Key>> getKeyLocatorIterators() {
+ List<Iterable<Key>> res = new LinkedList<>();
+ for (KeyLocator kl : this.keyLocators) {
+ if (kl instanceof Iterable) {
+ res.add(((Iterable<Key>) kl));
+ }
+ }
+ return Collections.unmodifiableCollection(res);
+ }
+
+ private class JointKeyIterator implements Iterable<Key> {
+
+ // based on http://stackoverflow.com/a/34126154/6930869
+ private final Iterator<Iterable<Key>> iterablesIterator;
+
+ public JointKeyIterator(Iterator<Iterable<Key>> iterablesIterator) {
+ this.iterablesIterator = iterablesIterator;
+ }
+
+ @Override
+ public Iterator<Key> iterator() {
+ if (! iterablesIterator.hasNext()) {
+ return Collections.<Key>emptyIterator();
+ }
+
+ return new Iterator<Key>() {
+ private Iterator<Key> currentIterator = nextIterator();
+
+ @Override
+ public boolean hasNext() {
+ return currentIterator.hasNext();
+ }
+
+ @Override
+ public Key next() {
+ final Key next = currentIterator.next();
+ findNext();
+ return next;
+ }
+
+ private Iterator<Key> nextIterator() {
+ return iterablesIterator.next().iterator();
+ }
+
+ private Iterator<Key> findNext() {
+ while (! currentIterator.hasNext()) {
+ if (! iterablesIterator.hasNext()) {
+ break;
+ }
+ currentIterator = nextIterator();
+ }
+ return this;
+ }
+ }.findNext();
+ }
+ }
+}
diff --git a/saml-core/src/main/java/org/keycloak/rotation/HardcodedKeyLocator.java b/saml-core/src/main/java/org/keycloak/rotation/HardcodedKeyLocator.java
new file mode 100644
index 0000000..ae2615a
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/rotation/HardcodedKeyLocator.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.rotation;
+
+import java.security.Key;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * Key locator that always returns a specified key.
+ *
+ * @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
+ */
+public class HardcodedKeyLocator implements KeyLocator, Iterable<Key> {
+
+ private final Collection<? extends Key> keys;
+
+ public HardcodedKeyLocator(Key key) {
+ this.keys = Collections.singleton(key);
+ }
+
+ public HardcodedKeyLocator(Collection<? extends Key> keys) {
+ if (keys == null) {
+ throw new NullPointerException("keys");
+ }
+ this.keys = new LinkedList<>(keys);
+ }
+
+ @Override
+ public Key getKey(String kid) {
+ if (this.keys.size() == 1) {
+ return this.keys.iterator().next();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void refreshKeyCache() {
+ // do nothing
+ }
+
+ @Override
+ public String toString() {
+ return "hardcoded keys, count: " + this.keys.size();
+ }
+
+ @Override
+ public Iterator<Key> iterator() {
+ return Collections.unmodifiableCollection(keys).iterator();
+ }
+}
diff --git a/saml-core/src/main/java/org/keycloak/rotation/KeyLocator.java b/saml-core/src/main/java/org/keycloak/rotation/KeyLocator.java
new file mode 100644
index 0000000..7112eca
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/rotation/KeyLocator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.rotation;
+
+import java.security.Key;
+import java.security.KeyManagementException;
+
+/**
+ * This interface defines a method for obtaining a security key by ID.
+ * <p>
+ * If the {@code KeyLocator} implementor wants to make all its keys available for iteration,
+ * it should implement {@link Iterable}<{@code T extends }{@link Key}> interface.
+ * The base {@code KeyLocator} does not extend this interface to enable {@code KeyLocators}
+ * that do not support listing their keys.
+ *
+ * @author <a href="mailto:hmlnarik@redhat.com">Hynek Mlnařík</a>
+ */
+public interface KeyLocator {
+
+ /**
+ * Returns a key with a particular ID.
+ * @param kid Key ID
+ * @param configuration Configuration
+ * @return key, which should be used for verify signature on given "input"
+ * @throws KeyManagementException
+ */
+ Key getKey(String kid) throws KeyManagementException;
+
+ /**
+ * If this key locator caches keys in any way, forces this cache cleanup
+ * and refreshing the keys.
+ */
+ void refreshKeyCache();
+
+}
diff --git a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
index 6d84c13..f820a5e 100755
--- a/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/BaseSAML2BindingBuilder.java
@@ -38,11 +38,14 @@ import javax.crypto.spec.SecretKeySpec;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.namespace.QName;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.URI;
+import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
+import java.security.SignatureException;
import java.security.cert.X509Certificate;
import static org.keycloak.common.util.HtmlUtils.escapeAttribute;
@@ -55,6 +58,7 @@ import static org.keycloak.saml.common.util.StringUtil.isNotNull;
public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
protected static final Logger logger = Logger.getLogger(BaseSAML2BindingBuilder.class);
+ protected String signingKeyId;
protected KeyPair signingKeyPair;
protected X509Certificate signingCertificate;
protected boolean sign;
@@ -82,23 +86,27 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
return (T)this;
}
- public T signWith(KeyPair keyPair) {
+ public T signWith(String signingKeyId, KeyPair keyPair) {
+ this.signingKeyId = signingKeyId;
this.signingKeyPair = keyPair;
return (T)this;
}
- public T signWith(PrivateKey privateKey, PublicKey publicKey) {
+ public T signWith(String signingKeyId, PrivateKey privateKey, PublicKey publicKey) {
+ this.signingKeyId = signingKeyId;
this.signingKeyPair = new KeyPair(publicKey, privateKey);
return (T)this;
}
- public T signWith(KeyPair keyPair, X509Certificate cert) {
+ public T signWith(String signingKeyId, KeyPair keyPair, X509Certificate cert) {
+ this.signingKeyId = signingKeyId;
this.signingKeyPair = keyPair;
this.signingCertificate = cert;
return (T)this;
}
- public T signWith(PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) {
+ public T signWith(String signingKeyId, PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) {
+ this.signingKeyId = signingKeyId;
this.signingKeyPair = new KeyPair(publicKey, privateKey);
this.signingCertificate = cert;
return (T)this;
@@ -263,7 +271,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
samlSignature.setX509Certificate(signingCertificate);
}
- samlSignature.signSAMLDocument(samlDocument, signingKeyPair, canonicalizationMethodType);
+ samlSignature.signSAMLDocument(samlDocument, signingKeyId, signingKeyPair, canonicalizationMethodType);
}
public void signAssertion(Document samlDocument) throws ProcessingException {
@@ -333,7 +341,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
public String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
String documentAsString = DocumentUtil.getDocumentAsString(document);
- logger.debugv("saml docment: {0}", documentAsString);
+ logger.debugv("saml document: {0}", documentAsString);
byte[] responseBytes = documentAsString.getBytes("UTF-8");
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
@@ -358,7 +366,7 @@ public class BaseSAML2BindingBuilder<T extends BaseSAML2BindingBuilder> {
signature.initSign(signingKeyPair.getPrivate());
signature.update(rawQuery.getBytes("UTF-8"));
sig = signature.sign();
- } catch (Exception e) {
+ } catch (InvalidKeyException | UnsupportedEncodingException | SignatureException e) {
throw new ProcessingException(e);
}
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/DefaultPicketLinkLogger.java b/saml-core/src/main/java/org/keycloak/saml/common/DefaultPicketLinkLogger.java
index 76a72ae..2f58911 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/DefaultPicketLinkLogger.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/DefaultPicketLinkLogger.java
@@ -450,6 +450,11 @@ public class DefaultPicketLinkLogger implements PicketLinkLogger {
return new RuntimeException(ErrorCodes.EXPECTED_TAG + tag + ">. Found <" + foundElementTag + ">");
}
+ @Override
+ public RuntimeException parserExpectedNamespace(String ns, String foundElementNs) {
+ return new RuntimeException(ErrorCodes.EXPECTED_NAMESPACE + ns + ">. Found <" + foundElementNs + ">");
+ }
+
/*
*(non-Javadoc)
*
@@ -2378,4 +2383,10 @@ public class DefaultPicketLinkLogger implements PicketLinkLogger {
return new ProcessingException("Wrong audience [" + serviceURL + "].");
}
+ @Override
+ public ProcessingException samlExtensionUnknownChild(Class<?> clazz) {
+ return new ProcessingException("Unknown child type specified for extension: "
+ + (clazz == null ? "<null>" : clazz.getSimpleName())
+ + ".");
+ }
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/ErrorCodes.java b/saml-core/src/main/java/org/keycloak/saml/common/ErrorCodes.java
index 09f4301..37a7755 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/ErrorCodes.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/ErrorCodes.java
@@ -48,6 +48,8 @@ public interface ErrorCodes {
String EXPECTED_TAG = "PL00066: Parser : Expected start tag:";
+ String EXPECTED_NAMESPACE = "PL00107: Parser : Expected start element namespace:";
+
String EXPECTED_TEXT_VALUE = "PL00071: Parser: Expected text value:";
String EXPECTED_END_TAG = "PL00066: Parser : Expected end tag:";
diff --git a/saml-core/src/main/java/org/keycloak/saml/common/PicketLinkLogger.java b/saml-core/src/main/java/org/keycloak/saml/common/PicketLinkLogger.java
index 7ac6a09..91f2f54 100755
--- a/saml-core/src/main/java/org/keycloak/saml/common/PicketLinkLogger.java
+++ b/saml-core/src/main/java/org/keycloak/saml/common/PicketLinkLogger.java
@@ -297,6 +297,14 @@ public interface PicketLinkLogger {
RuntimeException parserExpectedTag(String tag, String foundElementTag);
/**
+ * @param ns
+ * @param foundElementNs
+ *
+ * @return
+ */
+ RuntimeException parserExpectedNamespace(String ns, String foundElementNs);
+
+ /**
* @param elementName
*
* @return
@@ -1219,4 +1227,6 @@ public interface PicketLinkLogger {
RuntimeException parserFeatureNotSupported(String feature);
ProcessingException samlAssertionWrongAudience(String serviceURL);
+
+ ProcessingException samlExtensionUnknownChild(Class<?> clazz);
}
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
index 5ac8ce1..49c8df8 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/sig/SAML2Signature.java
@@ -35,8 +35,8 @@ import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.parsers.ParserConfigurationException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
-import java.security.PublicKey;
import java.security.cert.X509Certificate;
+import org.keycloak.rotation.KeyLocator;
/**
* Class that deals with SAML2 Signature
@@ -121,7 +121,7 @@ public class SAML2Signature {
* @throws MarshalException
* @throws GeneralSecurityException
*/
- public Document sign(Document doc, String referenceID, KeyPair keyPair, String canonicalizationMethodType) throws ParserConfigurationException,
+ public Document sign(Document doc, String referenceID, String keyId, KeyPair keyPair, String canonicalizationMethodType) throws ParserConfigurationException,
GeneralSecurityException, MarshalException, XMLSignatureException {
String referenceURI = "#" + referenceID;
@@ -130,6 +130,7 @@ public class SAML2Signature {
if (sibling != null) {
SignatureUtilTransferObject dto = new SignatureUtilTransferObject();
dto.setDocumentToBeSigned(doc);
+ dto.setKeyId(keyId);
dto.setKeyPair(keyPair);
dto.setDigestMethod(digestMethod);
dto.setSignatureMethod(signatureMethod);
@@ -142,7 +143,7 @@ public class SAML2Signature {
return XMLSignatureUtil.sign(dto, canonicalizationMethodType);
}
- return XMLSignatureUtil.sign(doc, keyPair, digestMethod, signatureMethod, referenceURI, canonicalizationMethodType);
+ return XMLSignatureUtil.sign(doc, keyId, keyPair, digestMethod, signatureMethod, referenceURI, canonicalizationMethodType);
}
/**
@@ -153,12 +154,12 @@ public class SAML2Signature {
*
* @throws org.keycloak.saml.common.exceptions.ProcessingException
*/
- public void signSAMLDocument(Document samlDocument, KeyPair keypair, String canonicalizationMethodType) throws ProcessingException {
+ public void signSAMLDocument(Document samlDocument, String keyId, KeyPair keypair, String canonicalizationMethodType) throws ProcessingException {
// Get the ID from the root
String id = samlDocument.getDocumentElement().getAttribute(ID_ATTRIBUTE_NAME);
try {
- sign(samlDocument, id, keypair, canonicalizationMethodType);
- } catch (Exception e) {
+ sign(samlDocument, id, keyId, keypair, canonicalizationMethodType);
+ } catch (ParserConfigurationException | GeneralSecurityException | MarshalException | XMLSignatureException e) {
throw new ProcessingException(logger.signatureError(e));
}
}
@@ -167,20 +168,18 @@ public class SAML2Signature {
* Validate the SAML2 Document
*
* @param signedDocument
- * @param publicKey
+ * @param keyLocator
*
* @return
*
* @throws ProcessingException
*/
- public boolean validate(Document signedDocument, PublicKey publicKey) throws ProcessingException {
+ public boolean validate(Document signedDocument, KeyLocator keyLocator) throws ProcessingException {
try {
configureIdAttribute(signedDocument);
- return XMLSignatureUtil.validate(signedDocument, publicKey);
- } catch (MarshalException me) {
+ return XMLSignatureUtil.validate(signedDocument, keyLocator);
+ } catch (MarshalException | XMLSignatureException me) {
throw new ProcessingException(logger.signatureError(me));
- } catch (XMLSignatureException xse) {
- throw new ProcessingException(logger.signatureError(xse));
}
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/util/KeyInfoTools.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/util/KeyInfoTools.java
new file mode 100644
index 0000000..be9bf51
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/util/KeyInfoTools.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.saml.processing.api.util;
+
+import java.security.cert.X509Certificate;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyName;
+import javax.xml.crypto.dsig.keyinfo.X509Data;
+
+/**
+ * Tools for {@link KeyInfo} object manipulation.
+ * @author hmlnarik
+ */
+public class KeyInfoTools {
+
+ /**
+ * Returns the first object of the given class from the given Iterable.
+ * @param <T>
+ * @param objects
+ * @param clazz
+ * @return The object or {@code null} if not found.
+ */
+ public static <T> T getContent(Iterable<Object> objects, Class<T> clazz) {
+ for (Object o : objects) {
+ if (clazz.isInstance(o)) {
+ return (T) o;
+ }
+ }
+ return null;
+ }
+
+
+ public static KeyName getKeyName(KeyInfo keyInfo) {
+ return getContent(keyInfo.getContent(), KeyName.class);
+ }
+
+ public static X509Data getX509Data(KeyInfo keyInfo) {
+ return getContent(keyInfo.getContent(), X509Data.class);
+ }
+
+ public static X509Certificate getX509Certificate(KeyInfo keyInfo) {
+ X509Data d = getX509Data(keyInfo);
+ return d == null ? null : getContent(d.getContent(), X509Certificate.class);
+ }
+
+}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResolveParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResolveParser.java
index ca0316b..06d6042 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResolveParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResolveParser.java
@@ -58,6 +58,8 @@ public class SAMLArtifactResolveParser extends SAMLRequestAbstractParser impleme
continue;
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
continue;
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ continue;
} else
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
+ startElement.getLocation());
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResponseParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResponseParser.java
index 5f98403..9d3686b 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResponseParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLArtifactResponseParser.java
@@ -68,6 +68,9 @@ public class SAMLArtifactResponseParser extends SAMLStatusResponseTypeParser imp
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
Element sig = StaxParserUtil.getDOMElement(xmlEventReader);
response.setSignature(sig);
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ SAMLExtensionsParser extensionsParser = new SAMLExtensionsParser();
+ response.setExtensions(extensionsParser.parse(xmlEventReader));
} else if (JBossSAMLConstants.AUTHN_REQUEST.get().equals(elementName)) {
SAMLAuthNRequestParser authnParser = new SAMLAuthNRequestParser();
AuthnRequestType authn = (AuthnRequestType) authnParser.parse(xmlEventReader);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAttributeQueryParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAttributeQueryParser.java
index 139e4e0..6102e9e 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAttributeQueryParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAttributeQueryParser.java
@@ -60,6 +60,8 @@ public class SAMLAttributeQueryParser extends SAMLRequestAbstractParser implemen
continue;
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
continue;
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ continue;
} else
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
+ startElement.getLocation());
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAuthNRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAuthNRequestParser.java
index 5a15b2c..f1d3349 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAuthNRequestParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLAuthNRequestParser.java
@@ -76,6 +76,8 @@ public class SAMLAuthNRequestParser extends SAMLRequestAbstractParser implements
continue;
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
continue;
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ continue;
} else
throw new RuntimeException(ErrorCodes.UNKNOWN_START_ELEMENT + elementName + "::location="
+ startElement.getLocation());
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLExtensionsParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLExtensionsParser.java
new file mode 100644
index 0000000..5f7ebbb
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLExtensionsParser.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.saml.processing.core.parsers.saml;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.events.EndElement;
+import javax.xml.stream.events.StartElement;
+import javax.xml.stream.events.XMLEvent;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import org.keycloak.saml.common.PicketLinkLogger;
+import org.keycloak.saml.common.PicketLinkLoggerFactory;
+import org.keycloak.saml.common.constants.JBossSAMLConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.common.parsers.ParserNamespaceSupport;
+import org.keycloak.saml.common.util.StaxParserUtil;
+
+/**
+ * Parses <samlp:Extensions> SAML2 element into series of DOM nodes.
+ *
+ * @author hmlnarik
+ */
+public class SAMLExtensionsParser implements ParserNamespaceSupport {
+
+ private static final String EXTENSIONS = JBossSAMLConstants.EXTENSIONS.get();
+
+ private static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
+
+ @Override
+ public ExtensionsType parse(XMLEventReader xmlEventReader) throws ParsingException {
+ // Get the startelement
+ StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
+ StaxParserUtil.validate(startElement, EXTENSIONS);
+
+ ExtensionsType extensions = new ExtensionsType();
+
+ while (xmlEventReader.hasNext()) {
+ XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
+ if (xmlEvent instanceof EndElement) {
+ EndElement endElement = (EndElement) xmlEvent;
+ if (StaxParserUtil.matches(endElement, EXTENSIONS)) {
+ endElement = StaxParserUtil.getNextEndElement(xmlEventReader);
+ break;
+ } else
+ throw logger.parserUnknownEndElement(StaxParserUtil.getEndElementName(endElement));
+ }
+
+ startElement = StaxParserUtil.peekNextStartElement(xmlEventReader);
+ if (startElement == null)
+ break;
+
+ extensions.addExtension(StaxParserUtil.getDOMElement(xmlEventReader));
+ }
+
+ return extensions;
+ }
+
+ @Override
+ public boolean supports(QName qname) {
+ String nsURI = qname.getNamespaceURI();
+ String localPart = qname.getLocalPart();
+
+ return nsURI.equals(JBossSAMLURIConstants.PROTOCOL_NSURI.get())
+ && localPart.equals(JBossSAMLConstants.EXTENSIONS.get());
+ }
+
+}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLResponseParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLResponseParser.java
index a2691c2..92eaf90 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLResponseParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLResponseParser.java
@@ -71,6 +71,9 @@ public class SAMLResponseParser extends SAMLStatusResponseTypeParser implements
} else if (JBossSAMLConstants.ASSERTION.get().equals(elementName)) {
SAMLAssertionParser assertionParser = new SAMLAssertionParser();
response.addAssertion(new RTChoiceType((AssertionType) assertionParser.parse(xmlEventReader)));
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ SAMLExtensionsParser extensionsParser = new SAMLExtensionsParser();
+ response.setExtensions(extensionsParser.parse(xmlEventReader));
} else if (JBossSAMLConstants.STATUS.get().equals(elementName)) {
response.setStatus(parseStatus(xmlEventReader));
} else if (JBossSAMLConstants.ENCRYPTED_ASSERTION.get().equals(elementName)) {
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloRequestParser.java
index f604cf5..22ed383 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloRequestParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloRequestParser.java
@@ -74,6 +74,8 @@ public class SAMLSloRequestParser extends SAMLRequestAbstractParser implements P
continue;
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
continue;
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ continue;
} else
throw logger.parserUnknownTag(elementName, startElement.getLocation());
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloResponseParser.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloResponseParser.java
index 167a3c5..c0f473b 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloResponseParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/parsers/saml/SAMLSloResponseParser.java
@@ -60,6 +60,9 @@ public class SAMLSloResponseParser extends SAMLStatusResponseTypeParser implemen
} else if (JBossSAMLConstants.SIGNATURE.get().equals(elementName)) {
startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
StaxParserUtil.bypassElementBlock(xmlEventReader, JBossSAMLConstants.SIGNATURE.get());
+ } else if (JBossSAMLConstants.EXTENSIONS.get().equals(elementName)) {
+ SAMLExtensionsParser extensionsParser = new SAMLExtensionsParser();
+ response.setExtensions(extensionsParser.parse(xmlEventReader));
} else if (JBossSAMLConstants.STATUS.get().equals(elementName)) {
response.setStatus(parseStatus(xmlEventReader));
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
index 67fb78f..ed941a0 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/util/AssertionUtil.java
@@ -62,6 +62,7 @@ import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import org.keycloak.rotation.HardcodedKeyLocator;
/**
* Utility to deal with assertions
@@ -276,7 +277,7 @@ public class AssertionUtil {
Node n = doc.importNode(assertionElement, true);
doc.appendChild(n);
- return new SAML2Signature().validate(doc, publicKey);
+ return new SAML2Signature().validate(doc, new HardcodedKeyLocator(publicKey));
} catch (Exception e) {
logger.signatureAssertionValidationError(e);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/BaseWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/BaseWriter.java
index 4c041d1..068c91a 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/BaseWriter.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/BaseWriter.java
@@ -43,8 +43,12 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
+import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
+import org.w3c.dom.Node;
/**
* Base Class for the Stax writers for SAML
@@ -244,6 +248,28 @@ public class BaseWriter {
StaxUtil.flush(writer);
}
+ public void write(ExtensionsType extensions) throws ProcessingException {
+ if (extensions.getAny().isEmpty()) {
+ return;
+ }
+
+ StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.EXTENSIONS.get(), PROTOCOL_NSURI.get());
+
+ for (Object o : extensions.getAny()) {
+ if (o instanceof Node) {
+ StaxUtil.writeDOMNode(writer, (Node) o);
+ } else if (o instanceof SamlProtocolExtensionsAwareBuilder.NodeGenerator) {
+ SamlProtocolExtensionsAwareBuilder.NodeGenerator ng = (SamlProtocolExtensionsAwareBuilder.NodeGenerator) o;
+ ng.write(writer);
+ } else {
+ throw logger.samlExtensionUnknownChild(o == null ? null : o.getClass());
+ }
+ }
+
+ StaxUtil.writeEndElement(writer);
+ StaxUtil.flush(writer);
+ }
+
private void write(SubjectConfirmationType subjectConfirmationType) throws ProcessingException {
StaxUtil.writeStartElement(writer, ASSERTION_PREFIX, JBossSAMLConstants.SUBJECT_CONFIRMATION.get(),
ASSERTION_NSURI.get());
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java
index 9f99780..8c115f5 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLRequestWriter.java
@@ -36,6 +36,7 @@ import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamWriter;
import java.net.URI;
import java.util.List;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
@@ -122,6 +123,11 @@ public class SAMLRequestWriter extends BaseWriter {
StaxUtil.writeDOMElement(writer, sig);
}
+ ExtensionsType extensions = request.getExtensions();
+ if (extensions != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
+
NameIDPolicyType nameIDPolicy = request.getNameIDPolicy();
if (nameIDPolicy != null) {
write(nameIDPolicy);
@@ -171,6 +177,11 @@ public class SAMLRequestWriter extends BaseWriter {
StaxUtil.writeDOMElement(writer, signature);
}
+ ExtensionsType extensions = logOutRequest.getExtensions();
+ if (extensions != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
+
NameIDType nameID = logOutRequest.getNameID();
if (nameID != null) {
write(nameID, new QName(ASSERTION_NSURI.get(), JBossSAMLConstants.NAMEID.get(), ASSERTION_PREFIX));
@@ -278,6 +289,11 @@ public class SAMLRequestWriter extends BaseWriter {
if (sig != null) {
StaxUtil.writeDOMElement(writer, sig);
}
+ ExtensionsType extensions = request.getExtensions();
+ if (extensions != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
+
String artifact = request.getArtifact();
if (StringUtil.isNotNull(artifact)) {
StaxUtil.writeStartElement(writer, PROTOCOL_PREFIX, JBossSAMLConstants.ARTIFACT.get(), PROTOCOL_NSURI.get());
@@ -315,6 +331,10 @@ public class SAMLRequestWriter extends BaseWriter {
if (sig != null) {
StaxUtil.writeDOMElement(writer, sig);
}
+ ExtensionsType extensions = request.getExtensions();
+ if (extensions != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
SubjectType subject = request.getSubject();
if (subject != null) {
write(subject);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java
index 07fae2a..9327a73 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/saml/v2/writers/SAMLResponseWriter.java
@@ -37,6 +37,7 @@ import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamWriter;
import java.net.URI;
import java.util.List;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
/**
* Write a SAML Response to stream
@@ -78,6 +79,10 @@ public class SAMLResponseWriter extends BaseWriter {
if (sig != null) {
StaxUtil.writeDOMElement(writer, sig);
}
+ ExtensionsType extensions = response.getExtensions();
+ if (extensions != null && extensions.getAny() != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
StatusType status = response.getStatus();
write(status);
@@ -119,6 +124,10 @@ public class SAMLResponseWriter extends BaseWriter {
if (sig != null) {
StaxUtil.writeDOMElement(writer, sig);
}
+ ExtensionsType extensions = response.getExtensions();
+ if (extensions != null && extensions.getAny() != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
StatusType status = response.getStatus();
if (status != null) {
@@ -163,6 +172,15 @@ public class SAMLResponseWriter extends BaseWriter {
NameIDType issuer = response.getIssuer();
write(issuer, new QName(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ISSUER.get(), ASSERTION_PREFIX));
+ Element sig = response.getSignature();
+ if (sig != null) {
+ StaxUtil.writeDOMElement(writer, sig);
+ }
+ ExtensionsType extensions = response.getExtensions();
+ if (extensions != null && extensions.getAny() != null && ! extensions.getAny().isEmpty()) {
+ write(extensions);
+ }
+
StatusType status = response.getStatus();
write(status);
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java
new file mode 100644
index 0000000..1bb90ea
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/KeycloakKeySamlExtensionGenerator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.saml.processing.core.util;
+
+import java.util.Objects;
+import javax.xml.stream.XMLStreamWriter;
+import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.common.util.StaxUtil;
+import org.w3c.dom.Element;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KeycloakKeySamlExtensionGenerator implements SamlProtocolExtensionsAwareBuilder.NodeGenerator {
+
+ public static final String NS_URI = "urn:keycloak:ext:key:1.0";
+
+ public static final String NS_PREFIX = "kckey";
+
+ public static final String KC_KEY_INFO_ELEMENT_NAME = "KeyInfo";
+
+ public static final String KEY_ID_ATTRIBUTE_NAME = "MessageSigningKeyId";
+
+ private final String keyId;
+
+ public KeycloakKeySamlExtensionGenerator(String keyId) {
+ this.keyId = keyId;
+ }
+
+ @Override
+ public void write(XMLStreamWriter writer) throws ProcessingException {
+ StaxUtil.writeStartElement(writer, NS_PREFIX, KC_KEY_INFO_ELEMENT_NAME, NS_URI);
+ StaxUtil.writeNameSpace(writer, NS_PREFIX, NS_URI);
+ if (this.keyId != null) {
+ StaxUtil.writeAttribute(writer, KEY_ID_ATTRIBUTE_NAME, this.keyId);
+ }
+ StaxUtil.writeEndElement(writer);
+ StaxUtil.flush(writer);
+ }
+
+ /**
+ * Checks that the given element is indeed a Keycloak extension {@code KeyInfo} element and
+ * returns a content of {@code MessageSigningKeyId} attribute in the given element.
+ * @param element Element to obtain the key info from.
+ * @return {@code null} if the element is unknown or there is {@code MessageSigningKeyId} attribute unset,
+ * value of the {@code MessageSigningKeyId} attribute otherwise.
+ */
+ public static String getMessageSigningKeyIdFromElement(Element element) {
+ if (Objects.equals(element.getNamespaceURI(), NS_URI) &&
+ Objects.equals(element.getLocalName(), KC_KEY_INFO_ELEMENT_NAME) &&
+ element.hasAttribute(KEY_ID_ATTRIBUTE_NAME)) {
+ return element.getAttribute(KEY_ID_ATTRIBUTE_NAME);
+ }
+
+ return null;
+ }
+
+}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/SignatureUtilTransferObject.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/SignatureUtilTransferObject.java
index 19924e9..f8181fe 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/SignatureUtilTransferObject.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/SignatureUtilTransferObject.java
@@ -32,6 +32,9 @@ public class SignatureUtilTransferObject {
private X509Certificate x509Certificate;
private Document documentToBeSigned;
+
+ private String keyId;
+
private KeyPair keyPair;
private Node nextSibling;
@@ -111,4 +114,12 @@ public class SignatureUtilTransferObject {
public void setX509Certificate(X509Certificate x509Certificate) {
this.x509Certificate = x509Certificate;
}
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public void setKeyId(String keyId) {
+ this.keyId = keyId;
+ }
}
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
index 98635b7..193af19 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/core/util/XMLSignatureUtil.java
@@ -54,8 +54,6 @@ import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
-import javax.xml.crypto.dsig.keyinfo.KeyValue;
-import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.namespace.QName;
@@ -69,6 +67,7 @@ import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyException;
+import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
@@ -79,7 +78,16 @@ import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.LinkedList;
import java.util.List;
+import javax.xml.crypto.AlgorithmMethod;
+import javax.xml.crypto.KeySelector;
+import javax.xml.crypto.KeySelectorException;
+import javax.xml.crypto.KeySelectorResult;
+import javax.xml.crypto.XMLCryptoContext;
+import javax.xml.crypto.dsig.keyinfo.KeyName;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.api.util.KeyInfoTools;
/**
* Utility for XML Signature <b>Note:</b> You can change the canonicalization method type by using the system property
@@ -105,15 +113,66 @@ public class XMLSignatureUtil {
;
- private static String canonicalizationMethodType = CanonicalizationMethod.EXCLUSIVE;
-
- private static XMLSignatureFactory fac = getXMLSignatureFactory();
+ private static final XMLSignatureFactory fac = getXMLSignatureFactory();
/**
* By default, we include the keyinfo in the signature
*/
private static boolean includeKeyInfoInSignature = true;
+ private static class KeySelectorUtilizingKeyNameHint extends KeySelector {
+
+ private final KeyLocator locator;
+
+ private boolean keyLocated = false;
+
+ private String keyName = null;
+
+ public KeySelectorUtilizingKeyNameHint(KeyLocator locator) {
+ this.locator = locator;
+ }
+
+ @Override
+ public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException {
+ try {
+ KeyName keyNameEl = KeyInfoTools.getKeyName(keyInfo);
+ this.keyName = keyNameEl == null ? null : keyNameEl.getName();
+ final Key key = locator.getKey(keyName);
+ this.keyLocated = key != null;
+ return new KeySelectorResult() {
+ @Override public Key getKey() {
+ return key;
+ }
+ };
+ } catch (KeyManagementException ex) {
+ throw new KeySelectorException(ex);
+ }
+
+ }
+
+ private boolean wasKeyLocated() {
+ return this.keyLocated;
+ }
+ }
+
+ private static class KeySelectorPresetKey extends KeySelector {
+
+ private final Key key;
+
+ public KeySelectorPresetKey(Key key) {
+ this.key = key;
+ }
+
+ @Override
+ public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) {
+ return new KeySelectorResult() {
+ @Override public Key getKey() {
+ return key;
+ }
+ };
+ }
+ }
+
private static XMLSignatureFactory getXMLSignatureFactory() {
XMLSignatureFactory xsf = null;
@@ -157,7 +216,7 @@ public class XMLSignatureUtil {
* @throws MarshalException
* @throws GeneralSecurityException
*/
- public static Document sign(Document doc, Node nodeToBeSigned, KeyPair keyPair, String digestMethod,
+ public static Document sign(Document doc, Node nodeToBeSigned, String keyId, KeyPair keyPair, String digestMethod,
String signatureMethod, String referenceURI, X509Certificate x509Certificate,
String canonicalizationMethodType) throws ParserConfigurationException, GeneralSecurityException,
MarshalException, XMLSignatureException {
@@ -179,7 +238,7 @@ public class XMLSignatureUtil {
if (!referenceURI.isEmpty()) {
propagateIDAttributeSetup(nodeToBeSigned, newDoc.getDocumentElement());
}
- newDoc = sign(newDoc, keyPair, digestMethod, signatureMethod, referenceURI, x509Certificate, canonicalizationMethodType);
+ newDoc = sign(newDoc, keyId, keyPair, digestMethod, signatureMethod, referenceURI, x509Certificate, canonicalizationMethodType);
// if the signed element is a SAMLv2.0 assertion we need to move the signature element to the position
// specified in the schema (before the assertion subject element).
@@ -220,10 +279,10 @@ public class XMLSignatureUtil {
* @throws MarshalException
* @throws XMLSignatureException
*/
- public static void sign(Element elementToSign, Node nextSibling, KeyPair keyPair, String digestMethod,
+ public static void sign(Element elementToSign, Node nextSibling, String keyId, KeyPair keyPair, String digestMethod,
String signatureMethod, String referenceURI, String canonicalizationMethodType)
throws GeneralSecurityException, MarshalException, XMLSignatureException {
- sign(elementToSign, nextSibling, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
+ sign(elementToSign, nextSibling, keyId, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
}
/**
@@ -242,7 +301,7 @@ public class XMLSignatureUtil {
* @throws XMLSignatureException
* @since 2.5.0
*/
- public static void sign(Element elementToSign, Node nextSibling, KeyPair keyPair, String digestMethod,
+ public static void sign(Element elementToSign, Node nextSibling, String keyId, KeyPair keyPair, String digestMethod,
String signatureMethod, String referenceURI, X509Certificate x509Certificate, String canonicalizationMethodType)
throws GeneralSecurityException, MarshalException, XMLSignatureException {
PrivateKey signingKey = keyPair.getPrivate();
@@ -250,7 +309,7 @@ public class XMLSignatureUtil {
DOMSignContext dsc = new DOMSignContext(signingKey, elementToSign, nextSibling);
- signImpl(dsc, digestMethod, signatureMethod, referenceURI, publicKey, x509Certificate, canonicalizationMethodType);
+ signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, x509Certificate, canonicalizationMethodType);
}
/**
@@ -284,9 +343,9 @@ public class XMLSignatureUtil {
* @throws XMLSignatureException
* @throws MarshalException
*/
- public static Document sign(Document doc, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI, String canonicalizationMethodType)
+ public static Document sign(Document doc, String keyId, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI, String canonicalizationMethodType)
throws GeneralSecurityException, MarshalException, XMLSignatureException {
- return sign(doc, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
+ return sign(doc, keyId, keyPair, digestMethod, signatureMethod, referenceURI, null, canonicalizationMethodType);
}
/**
@@ -304,7 +363,7 @@ public class XMLSignatureUtil {
* @throws MarshalException
* @since 2.5.0
*/
- public static Document sign(Document doc, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI,
+ public static Document sign(Document doc, String keyId, KeyPair keyPair, String digestMethod, String signatureMethod, String referenceURI,
X509Certificate x509Certificate, String canonicalizationMethodType)
throws GeneralSecurityException, MarshalException, XMLSignatureException {
logger.trace("Document to be signed=" + DocumentUtil.asString(doc));
@@ -313,7 +372,7 @@ public class XMLSignatureUtil {
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement());
- signImpl(dsc, digestMethod, signatureMethod, referenceURI, publicKey, x509Certificate, canonicalizationMethodType);
+ signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, x509Certificate, canonicalizationMethodType);
return doc;
}
@@ -331,6 +390,7 @@ public class XMLSignatureUtil {
public static Document sign(SignatureUtilTransferObject dto, String canonicalizationMethodType) throws GeneralSecurityException, MarshalException,
XMLSignatureException {
Document doc = dto.getDocumentToBeSigned();
+ String keyId = dto.getKeyId();
KeyPair keyPair = dto.getKeyPair();
Node nextSibling = dto.getNextSibling();
String digestMethod = dto.getDigestMethod();
@@ -344,13 +404,14 @@ public class XMLSignatureUtil {
DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement(), nextSibling);
- signImpl(dsc, digestMethod, signatureMethod, referenceURI, publicKey, dto.getX509Certificate(), canonicalizationMethodType);
+ signImpl(dsc, digestMethod, signatureMethod, referenceURI, keyId, publicKey, dto.getX509Certificate(), canonicalizationMethodType);
return doc;
}
/**
- * Validate a signed document with the given public key
+ * Validate a signed document with the given public key. All elements that contain a Signature are checked,
+ * this way both assertions and the containing document are verified when signed.
*
* @param signedDoc
* @param publicKey
@@ -361,7 +422,7 @@ public class XMLSignatureUtil {
* @throws XMLSignatureException
*/
@SuppressWarnings("unchecked")
- public static boolean validate(Document signedDoc, Key publicKey) throws MarshalException, XMLSignatureException {
+ public static boolean validate(Document signedDoc, final KeyLocator locator) throws MarshalException, XMLSignatureException {
if (signedDoc == null)
throw logger.nullArgumentError("Signed Document");
@@ -374,7 +435,7 @@ public class XMLSignatureUtil {
return false;
}
- if (publicKey == null)
+ if (locator == null)
throw logger.nullValueError("Public Key");
int signedAssertions = 0;
@@ -390,24 +451,7 @@ public class XMLSignatureUtil {
}
}
- DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(i));
- XMLSignature signature = fac.unmarshalXMLSignature(valContext);
-
- boolean coreValidity = signature.validate(valContext);
-
- if (!coreValidity) {
- if (logger.isTraceEnabled()) {
- boolean sv = signature.getSignatureValue().validate(valContext);
- logger.trace("Signature validation status: " + sv);
-
- List<Reference> references = signature.getSignedInfo().getReferences();
- for (Reference ref : references) {
- logger.trace("[Ref id=" + ref.getId() + ":uri=" + ref.getURI() + "]validity status:" + ref.validate(valContext));
- }
- }
-
- return false;
- }
+ if (! validateSingleNode(signatureNode, locator)) return false;
}
NodeList assertions = signedDoc.getElementsByTagNameNS(assertionNameSpaceUri, JBossSAMLConstants.ASSERTION.get());
@@ -423,6 +467,62 @@ public class XMLSignatureUtil {
return true;
}
+ private static boolean validateSingleNode(Node signatureNode, final KeyLocator locator) throws MarshalException, XMLSignatureException {
+ KeySelectorUtilizingKeyNameHint sel = new KeySelectorUtilizingKeyNameHint(locator);
+ try {
+ if (validateUsingKeySelector(signatureNode, sel)) {
+ return true;
+ }
+ if (sel.wasKeyLocated()) {
+ return false;
+ }
+ } catch (XMLSignatureException ex) { // pass through MarshalException
+ logger.debug("Verification failed for key " + sel.keyName + ": " + ex);
+ logger.trace(ex);
+ }
+
+ logger.trace("Could not validate signature using ds:KeyInfo/ds:KeyName hint.");
+
+ if (locator instanceof Iterable) {
+ Iterable<Key> availableKeys = (Iterable<Key>) locator;
+
+ logger.trace("Trying hard to validate XML signature using all available keys.");
+
+ for (Key key : availableKeys) {
+ try {
+ if (validateUsingKeySelector(signatureNode, new KeySelectorPresetKey(key))) {
+ return true;
+ }
+ } catch (XMLSignatureException ex) { // pass through MarshalException
+ logger.debug("Verification failed: " + ex);
+ logger.trace(ex);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean validateUsingKeySelector(Node signatureNode, KeySelector validationKeySelector) throws XMLSignatureException, MarshalException {
+ DOMValidateContext valContext = new DOMValidateContext(validationKeySelector, signatureNode);
+ XMLSignature signature = fac.unmarshalXMLSignature(valContext);
+ boolean coreValidity = signature.validate(valContext);
+
+ if (! coreValidity) {
+ if (logger.isTraceEnabled()) {
+ boolean sv = signature.getSignatureValue().validate(valContext);
+ logger.trace("Signature validation status: " + sv);
+
+ List<Reference> references = signature.getSignedInfo().getReferences();
+ for (Reference ref : references) {
+ logger.trace("[Ref id=" + ref.getId() + ":uri=" + ref.getURI() + "]validity status:" + ref.validate(valContext));
+ }
+ }
+ }
+
+ return coreValidity;
+ }
+
/**
* Marshall a SignatureType to output stream
*
@@ -594,7 +694,7 @@ public class XMLSignatureUtil {
throw logger.unsupportedType(key.toString());
}
- private static void signImpl(DOMSignContext dsc, String digestMethod, String signatureMethod, String referenceURI, PublicKey publicKey,
+ private static void signImpl(DOMSignContext dsc, String digestMethod, String signatureMethod, String referenceURI, String keyId, PublicKey publicKey,
X509Certificate x509Certificate, String canonicalizationMethodType)
throws GeneralSecurityException, MarshalException, XMLSignatureException {
dsc.setDefaultNamespacePrefix("dsig");
@@ -603,7 +703,7 @@ public class XMLSignatureUtil {
Transform transform1 = fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null);
Transform transform2 = fac.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", (TransformParameterSpec) null);
- List<Transform> transformList = new ArrayList<Transform>();
+ List<Transform> transformList = new ArrayList<>();
transformList.add(transform1);
transformList.add(transform2);
@@ -616,37 +716,34 @@ public class XMLSignatureUtil {
SignatureMethod signatureMethodObj = fac.newSignatureMethod(signatureMethod, null);
SignedInfo si = fac.newSignedInfo(canonicalizationMethod, signatureMethodObj, referenceList);
- KeyInfo ki = null;
+ KeyInfo ki;
if (includeKeyInfoInSignature) {
- ki = createKeyInfo(publicKey, x509Certificate);
+ ki = createKeyInfo(keyId, publicKey, x509Certificate);
+ } else {
+ ki = createKeyInfo(keyId, null, null);
}
XMLSignature signature = fac.newXMLSignature(si, ki);
signature.sign(dsc);
}
- private static KeyInfo createKeyInfo(PublicKey publicKey, X509Certificate x509Certificate) throws KeyException {
+ private static KeyInfo createKeyInfo(String keyId, PublicKey publicKey, X509Certificate x509Certificate) throws KeyException {
KeyInfoFactory keyInfoFactory = fac.getKeyInfoFactory();
- KeyInfo keyInfo = null;
- KeyValue keyValue = null;
- //Just with public key
- if (publicKey != null) {
- keyValue = keyInfoFactory.newKeyValue(publicKey);
- keyInfo = keyInfoFactory.newKeyInfo(Collections.singletonList(keyValue));
+
+ List<Object> items = new LinkedList<>();
+
+ if (keyId != null) {
+ items.add(keyInfoFactory.newKeyName(keyId));
}
- if (x509Certificate != null) {
- List x509list = new ArrayList();
- x509list.add(x509Certificate);
- X509Data x509Data = keyInfoFactory.newX509Data(x509list);
- List items = new ArrayList();
+ if (x509Certificate != null) {
+ items.add(keyInfoFactory.newX509Data(Collections.singletonList(x509Certificate)));
+ }
- items.add(x509Data);
- if (keyValue != null) {
- items.add(keyValue);
- }
- keyInfo = keyInfoFactory.newKeyInfo(items);
+ if (publicKey != null) {
+ items.add(keyInfoFactory.newKeyValue(publicKey));
}
- return keyInfo;
+
+ return keyInfoFactory.newKeyInfo(items);
}
}
\ No newline at end of file
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
index 302237e..ec4fc28 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
@@ -25,15 +25,19 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.w3c.dom.Document;
import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
/**
* @author pedroigor
*/
-public class SAML2AuthnRequestBuilder {
+public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2AuthnRequestBuilder> {
private final AuthnRequestType authnRequestType;
protected String destination;
protected String issuer;
+ protected final List<NodeGenerator> extensions = new LinkedList<>();
public SAML2AuthnRequestBuilder destination(String destination) {
this.destination = destination;
@@ -45,6 +49,12 @@ public class SAML2AuthnRequestBuilder {
return this;
}
+ @Override
+ public SAML2AuthnRequestBuilder addExtension(NodeGenerator extension) {
+ this.extensions.add(extension);
+ return this;
+ }
+
public SAML2AuthnRequestBuilder() {
try {
this.authnRequestType = new AuthnRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
@@ -90,6 +100,14 @@ public class SAML2AuthnRequestBuilder {
authnRequestType.setDestination(URI.create(this.destination));
+ if (! this.extensions.isEmpty()) {
+ ExtensionsType extensionsType = new ExtensionsType();
+ for (NodeGenerator extension : this.extensions) {
+ extensionsType.addExtension(extension);
+ }
+ authnRequestType.setExtensions(extensionsType);
+ }
+
return new SAML2Request().convert(authnRequestType);
} catch (Exception e) {
throw new RuntimeException("Could not convert " + authnRequestType + " to a document.", e);
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java
index 99d1c1f..6da6799 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java
@@ -17,7 +17,10 @@
package org.keycloak.saml;
+import java.util.LinkedList;
+import java.util.List;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
@@ -32,11 +35,12 @@ import org.w3c.dom.Document;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class SAML2ErrorResponseBuilder {
+public class SAML2ErrorResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2ErrorResponseBuilder> {
protected String status;
protected String destination;
protected String issuer;
+ protected final List<NodeGenerator> extensions = new LinkedList<>();
public SAML2ErrorResponseBuilder status(String status) {
this.status = status;
@@ -53,6 +57,11 @@ public class SAML2ErrorResponseBuilder {
return this;
}
+ @Override
+ public SAML2ErrorResponseBuilder addExtension(NodeGenerator extension) {
+ this.extensions.add(extension);
+ return this;
+ }
public Document buildDocument() throws ProcessingException {
@@ -66,6 +75,14 @@ public class SAML2ErrorResponseBuilder {
statusResponse.setIssuer(issuer);
statusResponse.setDestination(destination);
+ if (! this.extensions.isEmpty()) {
+ ExtensionsType extensionsType = new ExtensionsType();
+ for (NodeGenerator extension : this.extensions) {
+ extensionsType.addExtension(extension);
+ }
+ statusResponse.setExtensions(extensionsType);
+ }
+
SAML2Response saml2Response = new SAML2Response();
return saml2Response.convert(statusResponse);
} catch (ConfigurationException e) {
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java
index 2386edb..17dafc7 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2LoginResponseBuilder.java
@@ -39,6 +39,9 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.w3c.dom.Document;
import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
import static org.keycloak.saml.common.util.StringUtil.isNotNull;
@@ -49,7 +52,7 @@ import static org.keycloak.saml.common.util.StringUtil.isNotNull;
*
* @author bburke@redhat.com
*/
-public class SAML2LoginResponseBuilder {
+public class SAML2LoginResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LoginResponseBuilder> {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
protected String destination;
@@ -64,6 +67,7 @@ public class SAML2LoginResponseBuilder {
protected String authMethod;
protected String requestIssuer;
protected String sessionIndex;
+ protected final List<NodeGenerator> extensions = new LinkedList<>();
public SAML2LoginResponseBuilder sessionIndex(String sessionIndex) {
@@ -136,6 +140,12 @@ public class SAML2LoginResponseBuilder {
return this;
}
+ @Override
+ public SAML2LoginResponseBuilder addExtension(NodeGenerator extension) {
+ this.extensions.add(extension);
+ return this;
+ }
+
public Document buildDocument(ResponseType responseType) throws ConfigurationException, ProcessingException {
Document samlResponseDocument = null;
@@ -207,6 +217,14 @@ public class SAML2LoginResponseBuilder {
assertion.addStatement(authnStatement);
}
+ if (! this.extensions.isEmpty()) {
+ ExtensionsType extensionsType = new ExtensionsType();
+ for (NodeGenerator extension : this.extensions) {
+ extensionsType.addExtension(extension);
+ }
+ responseType.setExtensions(extensionsType);
+ }
+
return responseType;
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java
index 99b1cf8..d0e81ba 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutRequestBuilder.java
@@ -27,18 +27,22 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.w3c.dom.Document;
import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class SAML2LogoutRequestBuilder {
+public class SAML2LogoutRequestBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LogoutRequestBuilder> {
protected String userPrincipal;
protected String userPrincipalFormat;
protected String sessionIndex;
protected long assertionExpiration;
protected String destination;
protected String issuer;
+ protected final List<NodeGenerator> extensions = new LinkedList<>();
public SAML2LogoutRequestBuilder destination(String destination) {
this.destination = destination;
@@ -50,6 +54,12 @@ public class SAML2LogoutRequestBuilder {
return this;
}
+ @Override
+ public SAML2LogoutRequestBuilder addExtension(NodeGenerator extension) {
+ this.extensions.add(extension);
+ return this;
+ }
+
/**
* Length of time in seconds the assertion is valid for
* See SAML core specification 2.5.1.2 NotOnOrAfter
@@ -99,6 +109,15 @@ public class SAML2LogoutRequestBuilder {
if (assertionExpiration > 0) lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionExpiration * 1000));
lort.setDestination(URI.create(destination));
+
+ if (! this.extensions.isEmpty()) {
+ ExtensionsType extensionsType = new ExtensionsType();
+ for (NodeGenerator extension : this.extensions) {
+ extensionsType.addExtension(extension);
+ }
+ lort.setExtensions(extensionsType);
+ }
+
return lort;
}
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutResponseBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutResponseBuilder.java
index c00a4d4..8050e81 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutResponseBuilder.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAML2LogoutResponseBuilder.java
@@ -31,16 +31,20 @@ import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.w3c.dom.Document;
import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class SAML2LogoutResponseBuilder {
+public class SAML2LogoutResponseBuilder implements SamlProtocolExtensionsAwareBuilder<SAML2LogoutResponseBuilder> {
protected String logoutRequestID;
protected String destination;
protected String issuer;
+ protected final List<NodeGenerator> extensions = new LinkedList<>();
public SAML2LogoutResponseBuilder logoutRequestID(String logoutRequestID) {
this.logoutRequestID = logoutRequestID;
@@ -57,6 +61,11 @@ public class SAML2LogoutResponseBuilder {
return this;
}
+ @Override
+ public SAML2LogoutResponseBuilder addExtension(NodeGenerator extension) {
+ this.extensions.add(extension);
+ return this;
+ }
public Document buildDocument() throws ProcessingException {
Document samlResponse = null;
@@ -77,6 +86,14 @@ public class SAML2LogoutResponseBuilder {
statusResponse.setIssuer(issuer);
statusResponse.setDestination(destination);
+ if (! this.extensions.isEmpty()) {
+ ExtensionsType extensionsType = new ExtensionsType();
+ for (NodeGenerator extension : this.extensions) {
+ extensionsType.addExtension(extension);
+ }
+ statusResponse.setExtensions(extensionsType);
+ }
+
SAML2Response saml2Response = new SAML2Response();
samlResponse = saml2Response.convert(statusResponse);
} catch (ConfigurationException e) {
diff --git a/saml-core/src/main/java/org/keycloak/saml/SamlProtocolExtensionsAwareBuilder.java b/saml-core/src/main/java/org/keycloak/saml/SamlProtocolExtensionsAwareBuilder.java
new file mode 100644
index 0000000..2192df6
--- /dev/null
+++ b/saml-core/src/main/java/org/keycloak/saml/SamlProtocolExtensionsAwareBuilder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.saml;
+
+import javax.xml.stream.XMLStreamWriter;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+
+/**
+ * Implementations of this interface are builders that can register <samlp:Extensions>
+ * content providers.
+ *
+ * @author hmlnarik
+ */
+public interface SamlProtocolExtensionsAwareBuilder<T> {
+
+ public interface NodeGenerator {
+ /**
+ * Generate contents of the <samlp:Extensions> tag. When this method is invoked,
+ * the writer has already emitted the <samlp:Extensions> start tag.
+ *
+ * @param writer Writer to use for producing XML output
+ * @throws ProcessingException If any exception fails
+ */
+ void write(XMLStreamWriter writer) throws ProcessingException;
+ }
+
+ /**
+ * Adds a given node subtree as a SAML protocol extension into the SAML protocol message.
+ *
+ * @param extension
+ * @return
+ */
+ T addExtension(NodeGenerator extension);
+}
diff --git a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
index e6c10af..9a28137 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
@@ -22,21 +22,14 @@ package org.keycloak.saml;
* @version $Revision: 1 $
*/
public class SPMetadataDescriptor {
- public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String certificatePem) {
+
+ public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String signingCerts) {
String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\n" +
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n";
- if (wantAuthnRequestsSigned) {
- descriptor +=
- " <KeyDescriptor use=\"signing\">\n" +
- " <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
- " <dsig:X509Data>\n" +
- " <dsig:X509Certificate>\n" + certificatePem + "\n" +
- " </dsig:X509Certificate>\n" +
- " </dsig:X509Data>\n" +
- " </dsig:KeyInfo>\n" +
- " </KeyDescriptor>\n";
+ if (wantAuthnRequestsSigned && signingCerts != null) {
+ descriptor += signingCerts;
}
descriptor +=
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
@@ -44,10 +37,34 @@ public class SPMetadataDescriptor {
" </NameIDFormat>\n" +
" <AssertionConsumerService\n" +
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
- " index=\"1\" isDefault=\"true\" />\n";
- descriptor +=
+ " index=\"1\" isDefault=\"true\" />\n" +
" </SPSSODescriptor>\n" +
"</EntityDescriptor>\n";
return descriptor;
}
+
+ public static String xmlKeyInfo(String indentation, String keyId, String pemEncodedCertificate, String purpose, boolean declareDSigNamespace) {
+ if (pemEncodedCertificate == null) {
+ return "";
+ }
+
+ StringBuilder target = new StringBuilder()
+ .append(indentation).append("<KeyDescriptor use=\"").append(purpose).append("\">\n")
+ .append(indentation).append(" <dsig:KeyInfo").append(declareDSigNamespace ? " xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" : ">\n");
+
+ if (keyId != null) {
+ target.append(indentation).append(" <dsig:KeyName>").append(keyId).append("</dsig:KeyName>\n");
+ }
+
+ target
+ .append(indentation).append(" <dsig:X509Data>\n")
+ .append(indentation).append(" <dsig:X509Certificate>").append(pemEncodedCertificate).append("</dsig:X509Certificate>\n")
+ .append(indentation).append(" </dsig:X509Data>\n")
+ .append(indentation).append(" </dsig:KeyInfo>\n")
+ .append(indentation).append("</KeyDescriptor>\n")
+ ;
+
+ return target.toString();
+ }
+
}
diff --git a/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java b/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java
new file mode 100644
index 0000000..ad150e9
--- /dev/null
+++ b/saml-core/src/test/java/org/keycloak/saml/processing/core/parsers/saml/SAMLParserTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.saml.processing.core.parsers.saml;
+
+import java.io.InputStream;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import static org.hamcrest.CoreMatchers.*;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.w3c.dom.Element;
+
+/**
+ * Test class for SAML parser.
+ *
+ * TODO: Add further tests.
+ *
+ * @author hmlnarik
+ */
+public class SAMLParserTest {
+
+ @Test
+ public void testSaml20EncryptedAssertionsSignedReceivedWithRedirectBinding() throws Exception {
+ InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response.xml");
+ SAMLParser parser = new SAMLParser();
+
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType resp = (ResponseType) parsedObject;
+ assertThat(resp.getSignature(), nullValue());
+ assertThat(resp.getConsent(), nullValue());
+ assertThat(resp.getIssuer(), not(nullValue()));
+ assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
+
+ assertThat(resp.getExtensions(), not(nullValue()));
+ assertThat(resp.getExtensions().getAny().size(), is(1));
+ assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
+ Element el = (Element) resp.getExtensions().getAny().get(0);
+ assertThat(el.getLocalName(), is("KeyInfo"));
+ assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
+ assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
+ assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
+
+ assertThat(resp.getAssertions(), not(nullValue()));
+ assertThat(resp.getAssertions().size(), is(1));
+ }
+
+ @Test
+ public void testSaml20EncryptedAssertionsSignedTwoExtensionsReceivedWithRedirectBinding() throws Exception {
+ Element el;
+
+ InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-encrypted-signed-redirect-response-two-extensions.xml");
+ SAMLParser parser = new SAMLParser();
+
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(ResponseType.class));
+
+ ResponseType resp = (ResponseType) parsedObject;
+ assertThat(resp.getSignature(), nullValue());
+ assertThat(resp.getConsent(), nullValue());
+ assertThat(resp.getIssuer(), not(nullValue()));
+ assertThat(resp.getIssuer().getValue(), is("http://localhost:8081/auth/realms/saml-demo"));
+
+ assertThat(resp.getExtensions(), not(nullValue()));
+ assertThat(resp.getExtensions().getAny().size(), is(2));
+ assertThat(resp.getExtensions().getAny().get(0), instanceOf(Element.class));
+ el = (Element) resp.getExtensions().getAny().get(0);
+ assertThat(el.getLocalName(), is("KeyInfo"));
+ assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:key:1.0"));
+ assertThat(el.hasAttribute("MessageSigningKeyId"), is(true));
+ assertThat(el.getAttribute("MessageSigningKeyId"), is("FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"));
+ assertThat(resp.getExtensions().getAny().get(1), instanceOf(Element.class));
+ el = (Element) resp.getExtensions().getAny().get(1);
+ assertThat(el.getLocalName(), is("ever"));
+ assertThat(el.getNamespaceURI(), is("urn:keycloak:ext:what:1.0"));
+ assertThat(el.hasAttribute("what"), is(true));
+ assertThat(el.getAttribute("what"), is("ever"));
+
+ assertThat(resp.getAssertions(), not(nullValue()));
+ assertThat(resp.getAssertions().size(), is(1));
+ }
+
+ @Test
+ public void testSaml20PostLogoutRequest() throws Exception {
+ InputStream st = SAMLParserTest.class.getResourceAsStream("saml20-signed-logout-request.xml");
+ SAMLParser parser = new SAMLParser();
+
+ Object parsedObject = parser.parse(st);
+ assertThat(parsedObject, instanceOf(LogoutRequestType.class));
+
+ }
+}
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response.xml
new file mode 100644
index 0000000..d8d4c15
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response.xml
@@ -0,0 +1,29 @@
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/sales-post-enc/saml" ID="ID_0b43d444-d1a8-44a5-8caf-38e176489e1f" InResponseTo="ID_223d3591-22fb-4b3c-9e38-4719293b2d94" IssueInstant="2016-11-01T13:52:43.054Z" Version="2.0">
+ <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
+ <samlp:Extensions>
+ <kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
+ </samlp:Extensions>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+ </samlp:Status>
+ <saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
+ <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
+ <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
+ <xenc:CipherData>
+ <xenc:CipherValue>
+ OkvZTx/ifYLef74rY0F9I8lbJaatgSEguo+zwh5JrYWcO09Ib2gtz5+z+67Is2+wk/OzKp154r8qAI5vY9AYvuXCslKL/wbcZ1UILL78F0T/iiUW3VpWy8Wvz5nezBFPRqot8WiFQykByjlBg1Z8XOts+uIdyqBBi/WjYeJGMaQ=
+ </xenc:CipherValue>
+ </xenc:CipherData>
+ </xenc:EncryptedKey>
+ </ds:KeyInfo>
+ <xenc:CipherData>
+ <xenc:CipherValue>
+ RW2eu9nP2Ez9hfRlug9xC+kFfVF3HZpEb4kIFH33gmVbzrQjPk0l67uXkwRjC82FZZ482QnHCBIqNFlAryds/zTa6wdRvFmhQnIM6WxoAl8TM+e9h8MoKkalMc8J/Qfp+WQ7/XdmCg2pp9VvUZTK+g0+G4aGuL+S5+ssZq4rl9k7LrSYyp6vj+djgvISZiz5hPYJCN/WY/gWXfVuLHSpu4CmZt8D2APtT3ax1WmGcuzStAfTW8q3MFIDNV59hkpFmDb+gvyLNbZ95cDYxofiPXaC5cOTftnSBp68Ay1eienqdttEDo4fyakszdvq128KwXkH9azCg6sqLxli6B8l2xdq41MeuJO54VqmOhhLxwKy42NtnJvK/NkNwttH4yMwDPpPbC4vOKCXxT2r2F7jjvJQNB2VFv+oiUAWSSc3fGQcc2uNlx9YQVuzTmjqc7fXAWCGgYoogC8AeNWni204bnBoVpFrEo3gzuOe2fFsddJIclglmTH1hWf31FXUHDO2nl/lT4puQVTo+I+d6jpiV+qdp823NDntRxljRlUJO2AzSTXuIIGtF5q5KWyEi9Nj93BCWa1Llcddkn3ZEZMvDwR4MacwUj8G8hwoH73VvT3jAiakjSpNEIqYCzofeejdfN/gEuuAUfe8uNbTu+gBS+iP3QJe3Pc0Fs/lKJzd3frPNj7xb83wpOf865EQQoOozhnRIKKcMReSjakr/Px5NNooeiJcWEreDagQO2TbwTnHg1kCNG3BAXV/2lV3XBU4afZBoUfxAzYWFOl6xFCAPzhQCPL1SFJp1VRADY/1MU2Kaje5AZoJ4jjph8+yspxBvjic1vC1uYRGW8LWRind9w4eVhCm0LfPiFRCpP+jKPQOJzcNH580/nIMFXPHHnLKv/It7Qex1unDv/QjkuCFFHR6SWJm4WBrwDek+MyOIvgT6o878Cu0Ps472QpoYBQ+7l2WoylWdG1lHZV1UiHPj7PLHPNAL4rbbN3U88fS6N9OJHegQTfcX0i/1KPk4IN/5Z+/15dHI658BINjRvI/6O1QqaTVZkqM8ORcoGpn6BjAiz5rRhjWpOCwlmT+VzOAp3IqACURS1X+txjWE2mfVjlHLJsvyGRDLv1dUR3IeStDAEfsjR/ruRgn5XTFpYaccB/u//DJonJr5A+KFiLbYl+sbbSVAoQCAiAdxKdUpKPx7C473UJ2nYQGby5H5xwboa0Uj0SnJLYWdQ0jvVvzWpWFVWATc4UqnaxdoUDAmewrM6cSSIAmQBB34orCunFbriK9Z4efZ7gB9erQ1fpi3z/IjQBoTEpOUUIPW/qMAApIDPVM6UV9PumW7RL9zKEP5PuWJoGGnKbWGP/b9G4vMFiWMaSNHBYYMI6OLH4WJ3E+4QBGh2vjjfQ0gobhaLgIerIwCQFYEdl9KddAjaflUEFXal9fIQ8Bz9L3rDhQE5AGBZL6ULZmJe3GnkN6Cc+UWAGyD5zv2rsCG2lvR5ox4UE2mFi6nBJbC5Vj5m9Sz1l0QpRwUkH2kD2QQ5iV6nNmQOcU/mz7ulxluf8+FBJJimYVqK8UkJ6+W6j8Eft9Q8fTpEuEVLxqTWGgOAEUBf87RWDU+iF3A+AxFGsJLc5RC+5BKNTEDlV2qDCjHT7b5wqBKJ3FHulOih9EenlZiI51m6kg5yyxnMdbhasvSh6Az8Mp/4lFo/wSA/mXxNhBrEEmRhFiIE5yYUEYIj5F8fH+93tIuWQqyhXIwCntEOdSSmoei9EYFzj8deXcEzVf8y/N6HQErZcJjyg34caOsfRcJYoxEiCm4icA/btWhdjUNT02B20qnxGFndO4CRUQlyDqTbyVD8LRLK9/95L9+5v9zojLle8xQe30dsxKn7r9TTJH8QQai5iam9lU1ik50lwTKpZb18k4rNdO5cnnYoHzCXeCg38YZxyFt9G7um/MxlID5Qd5Ywq6thDzL7WxvanKeRhCuJ2MTVV0EoJxZKIj9Yv0Ars9mZHkoHoP0ikcW8d5ciDj1Onnbj+XDcYI3FZj0Y2vToZvYi/7eLWi8EnSjaIQrr/AHnrmZK1w3Uicd691U6r3Y0UdnzQEl4Ub/l1uhSaGAg2oEdDxkOdZ3Frvf/C4nTEBmunPlNvnJjVFssdeVVXKLBOZ5eRiJjasHUKnTeJVwolvd/dBI+ypfw1+5ae/0upxd9/gV1lbwX9N2yOwqbxz24cKXZWvOFBAGc3+gQFu8RrF6NAeQ96PlkuRsiNOKPPtJT3JNrLGvVKY8g==
+ </xenc:CipherValue>
+ </xenc:CipherData>
+ </xenc:EncryptedData>
+ </saml:EncryptedAssertion>
+</samlp:Response>
\ No newline at end of file
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
new file mode 100644
index 0000000..94a6fdb
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
@@ -0,0 +1,30 @@
+<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8080/sales-post-enc/saml" ID="ID_0b43d444-d1a8-44a5-8caf-38e176489e1f" InResponseTo="ID_223d3591-22fb-4b3c-9e38-4719293b2d94" IssueInstant="2016-11-01T13:52:43.054Z" Version="2.0">
+ <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
+ <samlp:Extensions>
+ <kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
+ <what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever"/>
+ </samlp:Extensions>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+ </samlp:Status>
+ <saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
+ <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+ <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+ <xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
+ <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
+ <xenc:CipherData>
+ <xenc:CipherValue>
+ OkvZTx/ifYLef74rY0F9I8lbJaatgSEguo+zwh5JrYWcO09Ib2gtz5+z+67Is2+wk/OzKp154r8qAI5vY9AYvuXCslKL/wbcZ1UILL78F0T/iiUW3VpWy8Wvz5nezBFPRqot8WiFQykByjlBg1Z8XOts+uIdyqBBi/WjYeJGMaQ=
+ </xenc:CipherValue>
+ </xenc:CipherData>
+ </xenc:EncryptedKey>
+ </ds:KeyInfo>
+ <xenc:CipherData>
+ <xenc:CipherValue>
+ RW2eu9nP2Ez9hfRlug9xC+kFfVF3HZpEb4kIFH33gmVbzrQjPk0l67uXkwRjC82FZZ482QnHCBIqNFlAryds/zTa6wdRvFmhQnIM6WxoAl8TM+e9h8MoKkalMc8J/Qfp+WQ7/XdmCg2pp9VvUZTK+g0+G4aGuL+S5+ssZq4rl9k7LrSYyp6vj+djgvISZiz5hPYJCN/WY/gWXfVuLHSpu4CmZt8D2APtT3ax1WmGcuzStAfTW8q3MFIDNV59hkpFmDb+gvyLNbZ95cDYxofiPXaC5cOTftnSBp68Ay1eienqdttEDo4fyakszdvq128KwXkH9azCg6sqLxli6B8l2xdq41MeuJO54VqmOhhLxwKy42NtnJvK/NkNwttH4yMwDPpPbC4vOKCXxT2r2F7jjvJQNB2VFv+oiUAWSSc3fGQcc2uNlx9YQVuzTmjqc7fXAWCGgYoogC8AeNWni204bnBoVpFrEo3gzuOe2fFsddJIclglmTH1hWf31FXUHDO2nl/lT4puQVTo+I+d6jpiV+qdp823NDntRxljRlUJO2AzSTXuIIGtF5q5KWyEi9Nj93BCWa1Llcddkn3ZEZMvDwR4MacwUj8G8hwoH73VvT3jAiakjSpNEIqYCzofeejdfN/gEuuAUfe8uNbTu+gBS+iP3QJe3Pc0Fs/lKJzd3frPNj7xb83wpOf865EQQoOozhnRIKKcMReSjakr/Px5NNooeiJcWEreDagQO2TbwTnHg1kCNG3BAXV/2lV3XBU4afZBoUfxAzYWFOl6xFCAPzhQCPL1SFJp1VRADY/1MU2Kaje5AZoJ4jjph8+yspxBvjic1vC1uYRGW8LWRind9w4eVhCm0LfPiFRCpP+jKPQOJzcNH580/nIMFXPHHnLKv/It7Qex1unDv/QjkuCFFHR6SWJm4WBrwDek+MyOIvgT6o878Cu0Ps472QpoYBQ+7l2WoylWdG1lHZV1UiHPj7PLHPNAL4rbbN3U88fS6N9OJHegQTfcX0i/1KPk4IN/5Z+/15dHI658BINjRvI/6O1QqaTVZkqM8ORcoGpn6BjAiz5rRhjWpOCwlmT+VzOAp3IqACURS1X+txjWE2mfVjlHLJsvyGRDLv1dUR3IeStDAEfsjR/ruRgn5XTFpYaccB/u//DJonJr5A+KFiLbYl+sbbSVAoQCAiAdxKdUpKPx7C473UJ2nYQGby5H5xwboa0Uj0SnJLYWdQ0jvVvzWpWFVWATc4UqnaxdoUDAmewrM6cSSIAmQBB34orCunFbriK9Z4efZ7gB9erQ1fpi3z/IjQBoTEpOUUIPW/qMAApIDPVM6UV9PumW7RL9zKEP5PuWJoGGnKbWGP/b9G4vMFiWMaSNHBYYMI6OLH4WJ3E+4QBGh2vjjfQ0gobhaLgIerIwCQFYEdl9KddAjaflUEFXal9fIQ8Bz9L3rDhQE5AGBZL6ULZmJe3GnkN6Cc+UWAGyD5zv2rsCG2lvR5ox4UE2mFi6nBJbC5Vj5m9Sz1l0QpRwUkH2kD2QQ5iV6nNmQOcU/mz7ulxluf8+FBJJimYVqK8UkJ6+W6j8Eft9Q8fTpEuEVLxqTWGgOAEUBf87RWDU+iF3A+AxFGsJLc5RC+5BKNTEDlV2qDCjHT7b5wqBKJ3FHulOih9EenlZiI51m6kg5yyxnMdbhasvSh6Az8Mp/4lFo/wSA/mXxNhBrEEmRhFiIE5yYUEYIj5F8fH+93tIuWQqyhXIwCntEOdSSmoei9EYFzj8deXcEzVf8y/N6HQErZcJjyg34caOsfRcJYoxEiCm4icA/btWhdjUNT02B20qnxGFndO4CRUQlyDqTbyVD8LRLK9/95L9+5v9zojLle8xQe30dsxKn7r9TTJH8QQai5iam9lU1ik50lwTKpZb18k4rNdO5cnnYoHzCXeCg38YZxyFt9G7um/MxlID5Qd5Ywq6thDzL7WxvanKeRhCuJ2MTVV0EoJxZKIj9Yv0Ars9mZHkoHoP0ikcW8d5ciDj1Onnbj+XDcYI3FZj0Y2vToZvYi/7eLWi8EnSjaIQrr/AHnrmZK1w3Uicd691U6r3Y0UdnzQEl4Ub/l1uhSaGAg2oEdDxkOdZ3Frvf/C4nTEBmunPlNvnJjVFssdeVVXKLBOZ5eRiJjasHUKnTeJVwolvd/dBI+ypfw1+5ae/0upxd9/gV1lbwX9N2yOwqbxz24cKXZWvOFBAGc3+gQFu8RrF6NAeQ96PlkuRsiNOKPPtJT3JNrLGvVKY8g==
+ </xenc:CipherValue>
+ </xenc:CipherData>
+ </xenc:EncryptedData>
+ </saml:EncryptedAssertion>
+</samlp:Response>
\ No newline at end of file
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-signed-logout-request.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-signed-logout-request.xml
new file mode 100644
index 0000000..8c4ab20
--- /dev/null
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-signed-logout-request.xml
@@ -0,0 +1,32 @@
+<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Destination="http://localhost:8081/auth/realms/saml-demo/protocol/saml" ID="ID_4790c6a3-4b9f-4c0a-a368-5c0e498544e4" IssueInstant="2016-11-01T14:36:43.194Z" Version="2.0">
+ <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8080/sales-post-enc/</saml:Issuer>
+ <dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+ <dsig:SignedInfo>
+ <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ <dsig:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+ <dsig:Reference URI="#ID_4790c6a3-4b9f-4c0a-a368-5c0e498544e4">
+ <dsig:Transforms>
+ <dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+ <dsig:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+ </dsig:Transforms>
+ <dsig:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <dsig:DigestValue>zeWNo5eav5tFOOCEJ1YU9eINkPnBSfixzAr8AOC4R4c=</dsig:DigestValue>
+ </dsig:Reference>
+ </dsig:SignedInfo>
+ <dsig:SignatureValue>
+ pyOiS1LsV/XR08zhcN6IqSYuKTDln4otmCvZxCc07ORP1C9jragu8V8rEE09qt/zBcdw7Arb8eLNNC6oCnrnMxuvzRInVTwt7T5K3t0UlzRWOb3HMElhcWFEgDzh6uKw5Cr45A01XNpojtJWCML/qU2Enyyy80FBlCJNcbzyLxE=
+ </dsig:SignatureValue>
+ <dsig:KeyInfo>
+ <dsig:KeyValue>
+ <dsig:RSAKeyValue>
+ <dsig:Modulus>
+ 2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEik=
+ </dsig:Modulus>
+ <dsig:Exponent>AQAB</dsig:Exponent>
+ </dsig:RSAKeyValue>
+ </dsig:KeyValue>
+ </dsig:KeyInfo>
+ </dsig:Signature>
+ <saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">bburke</saml:NameID>
+ <samlp:SessionIndex>a3b2df1c-1095-487b-8b56-f62818c449e3</samlp:SessionIndex>
+</samlp:LogoutRequest>
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 997bc93..0ef5276 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -73,9 +73,13 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
-import java.security.PublicKey;
+import java.security.Key;
import java.security.cert.X509Certificate;
+import java.util.LinkedList;
import java.util.List;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -174,14 +178,20 @@ public class SAMLEndpoint {
protected abstract void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException;
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
- protected PublicKey getIDPKey() {
- X509Certificate certificate = null;
- try {
- certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", ""));
- } catch (ProcessingException e) {
- throw new RuntimeException(e);
+
+ protected KeyLocator getIDPKeyLocator() {
+ List<Key> keys = new LinkedList<>();
+
+ for (String signingCertificate : config.getSigningCertificates()) {
+ try {
+ X509Certificate cert = XMLSignatureUtil.getX509CertificateFromKeyInfoString(signingCertificate.replaceAll("\\s", ""));
+ keys.add(cert.getPublicKey());
+ } catch (ProcessingException e) {
+ throw new RuntimeException(e);
+ }
}
- return certificate.getPublicKey();
+
+ return new HardcodedKeyLocator(keys);
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
@@ -265,14 +275,18 @@ public class SAMLEndpoint {
builder.issuer(issuerURL);
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
.relayState(relayState);
+ boolean postBinding = config.isPostBindingResponse();
if (config.isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
- binding.signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
+ binding.signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(provider.getSignatureAlgorithm())
.signDocument();
+ if (! postBinding && config.isAddExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
try {
- if (config.isPostBindingResponse()) {
+ if (postBinding) {
return binding.postBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
} else {
return binding.redirectBinding(builder.buildDocument()).response(config.getSingleLogoutServiceUrl());
@@ -418,7 +432,7 @@ public class SAMLEndpoint {
protected class PostBinding extends Binding {
@Override
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
- SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey());
+ SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKeyLocator());
}
@Override
@@ -440,8 +454,8 @@ public class SAMLEndpoint {
protected class RedirectBinding extends Binding {
@Override
protected void verifySignature(String key, SAMLDocumentHolder documentHolder) throws VerificationException {
- PublicKey publicKey = getIDPKey();
- SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, key);
+ KeyLocator locator = getIDPKeyLocator();
+ SamlProtocolUtils.verifyRedirectSignature(documentHolder, locator, uriInfo, key);
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index 104b8f8..f96f15a 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -50,8 +50,11 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
+import java.util.Set;
+import java.util.TreeSet;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.keys.KeyMetadata;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* @author Pedro Igor
@@ -97,18 +100,22 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
.relayState(request.getState());
+ boolean postBinding = getConfig().isPostBindingAuthnRequest();
if (getConfig().isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
KeyPair keypair = new KeyPair(keys.getPublicKey(), keys.getPrivateKey());
- binding.signWith(keypair);
+ binding.signWith(keys.getKid(), keypair);
binding.signatureAlgorithm(getSignatureAlgorithm());
binding.signDocument();
+ if (! postBinding && getConfig().isAddExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ authnRequestBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
- if (getConfig().isPostBindingAuthnRequest()) {
+ if (postBinding) {
return binding.postBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
} else {
return binding.redirectBinding(authnRequestBuilder.toDocument()).request(destinationUrl);
@@ -198,7 +205,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
.relayState(userSession.getId());
if (getConfig().isWantAuthnRequestsSigned()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
- binding.signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
+ binding.signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate())
.signatureAlgorithm(getSignatureAlgorithm())
.signDocument();
}
@@ -225,11 +232,27 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
String entityId = getEntityId(uriInfo, realm);
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
- String certificatePem = PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate());
- String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, certificatePem);
+
+ StringBuilder keysString = new StringBuilder();
+ Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
+ ? (int) (o2.getProviderPriority() - o1.getProviderPriority())
+ : (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
+ keys.addAll(session.keys().getKeys(realm, false));
+ for (KeyMetadata key : keys) {
+ addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
+ }
+ String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint, wantAuthnRequestsSigned, entityId, nameIDPolicyFormat, keysString.toString());
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
}
+ private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ if (key == null) {
+ return;
+ }
+
+ target.append(SPMetadataDescriptor.xmlKeyInfo(" ", key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, true));
+ }
+
public SignatureAlgorithm getSignatureAlgorithm() {
String alg = getConfig().getSignatureAlgorithm();
if (alg != null) {
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
index 1b2fb67..59b46ca 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java
@@ -62,14 +62,45 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put("forceAuthn", String.valueOf(forceAuthn));
}
+ /**
+ * @deprecated Prefer {@link #getSigningCertificates()}}
+ * @param signingCertificate
+ */
public String getSigningCertificate() {
- return getConfig().get("signingCertificate");
+ return getConfig().get(SIGNING_CERTIFICATE_KEY);
}
+ /**
+ * @deprecated Prefer {@link #addSigningCertificate(String)}}
+ * @param signingCertificate
+ */
public void setSigningCertificate(String signingCertificate) {
- getConfig().put("signingCertificate", signingCertificate);
+ getConfig().put(SIGNING_CERTIFICATE_KEY, signingCertificate);
}
+ public void addSigningCertificate(String signingCertificate) {
+ String crt = getConfig().get(SIGNING_CERTIFICATE_KEY);
+ if (crt == null || crt.isEmpty()) {
+ getConfig().put(SIGNING_CERTIFICATE_KEY, signingCertificate);
+ } else {
+ // Note that "," is not coding character per PEM format specification:
+ // see https://tools.ietf.org/html/rfc1421, section 4.3.2.4 Step 4: Printable Encoding
+ getConfig().put(SIGNING_CERTIFICATE_KEY, crt + "," + signingCertificate);
+ }
+ }
+
+ public String[] getSigningCertificates() {
+ String crt = getConfig().get(SIGNING_CERTIFICATE_KEY);
+ if (crt == null || crt.isEmpty()) {
+ return new String[] { };
+ }
+ // Note that "," is not coding character per PEM format specification:
+ // see https://tools.ietf.org/html/rfc1421, section 4.3.2.4 Step 4: Printable Encoding
+ return crt.split(",");
+ }
+
+ public static final String SIGNING_CERTIFICATE_KEY = "signingCertificate";
+
public String getNameIDPolicyFormat() {
return getConfig().get("nameIDPolicyFormat");
}
@@ -86,6 +117,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
}
+ public boolean isAddExtensionsElementWithKeyInfo() {
+ return Boolean.valueOf(getConfig().get("addExtensionsElementWithKeyInfo"));
+ }
+
+ public void setAddExtensionsElementWithKeyInfo(boolean addExtensionsElementWithKeyInfo) {
+ getConfig().put("addExtensionsElementWithKeyInfo", String.valueOf(addExtensionsElementWithKeyInfo));
+ }
+
public String getSignatureAlgorithm() {
return getConfig().get("signatureAlgorithm");
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
index 714c47e..0cc72da 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java
@@ -108,6 +108,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl);
samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl);
samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned());
+ samlIdentityProviderConfig.setAddExtensionsElementWithKeyInfo(false);
samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned());
samlIdentityProviderConfig.setPostBindingResponse(postBinding);
samlIdentityProviderConfig.setPostBindingAuthnRequest(postBinding);
@@ -121,7 +122,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
Element x509KeyInfo = DocumentUtil.getChildElement(keyInfo, new QName("dsig", "X509Certificate"));
if (KeyTypes.SIGNING.equals(keyDescriptorType.getUse())) {
- samlIdentityProviderConfig.setSigningCertificate(x509KeyInfo.getTextContent());
+ samlIdentityProviderConfig.addSigningCertificate(x509KeyInfo.getTextContent());
} else if (KeyTypes.ENCRYPTION.equals(keyDescriptorType.getUse())) {
samlIdentityProviderConfig.setEncryptionPublicKey(x509KeyInfo.getTextContent());
} else if (keyDescriptorType.getUse() == null) {
@@ -131,8 +132,8 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory
}
if (defaultCertificate != null) {
- if (samlIdentityProviderConfig.getSigningCertificate() == null) {
- samlIdentityProviderConfig.setSigningCertificate(defaultCertificate);
+ if (samlIdentityProviderConfig.getSigningCertificates().length == 0) {
+ samlIdentityProviderConfig.addSigningCertificate(defaultCertificate);
}
if (samlIdentityProviderConfig.getEncryptionPublicKey() == null) {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java
index 00caa11..cca12cb 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java
@@ -101,6 +101,7 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
app.setFullScopeAllowed(true);
app.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true
+ attributes.put(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT, SamlProtocol.ATTRIBUTE_FALSE_VALUE); // default to false
attributes.put(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString());
attributes.put(SamlConfigAttributes.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE);
SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java
index 2175b32..14166ce 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlClientInstallation.java
@@ -18,7 +18,6 @@
package org.keycloak.protocol.saml.installation;
import org.keycloak.Config;
-import org.keycloak.common.util.PemUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -42,14 +41,14 @@ public class KeycloakSamlClientInstallation implements ClientInstallationProvide
@Override
public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
SamlClient samlClient = new SamlClient(client);
- StringBuffer buffer = new StringBuffer();
+ StringBuilder buffer = new StringBuilder();
buffer.append("<keycloak-saml-adapter>\n");
baseXml(session, realm, client, baseUri, samlClient, buffer);
buffer.append("</keycloak-saml-adapter>\n");
return Response.ok(buffer.toString(), MediaType.TEXT_PLAIN_TYPE).build();
}
- public static void baseXml(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri, SamlClient samlClient, StringBuffer buffer) {
+ public static void baseXml(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri, SamlClient samlClient, StringBuilder buffer) {
buffer.append(" <SP entityID=\"").append(client.getClientId()).append("\"\n");
buffer.append(" sslPolicy=\"").append(realm.getSslRequired().name()).append("\"\n");
buffer.append(" logoutPage=\"SPECIFY YOUR LOGOUT PAGE!\">\n");
@@ -113,15 +112,6 @@ public class KeycloakSamlClientInstallation implements ClientInstallationProvide
buffer.append(" postBindingUrl=\"").append(bindingUrl).append("\"\n");
buffer.append(" redirectBindingUrl=\"").append(bindingUrl).append("\"");
buffer.append("/>\n");
- if (samlClient.requiresRealmSignature()) {
- buffer.append(" <Keys>\n");
- buffer.append(" <Key signing=\"true\">\n");
- buffer.append(" <CertificatePem>\n");
- buffer.append(" ").append(PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate())).append("\n");
- buffer.append(" </CertificatePem>\n");
- buffer.append(" </Key>\n");
- buffer.append(" </Keys>\n");
- }
buffer.append(" </IDP>\n");
buffer.append(" </SP>\n");
}
@@ -138,7 +128,7 @@ public class KeycloakSamlClientInstallation implements ClientInstallationProvide
@Override
public String getHelpText() {
- return "Keycloak SAML adapter configuration file. Put this in WEB-INF directory if your WAR.";
+ return "Keycloak SAML adapter configuration file. Put this in WEB-INF directory of your WAR.";
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlSubsystemInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlSubsystemInstallation.java
index ea77d47..bde0ccd 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlSubsystemInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/KeycloakSamlSubsystemInstallation.java
@@ -39,7 +39,7 @@ public class KeycloakSamlSubsystemInstallation implements ClientInstallationProv
@Override
public Response generateInstallation(KeycloakSession session, RealmModel realm, ClientModel client, URI baseUri) {
SamlClient samlClient = new SamlClient(client);
- StringBuffer buffer = new StringBuffer();
+ StringBuilder buffer = new StringBuilder();
buffer.append("<secure-deployment name=\"YOUR-WAR.war\">\n");
KeycloakSamlClientInstallation.baseXml(session, realm, client, baseUri, samlClient, buffer);
buffer.append("</secure-deployment>\n");
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
index 4b84363..3c451b3 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlIDPDescriptorClientInstallation.java
@@ -32,6 +32,11 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
+import java.util.Set;
+import java.util.TreeSet;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.keys.KeyMetadata;
+import org.keycloak.saml.SPMetadataDescriptor;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -41,49 +46,61 @@ public class SamlIDPDescriptorClientInstallation implements ClientInstallationPr
public static String getIDPDescriptorForClient(KeycloakSession session, RealmModel realm, ClientModel client, URI serverBaseUri) {
SamlClient samlClient = new SamlClient(client);
String idpEntityId = RealmsResource.realmBaseUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName()).toString();
- String idp = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
- "<EntityDescriptor entityID=\"" + idpEntityId + "\"\n" +
- " xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n" +
- " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
- " <IDPSSODescriptor WantAuthnRequestsSigned=\"" + Boolean.toString(samlClient.requiresClientSignature()) + "\"\n" +
- " protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n";
+ StringBuilder sb = new StringBuilder();
+ sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<EntityDescriptor entityID=\"").append(idpEntityId).append("\"\n"
+ + " xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\"\n"
+ + " xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\"\n"
+ + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"
+ + " <IDPSSODescriptor WantAuthnRequestsSigned=\"")
+ .append(samlClient.requiresClientSignature())
+ .append("\"\n"
+ + " protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n");
if (samlClient.forceNameIDFormat() && samlClient.getNameIDFormat() != null) {
- idp += " <NameIDFormat>" + samlClient.getNameIDFormat() + "</NameIDFormat>\n";
+ sb.append(" <NameIDFormat>").append(samlClient.getNameIDFormat()).append("</NameIDFormat>\n");
} else {
- idp += " <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>\n" +
- " <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n" +
- " <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>\n" +
- " <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>\n";
+ sb.append(" <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>\n"
+ + " <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>\n"
+ + " <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>\n"
+ + " <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>\n");
}
String bindUrl = RealmsResource.protocolUrl(UriBuilder.fromUri(serverBaseUri)).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString();
- idp += "\n" +
- " <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
- " Location=\"" + bindUrl + "\" />\n";
- if (!samlClient.forcePostBinding()) {
- idp += " <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n" +
- " Location=\"" + bindUrl + "\" />\n";
+ sb.append("\n"
+ + " <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n"
+ + " Location=\"").append(bindUrl).append("\" />\n");
+ if (! samlClient.forcePostBinding()) {
+ sb.append(" <SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n"
+ + " Location=\"").append(bindUrl).append("\" />\n");
}
- idp += " <SingleLogoutService\n" +
- " Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n" +
- " Location=\"" + bindUrl + "\" />\n";
- if (!samlClient.forcePostBinding()) {
- idp += " <SingleLogoutService\n" +
- " Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n" +
- " Location=\"" + bindUrl + "\" />\n";
+ sb.append(" <SingleLogoutService\n"
+ + " Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\n"
+ + " Location=\"").append(bindUrl).append("\" />\n");
+ if (! samlClient.forcePostBinding()) {
+ sb.append(" <SingleLogoutService\n"
+ + " Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"\n"
+ + " Location=\"").append(bindUrl).append("\" />\n");
}
- idp += " <KeyDescriptor use=\"signing\">\n" +
- " <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
- " <dsig:X509Data>\n" +
- " <dsig:X509Certificate>\n" +
- " " + PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate()) + "\n" +
- " </dsig:X509Certificate>\n" +
- " </dsig:X509Data>\n" +
- " </dsig:KeyInfo>\n" +
- " </KeyDescriptor>\n" +
- " </IDPSSODescriptor>\n" +
- "</EntityDescriptor>\n";
- return idp;
+
+ Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
+ ? (int) (o2.getProviderPriority() - o1.getProviderPriority())
+ : (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
+ keys.addAll(session.keys().getKeys(realm, false));
+ for (KeyMetadata key : keys) {
+ addKeyInfo(sb, key, KeyTypes.SIGNING.value());
+ }
+
+ sb.append(" </IDPSSODescriptor>\n"
+ + "</EntityDescriptor>\n");
+ return sb.toString();
+ }
+
+ private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ if (key == null) {
+ return;
+ }
+
+ target.append(SPMetadataDescriptor.xmlKeyInfo(" ", key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
index 9d12242..6349953 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/installation/SamlSPDescriptorClientInstallation.java
@@ -31,6 +31,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -45,7 +46,8 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
if (logoutUrl == null) logoutUrl = client.getManagementUrl();
String nameIdFormat = samlClient.getNameIDFormat();
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
- return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, samlClient.requiresClientSignature(), client.getClientId(), nameIdFormat, samlClient.getClientSigningCertificate());
+ String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true);
+ return SPMetadataDescriptor.getSPDescriptor(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(), assertionUrl, logoutUrl, samlClient.requiresClientSignature(), client.getClientId(), nameIdFormat, spCertificate);
}
@Override
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
index 0415a72..ee5aaba 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
@@ -23,6 +23,8 @@ import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
/**
+ * Configuration of a SAML-enabled client.
+ *
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@@ -116,7 +118,14 @@ public class SamlClient extends ClientConfigResolver {
public void setRequiresRealmSignature(boolean val) {
client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val));
+ }
+
+ public boolean addExtensionsElementWithKeyInfo() {
+ return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT));
+ }
+ public void setAddExtensionsElementWithKeyInfo(boolean val) {
+ client.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT, Boolean.toString(val));
}
public boolean forcePostBinding() {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java b/services/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java
index e5bc2fa..0af3be0 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlClientTemplate.java
@@ -17,6 +17,7 @@
package org.keycloak.protocol.saml;
+import java.util.Objects;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.saml.SignatureAlgorithm;
@@ -89,7 +90,14 @@ public class SamlClientTemplate {
public void setRequiresRealmSignature(boolean val) {
clientTemplate.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, Boolean.toString(val));
+ }
+
+ public boolean addExtensionsElementWithKeyInfo() {
+ return Objects.equals("true", clientTemplate.getAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT));
+ }
+ public void setAddExtensionsElementWithKeyInfo(boolean val) {
+ clientTemplate.setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT, Boolean.toString(val));
}
public boolean forcePostBinding() {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
index 3356c31..9837179 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
@@ -31,6 +31,7 @@ public interface SamlConfigAttributes {
String SAML_AUTHNSTATEMENT = "saml.authnstatement";
String SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE = "saml_force_name_id_format";
String SAML_SERVER_SIGNATURE = "saml.server.signature";
+ String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
String SAML_ENCRYPT = "saml.encrypt";
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 14726d3..486633f 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -74,8 +74,10 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -98,6 +100,7 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_REDIRECT_BINDING = "get";
public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
+ public static final String SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "saml.logout.addExtensionsElementWithKeyInfo";
public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
public static final String SAML_LOGOUT_CANONICALIZATION = "SAML_LOGOUT_CANONICALIZATION";
@@ -373,7 +376,15 @@ public class SamlProtocol implements LoginProtocol {
}
Document samlDocument = null;
+ KeyManager keyManager = session.keys();
+ KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
+ boolean postBinding = isPostBinding(clientSession);
+
try {
+ if ((! postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
+
ResponseType samlModel = builder.buildModel();
final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
populateRoles(roleListMapper, session, userSession, clientSession, attributeStatement);
@@ -394,22 +405,19 @@ public class SamlProtocol implements LoginProtocol {
JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder();
bindingBuilder.relayState(relayState);
- KeyManager keyManager = session.keys();
- KeyManager.ActiveKey keys = keyManager.getActiveKey(realm);
-
if (samlClient.requiresRealmSignature()) {
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
- bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
+ bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
}
if (samlClient.requiresAssertionSignature()) {
String canonicalization = samlClient.getCanonicalizationMethod();
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
- bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
+ bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signAssertions();
}
if (samlClient.requiresEncryption()) {
PublicKey publicKey = null;
@@ -496,12 +504,17 @@ public class SamlProtocol implements LoginProtocol {
if (isLogoutPostBindingForClient(clientSession)) {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
+ // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri);
} else {
logger.debug("frontchannel redirect binding");
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
+ if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
+ KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
+ logoutBuilder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
return binding.redirectBinding(logoutBuilder.buildDocument()).request(bindingUri);
}
@@ -534,6 +547,7 @@ public class SamlProtocol implements LoginProtocol {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
binding.relayState(logoutRelayState);
String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
+ boolean postBinding = isLogoutPostBindingForInitiator(userSession);
if (signingAlgorithm != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
String canonicalization = userSession.getNote(SAML_LOGOUT_CANONICALIZATION);
@@ -541,7 +555,11 @@ public class SamlProtocol implements LoginProtocol {
binding.canonicalizationMethod(canonicalization);
}
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
- binding.signatureAlgorithm(algorithm).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
+ binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
+ boolean addExtension = (! postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
+ if (addExtension) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
try {
@@ -577,6 +595,7 @@ public class SamlProtocol implements LoginProtocol {
String logoutRequestString = null;
try {
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
+ // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
logoutRequestString = binding.postBinding(logoutBuilder.buildDocument()).encoded();
} catch (Exception e) {
logger.warn("failed to send saml logout", e);
@@ -639,7 +658,7 @@ public class SamlProtocol implements LoginProtocol {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
if (samlClient.requiresRealmSignature()) {
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
- binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
+ binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
}
return binding;
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
index e1a7c98..026a54a 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
@@ -17,6 +17,7 @@
package org.keycloak.protocol.saml;
+import java.security.Key;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.models.ClientModel;
@@ -33,6 +34,15 @@ import javax.ws.rs.core.UriInfo;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ExtensionsType;
+import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.w3c.dom.Element;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -40,20 +50,36 @@ import java.security.cert.Certificate;
*/
public class SamlProtocolUtils {
-
+ /**
+ * Verifies a signature of the given SAML document using settings for the given client.
+ * Throws an exception if the client signature is expected to be present as per the client
+ * settings and it is invalid, otherwise returns back to the caller.
+ *
+ * @param client
+ * @param document
+ * @throws VerificationException
+ */
public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException {
SamlClient samlClient = new SamlClient(client);
if (!samlClient.requiresClientSignature()) {
return;
}
PublicKey publicKey = getSignatureValidationKey(client);
- verifyDocumentSignature(document, publicKey);
+ verifyDocumentSignature(document, new HardcodedKeyLocator(publicKey));
}
- public static void verifyDocumentSignature(Document document, PublicKey publicKey) throws VerificationException {
+ /**
+ * Verifies a signature of the given SAML document using keys obtained from the given key locator.
+ * Throws an exception if the client signature is invalid, otherwise returns back to the caller.
+ *
+ * @param document
+ * @param keyLocator
+ * @throws VerificationException
+ */
+ public static void verifyDocumentSignature(Document document, KeyLocator keyLocator) throws VerificationException {
SAML2Signature saml2Signature = new SAML2Signature();
try {
- if (!saml2Signature.validate(document, publicKey)) {
+ if (!saml2Signature.validate(document, keyLocator)) {
throw new VerificationException("Invalid signature on document");
}
} catch (ProcessingException e) {
@@ -61,10 +87,22 @@ public class SamlProtocolUtils {
}
}
+ /**
+ * Returns public part of SAML signing key from the client settings.
+ * @param client
+ * @return Public key for signature validation.
+ * @throws VerificationException
+ */
public static PublicKey getSignatureValidationKey(ClientModel client) throws VerificationException {
return getPublicKey(new SamlClient(client).getClientSigningCertificate());
}
+ /**
+ * Returns public part of SAML encryption key from the client settings.
+ * @param client
+ * @return Public key for encryption.
+ * @throws VerificationException
+ */
public static PublicKey getEncryptionValidationKey(ClientModel client) throws VerificationException {
return getPublicKey(client, SamlConfigAttributes.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE);
}
@@ -85,7 +123,7 @@ public class SamlProtocolUtils {
return cert.getPublicKey();
}
- public static void verifyRedirectSignature(PublicKey publicKey, UriInfo uriInformation, String paramKey) throws VerificationException {
+ public static void verifyRedirectSignature(SAMLDocumentHolder documentHolder, KeyLocator locator, UriInfo uriInformation, String paramKey) throws VerificationException {
MultivaluedMap<String, String> encodedParams = uriInformation.getQueryParameters(false);
String request = encodedParams.getFirst(paramKey);
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
@@ -96,10 +134,11 @@ public class SamlProtocolUtils {
if (algorithm == null) throw new VerificationException("SigAlg was null");
if (signature == null) throw new VerificationException("Signature was null");
+ String keyId = getMessageSigningKeyId(documentHolder.getSamlObject());
+
// Shibboleth doesn't sign the document for redirect binding.
// todo maybe a flag?
-
UriBuilder builder = UriBuilder.fromPath("/")
.queryParam(paramKey, request);
if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) {
@@ -113,8 +152,13 @@ public class SamlProtocolUtils {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg
- validator.initVerify(publicKey);
- validator.update(rawQuery.getBytes("UTF-8"));
+ Key key = locator.getKey(keyId);
+ if (key instanceof PublicKey) {
+ validator.initVerify((PublicKey) key);
+ validator.update(rawQuery.getBytes("UTF-8"));
+ } else {
+ throw new VerificationException("Invalid key locator for signature verification");
+ }
if (!validator.verify(decodedSignature)) {
throw new VerificationException("Invalid query param signature");
}
@@ -123,5 +167,32 @@ public class SamlProtocolUtils {
}
}
+ private static String getMessageSigningKeyId(SAML2Object doc) {
+ final ExtensionsType extensions;
+ if (doc instanceof RequestAbstractType) {
+ extensions = ((RequestAbstractType) doc).getExtensions();
+ } else if (doc instanceof StatusResponseType) {
+ extensions = ((StatusResponseType) doc).getExtensions();
+ } else {
+ return null;
+ }
+
+ if (extensions == null) {
+ return null;
+ }
+
+ for (Object ext : extensions.getAny()) {
+ if (! (ext instanceof Element)) {
+ continue;
+ }
+
+ String res = KeycloakKeySamlExtensionGenerator.getMessageSigningKeyIdFromElement((Element) ext);
+ if (res != null) {
+ return res;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java b/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
index b2b4ee4..a67374a 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
@@ -64,7 +64,11 @@ public class SamlRepresentationAttributes {
public String getSamlServerSignature() {
if (getAttributes() == null) return null;
return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE);
+ }
+ public String getAddExtensionsElementWithKeyInfo() {
+ if (getAttributes() == null) return null;
+ return getAttributes().get(SamlConfigAttributes.SAML_SERVER_SIGNATURE_KEYINFO_EXT);
}
public String getForcePostBinding() {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index ea01085..14c5503 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -74,6 +74,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import org.keycloak.common.util.StringPropertyReplacer;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.keys.KeyMetadata;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.SPMetadataDescriptor;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* Resource class for the oauth/openid connect token service
@@ -336,6 +347,8 @@ public class SamlService extends AuthorizationEndpointBase {
String logoutBinding = getBindingType();
if ("true".equals(samlClient.forcePostBinding()))
logoutBinding = SamlProtocol.SAML_POST_BINDING;
+ boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
+
String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
UserSessionModel userSession = authResult.getSession();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
@@ -347,6 +360,7 @@ public class SamlService extends AuthorizationEndpointBase {
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
+ userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
// remove client from logout requests
@@ -397,14 +411,17 @@ public class SamlService extends AuthorizationEndpointBase {
builder.destination(logoutBindingUri);
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
+ boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
if (samlClient.requiresRealmSignature()) {
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
- binding.signatureAlgorithm(algorithm).signWith(keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
-
+ binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
+ if (! postBinding && samlClient.addExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
+ builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
+ }
}
try {
- if (SamlProtocol.SAML_POST_BINDING.equals(logoutBinding)) {
+ if (postBinding) {
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
} else {
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
@@ -466,7 +483,8 @@ public class SamlService extends AuthorizationEndpointBase {
return;
}
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
- SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
+ KeyLocator clientKeyLocator = new HardcodedKeyLocator(publicKey);
+ SamlProtocolUtils.verifyRedirectSignature(documentHolder, clientKeyLocator, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
}
@Override
@@ -541,12 +559,30 @@ public class SamlService extends AuthorizationEndpointBase {
public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) throws IOException {
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
String template = StreamUtil.readString(is);
- template = template.replace("${idp.entityID}", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
- template = template.replace("${idp.sso.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
- template = template.replace("${idp.sso.HTTP-Redirect}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
- template = template.replace("${idp.sls.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
- template = template.replace("${idp.signing.certificate}", PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate()));
- return template;
+ Properties props = new Properties();
+ props.put("idp.entityID", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
+ props.put("idp.sso.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
+ props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
+ props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
+ StringBuilder keysString = new StringBuilder();
+ Set<KeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
+ ? (int) (o2.getProviderPriority() - o1.getProviderPriority())
+ : (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
+ keys.addAll(session.keys().getKeys(realm, false));
+ for (KeyMetadata key : keys) {
+ addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
+ }
+ props.put("idp.signing.certificates", keysString.toString());
+ return StringPropertyReplacer.replaceProperties(template, props);
+ }
+
+ private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ if (key == null) {
+ return;
+ }
+
+ target.append(SPMetadataDescriptor.xmlKeyInfo(" ",
+ key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
}
@GET
diff --git a/services/src/main/resources/idp-metadata-template.xml b/services/src/main/resources/idp-metadata-template.xml
index 0a53647..a4416cd 100755
--- a/services/src/main/resources/idp-metadata-template.xml
+++ b/services/src/main/resources/idp-metadata-template.xml
@@ -16,22 +16,12 @@
~ limitations under the License.
-->
-<EntitiesDescriptor Name="urn:keycloak"
- xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+<EntitiesDescriptor Name="urn:keycloak" xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<EntityDescriptor entityID="${idp.entityID}">
<IDPSSODescriptor WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
-
- <KeyDescriptor use="signing">
- <dsig:KeyInfo>
- <dsig:X509Data>
- <dsig:X509Certificate>
- ${idp.signing.certificate}
- </dsig:X509Certificate>
- </dsig:X509Data>
- </dsig:KeyInfo>
- </KeyDescriptor>
+${idp.signing.certificates}
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${idp.sls.HTTP-POST}" />
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
index dbf30e9..09ddb39 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
@@ -193,6 +193,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertEquals(true, config.isPostBindingAuthnRequest());
assertEquals(true, config.isPostBindingResponse());
assertEquals(true, config.isValidateSignature());
+ assertEquals(false, config.isAddExtensionsElementWithKeyInfo());
}
private void assertOidcIdentityProviderConfig(IdentityProviderModel identityProvider) {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigPostNoIdpKeyServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigPostNoIdpKeyServlet.java
new file mode 100644
index 0000000..5ef40ae
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigPostNoIdpKeyServlet.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+
+import java.net.URL;
+
+/**
+ * @author hmlnarik
+ */
+public class EmployeeSigPostNoIdpKeyServlet extends SAMLServlet {
+ public static final String DEPLOYMENT_NAME = "employee-sig-post-noidpkey";
+
+ @ArquillianResource
+ @OperateOnDeployment(DEPLOYMENT_NAME)
+ private URL url;
+
+ @Override
+ public URL getInjectedUrl() {
+ return url;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirNoIdpKeyServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirNoIdpKeyServlet.java
new file mode 100644
index 0000000..ac6d671
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirNoIdpKeyServlet.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+
+import java.net.URL;
+
+/**
+ * @author hmlnarik
+ */
+public class EmployeeSigRedirNoIdpKeyServlet extends SAMLServlet {
+ public static final String DEPLOYMENT_NAME = "employee-sig-redir-noidpkey";
+
+ @ArquillianResource
+ @OperateOnDeployment(DEPLOYMENT_NAME)
+ private URL url;
+
+ @Override
+ public URL getInjectedUrl() {
+ return url;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirOptNoIdpKeyServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirOptNoIdpKeyServlet.java
new file mode 100644
index 0000000..e37c12f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeSigRedirOptNoIdpKeyServlet.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+
+import java.net.URL;
+
+/**
+ * @author hmlnarik
+ */
+public class EmployeeSigRedirOptNoIdpKeyServlet extends SAMLServlet {
+ public static final String DEPLOYMENT_NAME = "employee-sig-redir-opt-noidpkey";
+
+ @ArquillianResource
+ @OperateOnDeployment(DEPLOYMENT_NAME)
+ private URL url;
+
+ @Override
+ public URL getInjectedUrl() {
+ return url;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 2906778..6dd3ee3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
@@ -24,6 +24,12 @@ import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.common.util.PemUtils;
+import org.keycloak.keys.Attributes;
+import org.keycloak.keys.KeyProvider;
+import org.keycloak.keys.RsaKeyProviderFactory;
+import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.representations.idm.ClientRepresentation;
@@ -40,6 +46,9 @@ import org.keycloak.testsuite.adapter.page.BadRealmSalesPostSigServlet;
import org.keycloak.testsuite.adapter.page.Employee2Servlet;
import org.keycloak.testsuite.adapter.page.EmployeeServlet;
import org.keycloak.testsuite.adapter.page.EmployeeSigFrontServlet;
+import org.keycloak.testsuite.adapter.page.EmployeeSigPostNoIdpKeyServlet;
+import org.keycloak.testsuite.adapter.page.EmployeeSigRedirNoIdpKeyServlet;
+import org.keycloak.testsuite.adapter.page.EmployeeSigRedirOptNoIdpKeyServlet;
import org.keycloak.testsuite.adapter.page.EmployeeSigServlet;
import org.keycloak.testsuite.adapter.page.InputPortal;
import org.keycloak.testsuite.adapter.page.MissingAssertionSig;
@@ -80,6 +89,8 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
+import java.security.KeyPair;
+import java.security.PublicKey;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -110,6 +121,15 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
protected EmployeeSigServlet employeeSigServletPage;
@Page
+ protected EmployeeSigPostNoIdpKeyServlet employeeSigPostNoIdpKeyServletPage;
+
+ @Page
+ protected EmployeeSigRedirNoIdpKeyServlet employeeSigRedirNoIdpKeyServletPage;
+
+ @Page
+ protected EmployeeSigRedirOptNoIdpKeyServlet employeeSigRedirOptNoIdpKeyServletPage;
+
+ @Page
protected EmployeeSigFrontServlet employeeSigFrontServletPage;
@Page
@@ -184,6 +204,21 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
return samlServletDeployment(EmployeeSigServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
}
+ @Deployment(name = EmployeeSigPostNoIdpKeyServlet.DEPLOYMENT_NAME)
+ protected static WebArchive employeeSigPostNoIdpKeyServlet() {
+ return samlServletDeployment(EmployeeSigPostNoIdpKeyServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
+ }
+
+ @Deployment(name = EmployeeSigRedirNoIdpKeyServlet.DEPLOYMENT_NAME)
+ protected static WebArchive employeeSigRedirNoIdpKeyServlet() {
+ return samlServletDeployment(EmployeeSigRedirNoIdpKeyServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
+ }
+
+ @Deployment(name = EmployeeSigRedirOptNoIdpKeyServlet.DEPLOYMENT_NAME)
+ protected static WebArchive employeeSigRedirOptNoIdpKeyServlet() {
+ return samlServletDeployment(EmployeeSigRedirOptNoIdpKeyServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
+ }
+
@Deployment(name = EmployeeSigFrontServlet.DEPLOYMENT_NAME)
protected static WebArchive employeeSigFront() {
return samlServletDeployment(EmployeeSigFrontServlet.DEPLOYMENT_NAME, SendUsernameServlet.class);
@@ -394,6 +429,69 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
testSuccessfulAndUnauthorizedLogin(employeeSigServletPage, testRealmSAMLRedirectLoginPage);
}
+ private PublicKey createKeys(String priority) throws Exception {
+ KeyPair keyPair = KeyUtils.generateRsaKeyPair(1024);
+ String privateKeyPem = PemUtils.encodeKey(keyPair.getPrivate());
+ PublicKey publicKey = keyPair.getPublic();
+
+ ComponentRepresentation rep = new ComponentRepresentation();
+ rep.setName("mycomponent");
+ rep.setParentId("demo");
+ rep.setProviderId(RsaKeyProviderFactory.ID);
+ rep.setProviderType(KeyProvider.class.getName());
+
+ org.keycloak.common.util.MultivaluedHashMap config = new org.keycloak.common.util.MultivaluedHashMap();
+ config.addFirst("priority", priority);
+ config.addFirst(Attributes.PRIVATE_KEY_KEY, privateKeyPem);
+ rep.setConfig(config);
+
+ testRealmResource().components().add(rep);
+
+ return publicKey;
+ }
+
+ private void dropKeys(String priority) {
+ for (ComponentRepresentation c : testRealmResource().components().query("demo", KeyProvider.class.getName())) {
+ if (c.getConfig().getFirst("priority").equals(priority)) {
+ testRealmResource().components().component(c.getId()).remove();
+ return;
+ }
+ }
+ throw new RuntimeException("Failed to find keys");
+ }
+
+ private void testRotatedKeysPropagated(SAMLServlet servletPage, Login loginPage) throws Exception {
+ boolean keyDropped = false;
+ try {
+ log.info("Creating new key");
+ createKeys("1000");
+ testSuccessfulAndUnauthorizedLogin(servletPage, loginPage);
+ log.info("Dropping new key");
+ dropKeys("1000");
+ keyDropped = true;
+ testSuccessfulAndUnauthorizedLogin(servletPage, loginPage);
+ } finally {
+ if (! keyDropped) {
+ dropKeys("1000");
+ }
+ }
+ }
+
+ @Test
+ public void employeeSigPostNoIdpKeyTest() throws Exception {
+ testRotatedKeysPropagated(employeeSigPostNoIdpKeyServletPage, testRealmSAMLPostLoginPage);
+ }
+
+ @Test
+ public void employeeSigRedirNoIdpKeyTest() throws Exception {
+ testRotatedKeysPropagated(employeeSigRedirNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
+ }
+
+ @Test
+ public void employeeSigRedirOptNoIdpKeyTest() throws Exception {
+ testRotatedKeysPropagated(employeeSigRedirOptNoIdpKeyServletPage, testRealmSAMLRedirectLoginPage);
+ }
+
@Test
public void employeeSigFrontTest() {
testSuccessfulAndUnauthorizedLogin(employeeSigFrontServletPage, testRealmSAMLRedirectLoginPage);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java
index 626c0d4..4328c8f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java
@@ -21,13 +21,11 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientResource;
-import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.*;
/**
* Test getting the installation/configuration files for OIDC and SAML.
@@ -71,7 +69,7 @@ public class InstallationTest extends AbstractClientTest {
public void testOidcJBossXml() {
String xml = oidcClient.getInstallationProvider("keycloak-oidc-jboss-subsystem");
assertOidcInstallationConfig(xml);
- assertTrue(xml.contains("<secure-deployment"));
+ assertThat(xml, containsString("<secure-deployment"));
}
@Test
@@ -81,43 +79,43 @@ public class InstallationTest extends AbstractClientTest {
}
private void assertOidcInstallationConfig(String config) {
- assertTrue(config.contains("master"));
- assertFalse(config.contains(ApiUtil.findActiveKey(testRealmResource()).getPublicKey()));
- assertTrue(config.contains(authServerUrl()));
+ assertThat(config, containsString("master"));
+ assertThat(config, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getPublicKey())));
+ assertThat(config, containsString(authServerUrl()));
}
@Test
public void testSamlMetadataIdpDescriptor() {
String xml = samlClient.getInstallationProvider("saml-idp-descriptor");
- assertTrue(xml.contains("<EntityDescriptor"));
- assertTrue(xml.contains("<IDPSSODescriptor"));
- assertTrue(xml.contains(ApiUtil.findActiveKey(testRealmResource()).getCertificate()));
- assertTrue(xml.contains(samlUrl()));
+ assertThat(xml, containsString("<EntityDescriptor"));
+ assertThat(xml, containsString("<IDPSSODescriptor"));
+ assertThat(xml, containsString(ApiUtil.findActiveKey(testRealmResource()).getCertificate()));
+ assertThat(xml, containsString(samlUrl()));
}
@Test
public void testSamlAdapterXml() {
String xml = samlClient.getInstallationProvider("keycloak-saml");
- assertTrue(xml.contains("<keycloak-saml-adapter>"));
- assertTrue(xml.contains(SAML_NAME));
- assertTrue(xml.contains(ApiUtil.findActiveKey(testRealmResource()).getCertificate()));
- assertTrue(xml.contains(samlUrl()));
+ assertThat(xml, containsString("<keycloak-saml-adapter>"));
+ assertThat(xml, containsString(SAML_NAME));
+ assertThat(xml, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getCertificate())));
+ assertThat(xml, containsString(samlUrl()));
}
@Test
public void testSamlMetadataSpDescriptor() {
String xml = samlClient.getInstallationProvider("saml-sp-descriptor");
- assertTrue(xml.contains("<EntityDescriptor"));
- assertTrue(xml.contains("<SPSSODescriptor"));
- assertTrue(xml.contains(SAML_NAME));
+ assertThat(xml, containsString("<EntityDescriptor"));
+ assertThat(xml, containsString("<SPSSODescriptor"));
+ assertThat(xml, containsString(SAML_NAME));
}
@Test
public void testSamlJBossXml() {
String xml = samlClient.getInstallationProvider("keycloak-saml-subsystem");
- assertTrue(xml.contains("<secure-deployment"));
- assertTrue(xml.contains(SAML_NAME));
- assertTrue(xml.contains(ApiUtil.findActiveKey(testRealmResource()).getCertificate()));
- assertTrue(xml.contains(samlUrl()));
+ assertThat(xml, containsString("<secure-deployment"));
+ assertThat(xml, containsString(SAML_NAME));
+ assertThat(xml, not(containsString(ApiUtil.findActiveKey(testRealmResource()).getCertificate())));
+ assertThat(xml, containsString(samlUrl()));
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
index e3392b3..f550c17 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
@@ -57,17 +57,43 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import javax.xml.crypto.dsig.XMLSignature;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.hamcrest.Matchers.*;
+import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
+import org.w3c.dom.NodeList;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class IdentityProviderTest extends AbstractAdminTest {
+ // Certificate imported from
+ private static final String SIGNING_CERT_1 = "MIICmzCCAYMCBgFUYnC0OjANBgkqhkiG9w0BAQsFADARMQ8wDQY"
+ + "DVQQDDAZtYXN0ZXIwHhcNMTYwNDI5MTQzMjEzWhcNMjYwNDI5MTQzMzUzWjARMQ8wDQYDVQQDDAZtYXN0ZXI"
+ + "wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCN25AW1poMEZRbuMAHG58AThZmCwMV6/Gcui4mjGa"
+ + "cRFyudgqzLjQ2rxpoW41JAtLjbjeAhuWvirUcFVcOeS3gM/ZC27qCpYighAcylZz6MYocnEe1+e8rPPk4JlI"
+ + "D6Wv62dgu+pL/vYsQpRhvD3Y2c/ytgr5D32xF+KnzDehUy5BSyzypvu12Wq9mS5vK5tzkN37EjkhpY2ZxaXP"
+ + "ubjDIITCAL4Q8M/m5IlacBaUZbzI4AQrHnMP1O1IH2dHSWuMiBe+xSDTco72PmuYPJKTV4wQdeBUIkYbfLc4"
+ + "RxVmXEvgkQgyW86EoMPxlWJpj7+mTIR+l+2thZPr/VgwTs82rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAA/"
+ + "Ip/Hi8RoVu5ouaFFlc5whT7ltuK8slfLGW4tM4vJXhInYwsqIRQKBNDYW/64xle3eII4u1yAH1OYRRwEs7Em"
+ + "1pr4QuFuTY1at+aE0sE46XDlyESI0txJjWxYoT133vM0We2pj1b2nxgU30rwjKA3whnKEfTEYT/n3JBSqNgg"
+ + "y6l8ZGw/oPSgvPaR4+xeB1tfQFC4VrLoYKoqH6hAL530nKxL+qV8AIfL64NDEE8ankIAEDAAFe8x3CPUfXR/"
+ + "p4KOANKkpz8ieQaHDb1eITkAwUwjESj6UF9D1aePlhWls/HX0gujFXtWfWfrJ8CU/ogwlH8y1jgRuLjFQYZk6llc=";
+
+ private static final String SIGNING_CERT_2 = "MIIBnDCCAQUCBgFYKXKsPTANBgkqhkiG9w0BAQsFADAUMRIwEAY"
+ + "DVQQDDAlzYW1sLWRlbW8wHhcNMTYxMTAzMDkwNzEwWhcNMjYxMTAzMDkwODUwWjAUMRIwEAYDVQQDDAlzYW1"
+ + "sLWRlbW8wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKtWsK5O0CtuBpnMvWG+HTG0vmZzujQ2o9WdheQ"
+ + "u+BzCILcGMsbDW0YQaglpcO5JpGWWhubnckGGPHfdQ2/7nP9QwbiTK0FbGF41UqcvoaCqU1psxoV88s8IXyQ"
+ + "CAqeyLv00yj6foqdJjxh5SZ5z+na+M7Y2OxIBVxYRAxWEnfUvAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAhet"
+ + "vOU8TyqfZF5jpv0IcrviLl/DoFrbjByeHR+pu/vClcAOjL/u7oQELuuTfNsBI4tpexUj5G8q/YbEz0gk7idf"
+ + "LXrAUVcsR73oTngrhRfwUSmPrjjK0kjcRb6HL9V/+wh3R/6mEd59U08ExT8N38rhmn0CI3ehMdebReprP7U8=";
+
@Test
public void testFindAll() {
create(createRep("google", "google"));
@@ -303,7 +329,45 @@ public class IdentityProviderTest extends AbstractAdminTest {
form.addFormData("file", body, MediaType.APPLICATION_XML_TYPE, "saml-idp-metadata.xml");
Map<String, String> result = realm.identityProviders().importFrom(form);
- assertSamlImport(result);
+ assertSamlImport(result, SIGNING_CERT_1);
+
+ // Create new SAML identity provider using configuration retrieved from import-config
+ create(createRep("saml", "saml", result));
+
+ IdentityProviderResource provider = realm.identityProviders().get("saml");
+ IdentityProviderRepresentation rep = provider.toRepresentation();
+ assertCreatedSamlIdp(rep);
+
+ // Now list the providers - we should see the one just created
+ List<IdentityProviderRepresentation> providers = realm.identityProviders().findAll();
+ Assert.assertNotNull("identityProviders not null", providers);
+ Assert.assertEquals("identityProviders instance count", 1, providers.size());
+ assertEqual(rep, providers.get(0));
+
+ // Perform export, and make sure some of the values are like they're supposed to be
+ Response response = realm.identityProviders().get("saml").export("xml");
+ Assert.assertEquals(200, response.getStatus());
+ body = response.readEntity(String.class);
+ response.close();
+
+ assertSamlExport(body);
+ }
+
+ @Test
+ public void testSamlImportAndExportMultipleSigningKeys() throws URISyntaxException, IOException, ParsingException {
+
+ // Use import-config to convert IDPSSODescriptor file into key value pairs
+ // to use when creating a SAML Identity Provider
+ MultipartFormDataOutput form = new MultipartFormDataOutput();
+ form.addFormData("providerId", "saml", MediaType.TEXT_PLAIN_TYPE);
+
+ URL idpMeta = getClass().getClassLoader().getResource("admin-test/saml-idp-metadata-two-signing-certs.xml");
+ byte [] content = Files.readAllBytes(Paths.get(idpMeta.toURI()));
+ String body = new String(content, Charset.forName("utf-8"));
+ form.addFormData("file", body, MediaType.APPLICATION_XML_TYPE, "saml-idp-metadata-two-signing-certs");
+
+ Map<String, String> result = realm.identityProviders().importFrom(form);
+ assertSamlImport(result, SIGNING_CERT_1 + "," + SIGNING_CERT_2);
// Create new SAML identity provider using configuration retrieved from import-config
create(createRep("saml", "saml", result));
@@ -464,18 +528,29 @@ public class IdentityProviderTest extends AbstractAdminTest {
// import endpoint simply converts IDPSSODescriptor into key value pairs.
// check that saml-idp-metadata.xml was properly converted into key value pairs
//System.out.println(config);
- Assert.assertEquals("Config size", 7, config.size());
- Assert.assertEquals("validateSignature", "true", config.get("validateSignature"));
- Assert.assertEquals("singleLogoutServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml", config.get("singleLogoutServiceUrl"));
- Assert.assertEquals("postBindingResponse", "true", config.get("postBindingResponse"));
- Assert.assertEquals("postBindingAuthnRequest", "true", config.get("postBindingAuthnRequest"));
- Assert.assertEquals("singleSignOnServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml", config.get("singleSignOnServiceUrl"));
- Assert.assertEquals("wantAuthnRequestsSigned", "true", config.get("wantAuthnRequestsSigned"));
- Assert.assertNotNull("signingCertificate not null", config.get("signingCertificate"));
+ assertThat(config.keySet(), containsInAnyOrder(
+ "validateSignature",
+ "singleLogoutServiceUrl",
+ "postBindingResponse",
+ "postBindingAuthnRequest",
+ "singleSignOnServiceUrl",
+ "wantAuthnRequestsSigned",
+ "signingCertificate",
+ "addExtensionsElementWithKeyInfo"
+ ));
+ assertThat(config, hasEntry("validateSignature", "true"));
+ assertThat(config, hasEntry("singleLogoutServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml"));
+ assertThat(config, hasEntry("postBindingResponse", "true"));
+ assertThat(config, hasEntry("postBindingAuthnRequest", "true"));
+ assertThat(config, hasEntry("singleSignOnServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml"));
+ assertThat(config, hasEntry("wantAuthnRequestsSigned", "true"));
+ assertThat(config, hasEntry("addExtensionsElementWithKeyInfo", "false"));
+ assertThat(config, hasEntry(is("signingCertificate"), notNullValue()));
}
- private void assertSamlImport(Map<String, String> config) {
+ private void assertSamlImport(Map<String, String> config, String expectedSigningCertificates) {
assertSamlConfig(config);
+ assertThat(config, hasEntry("signingCertificate", expectedSigningCertificates));
}
private void assertSamlExport(String body) throws ParsingException, URISyntaxException {
@@ -534,7 +609,11 @@ public class IdentityProviderTest extends AbstractAdminTest {
Assert.assertNotNull("KeyDescriptor not null", desc.getKeyDescriptor());
Assert.assertEquals("KeyDescriptor.size", 1, desc.getKeyDescriptor().size());
- Assert.assertEquals("KeyDescriptor.Use", KeyTypes.SIGNING, desc.getKeyDescriptor().get(0).getUse());
+ KeyDescriptorType keyDesc = desc.getKeyDescriptor().get(0);
+ assertThat(keyDesc, notNullValue());
+ assertThat(keyDesc.getUse(), equalTo(KeyTypes.SIGNING));
+ NodeList cert = keyDesc.getKeyInfo().getElementsByTagNameNS(XMLSignature.XMLNS, "X509Certificate");
+ assertThat("KeyDescriptor.Signing.Cert existence", cert.getLength(), is(1));
}
private void assertProviderInfo(Map<String, String> info, String id, String name) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keycloak-saml.xml
new file mode 100644
index 0000000..acd3d9d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,54 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<keycloak-saml-adapter>
+ <SP entityID="http://localhost:8081/employee-sig-post-noidpkey/"
+ sslPolicy="EXTERNAL"
+ logoutPage="/logout.jsp"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ forceAuthentication="false">
+ <Keys>
+ <Key signing="true" >
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <PrivateKey alias="http://localhost:8080/employee-sig/" password="test123"/>
+ <Certificate alias="http://localhost:8080/employee-sig/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="POST"
+ bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+ />
+
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+ />
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keystore.jks
new file mode 100644
index 0000000..4daad21
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-post-noidpkey/WEB-INF/keystore.jks differ
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keycloak-saml.xml
new file mode 100644
index 0000000..c134693
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,54 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<keycloak-saml-adapter>
+ <SP entityID="http://localhost:8081/employee-sig-redir-noidpkey/"
+ sslPolicy="EXTERNAL"
+ logoutPage="/logout.jsp"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ forceAuthentication="false">
+ <Keys>
+ <Key signing="true" >
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <PrivateKey alias="http://localhost:8080/employee-sig/" password="test123"/>
+ <Certificate alias="http://localhost:8080/employee-sig/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="REDIRECT"
+ bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+ />
+
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="REDIRECT"
+ responseBinding="REDIRECT"
+ redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+ />
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keystore.jks
new file mode 100644
index 0000000..4daad21
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-noidpkey/WEB-INF/keystore.jks differ
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keycloak-saml.xml
new file mode 100644
index 0000000..8df82ae
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,54 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<keycloak-saml-adapter>
+ <SP entityID="http://localhost:8081/employee-sig-redir-opt-noidpkey/"
+ sslPolicy="EXTERNAL"
+ logoutPage="/logout.jsp"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ forceAuthentication="false">
+ <Keys>
+ <Key signing="true" >
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <PrivateKey alias="http://localhost:8080/employee-sig/" password="test123"/>
+ <Certificate alias="http://localhost:8080/employee-sig/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="REDIRECT"
+ bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+ />
+
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="REDIRECT"
+ responseBinding="REDIRECT"
+ redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
+ />
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keystore.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keystore.jks
new file mode 100644
index 0000000..4daad21
Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-sig-redir-opt-noidpkey/WEB-INF/keystore.jks differ
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
index 0e25d89..cca23fc 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
@@ -334,6 +334,61 @@
}
},
{
+ "clientId": "http://localhost:8081/employee-sig-redir-noidpkey/",
+ "enabled": true,
+ "protocol": "saml",
+ "fullScopeAllowed": true,
+ "baseUrl": "http://localhost:8080/employee-sig-redir-noidpkey",
+ "redirectUris": [
+ "http://localhost:8080/employee-sig-redir-noidpkey/*"
+ ],
+ "adminUrl": "http://localhost:8080/employee-sig-redir-noidpkey",
+ "attributes": {
+ "saml.server.signature": "true",
+ "saml.client.signature": "true",
+ "saml.signature.algorithm": "RSA_SHA256",
+ "saml.authnstatement": "true",
+ "saml.signing.certificate": "MIIB0DCCATkCBgFJH5u0EDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNodHRwOi8vbG9jYWxob3N0OjgwODAvZW1wbG95ZWUtc2lnLzAeFw0xNDEwMTcxOTMzNThaFw0yNDEwMTcxOTM1MzhaMC4xLDAqBgNVBAMTI2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9lbXBsb3llZS1zaWcvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+9kVgPFpshjS2aT2g52lqTv2lqb1jgvXZVk7iFF4LAO6SdCXKXRZI4SuzIRkVNpE1a42V1kQRlaozoFklgvX5sje8tkpa9ylq+bxGXM9RRycqRu2B+oWUV7Aqq7Bs0Xud0WeHQYRcEoCjqsFKGy65qkLRDdT70FTJgpSHts+gDwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBACKyPLGqMX8GsIrCfJU8eVnpaqzTXMglLVo/nTcfAnWe9UAdVe8N3a2PXpDBvuqNA/DEAhVcQgxdlOTWnB6s8/yLTRuH0bZgb3qGdySif+lU+E7zZ/SiDzavAvn+ABqemnzHcHyhYO+hNRGHvUbW5OAii9Vdjhm8BI32YF1NwhKp"
+ }
+ },
+ {
+ "clientId": "http://localhost:8081/employee-sig-redir-opt-noidpkey/",
+ "enabled": true,
+ "protocol": "saml",
+ "fullScopeAllowed": true,
+ "baseUrl": "http://localhost:8080/employee-sig-redir-opt-noidpkey",
+ "redirectUris": [
+ "http://localhost:8080/employee-sig-redir-opt-noidpkey/*"
+ ],
+ "adminUrl": "http://localhost:8080/employee-sig-redir-opt-noidpkey",
+ "attributes": {
+ "saml.server.signature": "true",
+ "saml.server.signature.keyinfo.ext": "true",
+ "saml.client.signature": "true",
+ "saml.signature.algorithm": "RSA_SHA256",
+ "saml.authnstatement": "true",
+ "saml.signing.certificate": "MIIB0DCCATkCBgFJH5u0EDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNodHRwOi8vbG9jYWxob3N0OjgwODAvZW1wbG95ZWUtc2lnLzAeFw0xNDEwMTcxOTMzNThaFw0yNDEwMTcxOTM1MzhaMC4xLDAqBgNVBAMTI2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9lbXBsb3llZS1zaWcvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+9kVgPFpshjS2aT2g52lqTv2lqb1jgvXZVk7iFF4LAO6SdCXKXRZI4SuzIRkVNpE1a42V1kQRlaozoFklgvX5sje8tkpa9ylq+bxGXM9RRycqRu2B+oWUV7Aqq7Bs0Xud0WeHQYRcEoCjqsFKGy65qkLRDdT70FTJgpSHts+gDwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBACKyPLGqMX8GsIrCfJU8eVnpaqzTXMglLVo/nTcfAnWe9UAdVe8N3a2PXpDBvuqNA/DEAhVcQgxdlOTWnB6s8/yLTRuH0bZgb3qGdySif+lU+E7zZ/SiDzavAvn+ABqemnzHcHyhYO+hNRGHvUbW5OAii9Vdjhm8BI32YF1NwhKp"
+ }
+ },
+ {
+ "clientId": "http://localhost:8081/employee-sig-post-noidpkey/",
+ "enabled": true,
+ "protocol": "saml",
+ "fullScopeAllowed": true,
+ "baseUrl": "http://localhost:8080/employee-sig-post-noidpkey",
+ "redirectUris": [
+ "http://localhost:8080/employee-sig-post-noidpkey/*"
+ ],
+ "adminUrl": "http://localhost:8080/employee-sig-post-noidpkey",
+ "attributes": {
+ "saml.server.signature": "true",
+ "saml.client.signature": "true",
+ "saml.signature.algorithm": "RSA_SHA256",
+ "saml.authnstatement": "true",
+ "saml.signing.certificate": "MIIB0DCCATkCBgFJH5u0EDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNodHRwOi8vbG9jYWxob3N0OjgwODAvZW1wbG95ZWUtc2lnLzAeFw0xNDEwMTcxOTMzNThaFw0yNDEwMTcxOTM1MzhaMC4xLDAqBgNVBAMTI2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9lbXBsb3llZS1zaWcvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+9kVgPFpshjS2aT2g52lqTv2lqb1jgvXZVk7iFF4LAO6SdCXKXRZI4SuzIRkVNpE1a42V1kQRlaozoFklgvX5sje8tkpa9ylq+bxGXM9RRycqRu2B+oWUV7Aqq7Bs0Xud0WeHQYRcEoCjqsFKGy65qkLRDdT70FTJgpSHts+gDwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBACKyPLGqMX8GsIrCfJU8eVnpaqzTXMglLVo/nTcfAnWe9UAdVe8N3a2PXpDBvuqNA/DEAhVcQgxdlOTWnB6s8/yLTRuH0bZgb3qGdySif+lU+E7zZ/SiDzavAvn+ABqemnzHcHyhYO+hNRGHvUbW5OAii9Vdjhm8BI32YF1NwhKp"
+ }
+ },
+ {
"clientId": "http://localhost:8081/employee/",
"enabled": true,
"protocol": "saml",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml
index 2bcfc21..f28e206 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<EntityDescriptor entityID="http://localhost:8080/auth/realms/master"
xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
>
<IDPSSODescriptor WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml
new file mode 100644
index 0000000..dba0d5a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/saml-idp-metadata-two-signing-certs.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor entityID="http://localhost:8080/auth/realms/master"
+ xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"
+>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true"
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+ <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
+ <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+ <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
+ <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
+
+ <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ Location="http://localhost:8080/auth/realms/master/protocol/saml" />
+ <SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ Location="http://localhost:8080/auth/realms/master/protocol/saml" />
+ <KeyDescriptor use="signing">
+ <dsig:KeyInfo>
+ <dsig:KeyName>hAoy_sBtpu6FdRVCk7ykihF6Ug-o0pKPK3LN9RYkeqs</dsig:KeyName>
+ <dsig:X509Data>
+ <dsig:X509Certificate>
+ MIICmzCCAYMCBgFUYnC0OjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMTYwNDI5MTQzMjEzWhcNMjYwNDI5MTQzMzUzWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCN25AW1poMEZRbuMAHG58AThZmCwMV6/Gcui4mjGacRFyudgqzLjQ2rxpoW41JAtLjbjeAhuWvirUcFVcOeS3gM/ZC27qCpYighAcylZz6MYocnEe1+e8rPPk4JlID6Wv62dgu+pL/vYsQpRhvD3Y2c/ytgr5D32xF+KnzDehUy5BSyzypvu12Wq9mS5vK5tzkN37EjkhpY2ZxaXPubjDIITCAL4Q8M/m5IlacBaUZbzI4AQrHnMP1O1IH2dHSWuMiBe+xSDTco72PmuYPJKTV4wQdeBUIkYbfLc4RxVmXEvgkQgyW86EoMPxlWJpj7+mTIR+l+2thZPr/VgwTs82rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAA/Ip/Hi8RoVu5ouaFFlc5whT7ltuK8slfLGW4tM4vJXhInYwsqIRQKBNDYW/64xle3eII4u1yAH1OYRRwEs7Em1pr4QuFuTY1at+aE0sE46XDlyESI0txJjWxYoT133vM0We2pj1b2nxgU30rwjKA3whnKEfTEYT/n3JBSqNggy6l8ZGw/oPSgvPaR4+xeB1tfQFC4VrLoYKoqH6hAL530nKxL+qV8AIfL64NDEE8ankIAEDAAFe8x3CPUfXR/p4KOANKkpz8ieQaHDb1eITkAwUwjESj6UF9D1aePlhWls/HX0gujFXtWfWfrJ8CU/ogwlH8y1jgRuLjFQYZk6llc=
+ </dsig:X509Certificate>
+ </dsig:X509Data>
+ </dsig:KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="signing">
+ <dsig:KeyInfo>
+ <dsig:KeyName>FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE</dsig:KeyName>
+ <dsig:X509Data>
+ <dsig:X509Certificate>
+ MIIBnDCCAQUCBgFYKXKsPTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlzYW1sLWRlbW8wHhcNMTYxMTAzMDkwNzEwWhcNMjYxMTAzMDkwODUwWjAUMRIwEAYDVQQDDAlzYW1sLWRlbW8wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKtWsK5O0CtuBpnMvWG+HTG0vmZzujQ2o9WdheQu+BzCILcGMsbDW0YQaglpcO5JpGWWhubnckGGPHfdQ2/7nP9QwbiTK0FbGF41UqcvoaCqU1psxoV88s8IXyQCAqeyLv00yj6foqdJjxh5SZ5z+na+M7Y2OxIBVxYRAxWEnfUvAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAhetvOU8TyqfZF5jpv0IcrviLl/DoFrbjByeHR+pu/vClcAOjL/u7oQELuuTfNsBI4tpexUj5G8q/YbEz0gk7idfLXrAUVcsR73oTngrhRfwUSmPrjjK0kjcRb6HL9V/+wh3R/6mEd59U08ExT8N38rhmn0CI3ehMdebReprP7U8=
+ </dsig:X509Certificate>
+ </dsig:X509Data>
+ </dsig:KeyInfo>
+ </KeyDescriptor>
+ </IDPSSODescriptor>
+</EntityDescriptor>
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
index 79092cd..b8cbf4d 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
@@ -223,6 +223,7 @@ public class ClientSettingsForm extends CreateClientForm {
public static final String SAML_FORCE_POST_BINDING = "saml.force.post.binding";
public static final String SAML_MULTIVALUED_ROLES = "saml.multivalued.roles";
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
+ public static final String SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
public static final String SAML_ASSERTION_CONSUMER_URL_POST = "saml_assertion_consumer_url_post";
public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect";
@@ -236,6 +237,8 @@ public class ClientSettingsForm extends CreateClientForm {
private OnOffSwitch samlAuthnStatement;
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignature']]")
private OnOffSwitch samlServerSignature;
+ @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlServerSignatureEnableKeyInfoExtension']]")
+ private OnOffSwitch samlServerSignatureKeyInfoExt;
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlAssertionSignature']]")
private OnOffSwitch samlAssertionSignature;
@FindBy(id = "signatureAlgorithm")
@@ -277,6 +280,7 @@ public class ClientSettingsForm extends CreateClientForm {
if (samlServerSignature.isOn() || samlAssertionSignature.isOn()) {
signatureAlgorithm.selectByVisibleText(attributes.get(SAML_SIGNATURE_ALGORITHM));
canonicalization.selectByValue("string:" + attributes.get(SAML_SIGNATURE_CANONICALIZATION_METHOD));
+ samlServerSignatureKeyInfoExt.setOn("true".equals(attributes.get(SAML_SERVER_SIGNATURE_KEYINFO_EXT)));
}
samlEncrypt.setOn("true".equals(attributes.get(SAML_ENCRYPT)));
samlClientSignature.setOn("true".equals(attributes.get(SAML_CLIENT_SIGNATURE)));
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index b330a3c..a0b0d78 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -209,6 +209,8 @@ include-authnstatement=Include AuthnStatement
include-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses?
sign-documents=Sign Documents
sign-documents.tooltip=Should SAML documents be signed by the realm?
+sign-documents-redirect-enable-key-info-ext=Optimize REDIRECT signing key lookup
+sign-documents-redirect-enable-key-info-ext.tooltip=When signing SAML documents in REDIRECT binding for SP that is secured by Keycloak adapter, should the ID of the signing key be included in SAML protocol message in <Extensions> element? This optimizes validation of the signature as the validating party uses a single key instead of trying every known key for validation.
sign-assertions=Sign Assertions
sign-assertions.tooltip=Should assertions inside SAML documents be signed? This setting isn't needed if document is already being signed.
signature-algorithm=Signature Algorithm
@@ -506,8 +508,8 @@ force-authentication=Force Authentication
identity-provider.force-authentication.tooltip=Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context.
validate-signature=Validate Signature
saml.validate-signature.tooltip=Enable/disable signature validation of SAML responses.
-validating-x509-certificate=Validating X509 Certificate
-validating-x509-certificate.tooltip=The certificate in PEM format that must be used to check for signatures.
+validating-x509-certificate=Validating X509 Certificates
+validating-x509-certificate.tooltip=The certificate in PEM format that must be used to check for signatures. Multiple certificates can be entered, separated by comma (,).
saml.import-from-url.tooltip=Import metadata from a remote IDP SAML entity descriptor.
social.client-id.tooltip=The client identifier registered with the identity provider.
social.client-secret.tooltip=The client secret registered with the identity provider.
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 4503b2f..624e9a5 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -860,6 +860,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.samlAuthnStatement = false;
$scope.samlMultiValuedRoles = false;
$scope.samlServerSignature = false;
+ $scope.samlServerSignatureEnableKeyInfoExtension = false;
$scope.samlAssertionSignature = false;
$scope.samlClientSignature = false;
$scope.samlEncrypt = false;
@@ -908,6 +909,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
}
}
+ if ($scope.client.attributes["saml.server.signature.keyinfo.ext"]) {
+ if ($scope.client.attributes["saml.server.signature.keyinfo.ext"] == "true") {
+ $scope.samlServerSignatureEnableKeyInfoExtension = true;
+ } else {
+ $scope.samlServerSignatureEnableKeyInfoExtension = false;
+ }
+ }
if ($scope.client.attributes["saml.assertion.signature"]) {
if ($scope.client.attributes["saml.assertion.signature"] == "true") {
$scope.samlAssertionSignature = true;
@@ -1115,7 +1123,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
$scope.client.attributes["saml.server.signature"] = "true";
} else {
$scope.client.attributes["saml.server.signature"] = "false";
-
+ }
+ if ($scope.samlServerSignatureEnableKeyInfoExtension == true) {
+ $scope.client.attributes["saml.server.signature.keyinfo.ext"] = "true";
+ } else {
+ $scope.client.attributes["saml.server.signature.keyinfo.ext"] = "false";
}
if ($scope.samlAssertionSignature == true) {
$scope.client.attributes["saml.assertion.signature"] = "true";
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 8601770..6b0f0a6 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -118,7 +118,7 @@
</div>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
- <label class="col-md-2 control-label" for="samlServerSignature">{{:: 'include-authnstatement' | translate}}</label>
+ <label class="col-md-2 control-label" for="samlAuthnStatement">{{:: 'include-authnstatement' | translate}}</label>
<div class="col-sm-6">
<input ng-model="samlAuthnStatement" ng-click="switchChange()" name="samlAuthnStatement" id="samlAuthnStatement" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
@@ -131,6 +131,13 @@
</div>
<kc-tooltip>{{:: 'sign-documents.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group clearfix block" data-ng-show="protocol == 'saml' && samlServerSignature == true">
+ <label class="col-md-2 control-label" for="samlServerSignatureEnableKeyInfoExtension">{{:: 'sign-documents-redirect-enable-key-info-ext' | translate}}</label>
+ <div class="col-sm-6">
+ <input ng-model="samlServerSignatureEnableKeyInfoExtension" ng-click="switchChange()" name="samlServerSignatureEnableKeyInfoExtension" id="samlServerSignatureEnableKeyInfoExtension" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ </div>
+ <kc-tooltip>{{:: 'sign-documents-redirect-enable-key-info-ext.tooltip' | translate}}</kc-tooltip>
+ </div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlAssertionSignature">{{:: 'sign-assertions' | translate}}</label>
<div class="col-sm-6">