From d7615d6a684439fce58377f4dbda4d1acb2cf844 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Wed, 26 Apr 2017 11:29:59 +0200 Subject: [PATCH] KEYCLOAK-2122 Configuration of AssertionConsumerServiceUrl in SAML adapter --- .../adapters/saml/AbstractInitiateLogin.java | 12 +- .../adapters/saml/DefaultSamlDeployment.java | 21 +- .../adapters/saml/SamlDeployment.java | 18 +- .../keycloak/adapters/saml/config/IDP.java | 9 + .../config/parsers/ConfigXmlConstants.java | 1 + .../config/parsers/DeploymentBuilder.java | 7 + .../saml/config/parsers/IDPXmlParser.java | 1 + .../profile/ecp/EcpAuthenticationHandler.java | 10 +- .../schema/keycloak_saml_adapter_1_8.xsd | 456 ++++++++++++++++++ .../saml/SAML2AuthnRequestBuilder.java | 5 + .../adapter/page/EmployeeAcsServlet.java | 39 ++ .../DeploymentArchiveProcessor.java | 3 + .../AbstractSAMLFilterServletAdapterTest.java | 2 + .../AbstractSAMLServletsAdapterTest.java | 49 +- .../employee-acs/WEB-INF/keycloak-saml.xml | 46 ++ .../adapter-test/keycloak-saml/testsaml.json | 50 ++ 16 files changed, 688 insertions(+), 41 deletions(-) create mode 100644 adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeAcsServlet.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-acs/WEB-INF/keycloak-saml.xml 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 6ddf52c693..d1e6664028 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 @@ -17,6 +17,7 @@ package org.keycloak.adapters.saml; +import org.keycloak.adapters.saml.SamlDeployment.IDP.SingleSignOnService; import org.jboss.logging.Logger; import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.HttpFacade; @@ -95,21 +96,22 @@ public abstract class AbstractInitiateLogin implements AuthChallenge { nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); } + SingleSignOnService sso = deployment.getIDP().getSingleSignOnService(); SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder() - .destination(deployment.getIDP().getSingleSignOnService().getRequestBindingUrl()) + .destination(sso.getRequestBindingUrl()) .issuer(issuerURL) .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive()) .nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat)); - if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) { + if (sso.getResponseBinding() != null) { String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get(); - if (deployment.getIDP().getSingleSignOnService().getResponseBinding() == SamlDeployment.Binding.POST) { + if (sso.getResponseBinding() == SamlDeployment.Binding.POST) { protocolBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get(); } authnRequestBuilder.protocolBinding(protocolBinding); } - if (deployment.getAssertionConsumerServiceUrl() != null) { - authnRequestBuilder.assertionConsumerUrl(deployment.getAssertionConsumerServiceUrl()); + if (sso.getAssertionConsumerServiceUrl() != null) { + authnRequestBuilder.assertionConsumerUrl(sso.getAssertionConsumerServiceUrl()); } return authnRequestBuilder; } 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 a52cdc2ed2..d92884fd5d 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 @@ -31,6 +31,7 @@ import org.keycloak.adapters.saml.rotation.SamlDescriptorPublicKeyLocator; import org.keycloak.rotation.CompositeKeyLocator; import org.keycloak.rotation.HardcodedKeyLocator; import org.keycloak.rotation.KeyLocator; +import java.net.URI; /** * @author Bill Burke @@ -45,6 +46,7 @@ public class DefaultSamlDeployment implements SamlDeployment { private Binding requestBinding; private Binding responseBinding; private String requestBindingUrl; + private URI assertionConsumerServiceUrl; @Override public boolean signRequest() { @@ -76,6 +78,15 @@ public class DefaultSamlDeployment implements SamlDeployment { return requestBindingUrl; } + @Override + public URI getAssertionConsumerServiceUrl() { + return assertionConsumerServiceUrl; + } + + public void setAssertionConsumerServiceUrl(URI assertionConsumerServiceUrl) { + this.assertionConsumerServiceUrl = assertionConsumerServiceUrl; + } + public void setSignRequest(boolean signRequest) { this.signRequest = signRequest; } @@ -277,7 +288,6 @@ public class DefaultSamlDeployment implements SamlDeployment { private boolean turnOffChangeSessionIdOnLogin; private PrivateKey decryptionKey; private KeyPair signingKeyPair; - private String assertionConsumerServiceUrl; private Set roleAttributeNames; private PrincipalNamePolicy principalNamePolicy = PrincipalNamePolicy.FROM_NAME_ID; private String principalAttributeName; @@ -340,11 +350,6 @@ public class DefaultSamlDeployment implements SamlDeployment { return signingKeyPair; } - @Override - public String getAssertionConsumerServiceUrl() { - return assertionConsumerServiceUrl; - } - @Override public Set getRoleAttributeNames() { return roleAttributeNames; @@ -396,10 +401,6 @@ public class DefaultSamlDeployment implements SamlDeployment { this.signingKeyPair = signingKeyPair; } - public void setAssertionConsumerServiceUrl(String assertionConsumerServiceUrl) { - this.assertionConsumerServiceUrl = assertionConsumerServiceUrl; - } - public void setRoleAttributeNames(Set roleAttributeNames) { this.roleAttributeNames = roleAttributeNames; } 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 444217785b..a9df7e9461 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 @@ -25,6 +25,7 @@ import java.security.PrivateKey; import java.util.Set; import org.apache.http.client.HttpClient; import org.keycloak.rotation.KeyLocator; +import java.net.URI; /** * Represents SAML deployment configuration. @@ -103,8 +104,24 @@ public interface SamlDeployment { */ boolean validateAssertionSignature(); Binding getRequestBinding(); + /** + * SAML allows the client to request what binding type it wants authn responses to use. The default is + * that the client will not request a specific binding type for responses. + * @return + */ Binding getResponseBinding(); + /** + * Returns URL for the IDP login service that the client will send requests to. + * @return + */ String getRequestBindingUrl(); + /** + * Returns URI where the IdP should send the responses to. The default is + * that the client will not request a specific assertion consumer service URL. + * This property is typically accompanied by the ProtocolBinding attribute. + * @return + */ + URI getAssertionConsumerServiceUrl(); } public interface SingleLogoutService { @@ -141,7 +158,6 @@ public interface SamlDeployment { KeyPair getSigningKeyPair(); String getSignatureCanonicalizationMethod(); SignatureAlgorithm getSignatureAlgorithm(); - String getAssertionConsumerServiceUrl(); String getLogoutPage(); Set getRoleAttributeNames(); 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 de95d877a7..76d7648425 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 @@ -32,6 +32,7 @@ public class IDP implements Serializable { private String requestBinding; private String responseBinding; private String bindingUrl; + private String assertionConsumerServiceUrl; private boolean validateAssertionSignature; public boolean isSignRequest() { @@ -81,6 +82,14 @@ public class IDP implements Serializable { public void setBindingUrl(String bindingUrl) { this.bindingUrl = bindingUrl; } + + public String getAssertionConsumerServiceUrl() { + return assertionConsumerServiceUrl; + } + + public void setAssertionConsumerServiceUrl(String assertionConsumerServiceUrl) { + this.assertionConsumerServiceUrl = assertionConsumerServiceUrl; + } } public static class SingleLogoutService implements Serializable { 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 1a3dd04850..256bfa2a97 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,6 +72,7 @@ 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 ASSERTION_CONSUMER_SERVICE_URL_ATTR = "assertionConsumerServiceUrl"; public static final String HTTP_CLIENT_ELEMENT = "HttpClient"; public static final String ALLOW_ANY_HOSTNAME_ATTR = "allowAnyHostname"; 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 7af71bac1e..b5f8abad22 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 @@ -41,6 +41,7 @@ import java.security.cert.Certificate; import java.util.HashSet; import java.util.Set; import org.keycloak.adapters.cloned.HttpClientBuilder; +import java.net.URI; /** * @author Bill Burke @@ -156,6 +157,12 @@ public class DeploymentBuilder { if (sp.getIdp().getSingleSignOnService().getResponseBinding() != null) { sso.setResponseBinding(SamlDeployment.Binding.parseBinding(sp.getIdp().getSingleSignOnService().getResponseBinding())); } + if (sp.getIdp().getSingleSignOnService().getAssertionConsumerServiceUrl() != null) { + if (! sp.getIdp().getSingleSignOnService().getAssertionConsumerServiceUrl().endsWith("/saml")) { + throw new RuntimeException("AssertionConsumerServiceUrl must end with \"/saml\"."); + } + sso.setAssertionConsumerServiceUrl(URI.create(sp.getIdp().getSingleSignOnService().getAssertionConsumerServiceUrl())); + } sso.setSignRequest(sp.getIdp().getSingleSignOnService().isSignRequest()); sso.setValidateResponseSignature(sp.getIdp().getSingleSignOnService().isValidateResponseSignature()); sso.setValidateAssertionSignature(sp.getIdp().getSingleSignOnService().isValidateAssertionSignature()); 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 be54223843..839d4f6abd 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 @@ -118,6 +118,7 @@ public class IDPXmlParser extends AbstractParser { sso.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR)); sso.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR)); sso.setBindingUrl(getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR)); + sso.setAssertionConsumerServiceUrl(getAttributeValue(element, ConfigXmlConstants.ASSERTION_CONSUMER_SERVICE_URL_ATTR)); return sso; } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java index 96cf6f8e55..7f40e5aed3 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/ecp/EcpAuthenticationHandler.java @@ -156,7 +156,15 @@ public class EcpAuthenticationHandler extends AbstractSamlAuthenticationHandler paosRequestHeader.setMustUnderstand(true); paosRequestHeader.setActor("http://schemas.xmlsoap.org/soap/actor/next"); paosRequestHeader.addAttribute(envelope.createName("service"), JBossSAMLURIConstants.ECP_PROFILE.get()); - paosRequestHeader.addAttribute(envelope.createName("responseConsumerURL"), deployment.getAssertionConsumerServiceUrl()); + paosRequestHeader.addAttribute(envelope.createName("responseConsumerURL"), getResponseConsumerUrl()); + } + + private String getResponseConsumerUrl() { + return (deployment.getIDP() == null + || deployment.getIDP().getSingleSignOnService() == null + || deployment.getIDP().getSingleSignOnService().getAssertionConsumerServiceUrl() == null + ) ? null + : deployment.getIDP().getSingleSignOnService().getAssertionConsumerServiceUrl().toString(); } }; } diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd new file mode 100644 index 0000000000..a5169d3c35 --- /dev/null +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_8.xsd @@ -0,0 +1,456 @@ + + + + + + + + + + Keycloak SAML Adapter configuration file. + + + + + Describes SAML service provider configuration. + + + + + + + + + + + List of service provider encryption and validation keys. + + If the IDP requires that the client application (SP) sign all of its requests and/or if the IDP will encrypt assertions, you must define the keys used to do this. For client signed documents you must define both the private and public key or certificate that will be used to sign documents. For encryption, you only have to define the private key that will be used to decrypt. + + + + + When creating a Java Principal object that you obtain from methods like HttpServletRequest.getUserPrincipal(), you can define what name that is returned by the Principal.getName() method. + + + + + Defines what SAML attributes within the assertion received from the user should be used as role identifiers within the Java EE Security Context for the user. + By default Role attribute values are converted to Java EE roles. Some IDPs send roles via a member or memberOf attribute assertion. You can define one or more Attribute elements to specify which SAML attributes must be converted into roles. + + + + + Describes configuration of SAML identity provider for this service provider. + + + + + + This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it. + + + + + SSL policy the adapter will enforce. + + + + + SAML clients can request a specific NameID Subject format. Fill in this value if you want a specific format. It must be a standard SAML format identifier, i.e. urn:oasis:names:tc:SAML:2.0:nameid-format:transient. By default, no special format is requested. + + + + + URL of the logout page. + + + + + SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false. + + + + + SAML clients can request that a user is never asked to authenticate even if they are not logged in at the IDP. Set this to true if you want this. Do not use together with forceAuthentication as they are opposite. Default value is false. + + + + + The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true to disable this. It is recommended you do not turn it off. Default value is false. + + + + + + + + + Describes a single key used for signing or encryption. + + + + + + + + + Java keystore to load keys and certificates from. + + + + + Private key (PEM format) + + + + + Public key (PEM format) + + + + + Certificate key (PEM format) + + + + + + Flag defining whether the key should be used for signing. + + + + + Flag defining whether the key should be used for encryption + + + + + + + + Private key declaration + + + + + Certificate declaration + + + + + + File path to the key store. + + + + + WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream(). + + + + + The password of the key store. + + + + + + + Alias that points to the key or cert within the keystore. + + + + + Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute. + + + + + + + Alias that points to the key or cert within the keystore. + + + + + + + Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal(). + + + + + Name of the SAML assertion attribute to use within. + + + + + + + + This policy just uses whatever the SAML subject value is. This is the default setting + + + + + This will pull the value from one of the attributes declared in the SAML assertion received from the server. You'll need to specify the name of the SAML assertion attribute to use within the attribute XML attribute. + + + + + + + + + All requests must come in via HTTPS. + + + + + Only non-private IP addresses must come over the wire via HTTPS. + + + + + no requests are required to come over via HTTPS. + + + + + + + + + + + + + + + + + + + + + + + Specifies SAML attribute to be converted into roles. + + + + + + + + Specifies name of the SAML attribute to be converted into roles. + + + + + + + + Configuration of the login SAML endpoint of the IDP. + + + + + Configuration of the logout SAML endpoint of the IDP + + + + + The Keys sub element of IDP is only used to define the certificate or public key to use to verify documents signed by the IDP. + + + + + Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP. + + + + + + issuer ID of the IDP. + + + + + If set to true, the client adapter will sign every document it sends to the IDP. Also, the client will expect that the IDP will be signing any documents sent to it. This switch sets the default for all request and response types. + + + + + Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256 + + + + + This is the signature canonicalization method that the IDP expects signed documents to use. The default value is https://www.w3.org/2001/10/xml-exc-c14n# and should be good for most IDPs. + + + + + + + + + + + + Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect the IDP to sign the assertion response document sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect the IDP to sign the individual assertions sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is. + + + + + SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well. + + + + + SAML allows the client to request what binding type it wants authn responses to use. This value maps to ProtocolBinding attribute in SAML AuthnRequest. The default is that the client will not request a specific binding type for responses. + + + + + This is the URL for the IDP login service that the client will send requests to. + + + + + URL of the assertion consumer service (ACS) where the IDP login service should send responses to. By default it is unset, relying on the IdP settings. When set, it must end in "/saml". This property is typically accompanied by the responseBinding attribute. + + + + + + + + Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is. + + + + + This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST. + + + + + This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST. + + + + + This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding. + + + + + This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding. + + + + + + + + If the the IDP server requires HTTPS and this config option is set to true the IDP's certificate + is validated via the truststore, but host name validation is not done. This setting should only be used during + development and never in production as it will partly disable verification of SSL certificates. + This seting may be useful in test environments. The default value is false. + + + + + This is the file path to a keystore file. This keystore contains client certificate + for two-way SSL when the adapter makes HTTPS requests to the IDP server. + + + + + Password for the client keystore and for the client's key. + + + + + Defines number of pooled connections. + + + + + If the the IDP server requires HTTPS and this config option is set to true you do not have to specify a truststore. + This setting should only be used during development and never in production as it will disable verification of SSL certificates. + The default value is false. + + + + + URL to HTTP proxy to use for HTTP connections. + + + + + The value is the file path to a keystore file. If you prefix the path with classpath:, + then the truststore will be obtained from the deployment's classpath instead. Used for outgoing + HTTPS communications to the IDP server. Client making HTTPS requests need + a way to verify the host of the server they are talking to. This is what the trustore does. + The keystore contains one or more trusted host certificates or certificate authorities. + You can create this truststore by extracting the public certificate of the IDP's SSL keystore. + + + + + + Password for the truststore keystore. + + + + + 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 ec4fc28a84..916f7f3a0e 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java +++ b/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java @@ -68,6 +68,11 @@ public class SAML2AuthnRequestBuilder implements SamlProtocolExtensionsAwareBuil return this; } + public SAML2AuthnRequestBuilder assertionConsumerUrl(URI assertionConsumerUrl) { + this.authnRequestType.setAssertionConsumerServiceURL(assertionConsumerUrl); + return this; + } + public SAML2AuthnRequestBuilder forceAuthn(boolean forceAuthn) { this.authnRequestType.setForceAuthn(forceAuthn); return this; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeAcsServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeAcsServlet.java new file mode 100644 index 0000000000..a1da272a61 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeAcsServlet.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 mhajas + */ +public class EmployeeAcsServlet extends SAMLServlet { + public static final String DEPLOYMENT_NAME = "employee-acs"; + + @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/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java index 3b4f48df73..2f1f8419d8 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java @@ -111,12 +111,15 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { if (authServerSslRequired) { modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.https.port")); modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "http", "https"); + modifyDocElementAttribute(doc, "SingleSignOnService", "assertionConsumerServiceUrl", "8081", System.getProperty("app.server.http.port")); + modifyDocElementAttribute(doc, "SingleSignOnService", "assertionConsumerServiceUrl", "http", "https"); modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", "8080", System.getProperty("auth.server.https.port")); modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", "http", "https"); modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "8080", System.getProperty("auth.server.https.port")); modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "http", "https"); } else { modifyDocElementAttribute(doc, "SingleSignOnService", "bindingUrl", "8080", System.getProperty("auth.server.http.port")); + modifyDocElementAttribute(doc, "SingleSignOnService", "assertionConsumerServiceUrl", "8081", System.getProperty("app.server.http.port")); modifyDocElementAttribute(doc, "SingleLogoutService", "postBindingUrl", "8080", System.getProperty("auth.server.http.port")); modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "8080", System.getProperty("auth.server.http.port")); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java index d89fe67fd0..ce92fb8bd4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLFilterServletAdapterTest.java @@ -18,6 +18,7 @@ public abstract class AbstractSAMLFilterServletAdapterTest extends AbstractSAMLS public void checkRoles() { badClientSalesPostSigServletPage.checkRoles(true); badRealmSalesPostSigServletPage.checkRoles(true); + employeeAcsServletPage.checkRoles(true); employeeSigServletPage.checkRoles(true); employeeSigFrontServletPage.checkRoles(true); salesMetadataServletPage.checkRoles(true); @@ -48,6 +49,7 @@ public abstract class AbstractSAMLFilterServletAdapterTest extends AbstractSAMLS public void uncheckRoles() { badClientSalesPostSigServletPage.checkRoles(false); badRealmSalesPostSigServletPage.checkRoles(false); + employeeAcsServletPage.checkRoles(false); employee2ServletPage.checkRoles(false); employeeSigServletPage.checkRoles(false); employeeSigFrontServletPage.checkRoles(false); 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 a199636e57..2795a2d21c 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 @@ -57,36 +57,14 @@ import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; -import org.keycloak.testsuite.adapter.page.BadAssertionSalesPostSig; -import org.keycloak.testsuite.adapter.page.BadClientSalesPostSigServlet; -import org.keycloak.testsuite.adapter.page.BadRealmSalesPostSigServlet; -import org.keycloak.testsuite.adapter.page.DifferentCookieNameServlet; -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; -import org.keycloak.testsuite.adapter.page.SAMLServlet; -import org.keycloak.testsuite.adapter.page.SalesMetadataServlet; -import org.keycloak.testsuite.adapter.page.SalesPost2Servlet; -import org.keycloak.testsuite.adapter.page.SalesPostAssertionAndResponseSig; -import org.keycloak.testsuite.adapter.page.SalesPostEncServlet; -import org.keycloak.testsuite.adapter.page.SalesPostPassiveServlet; -import org.keycloak.testsuite.adapter.page.SalesPostServlet; -import org.keycloak.testsuite.adapter.page.SalesPostSigEmailServlet; -import org.keycloak.testsuite.adapter.page.SalesPostSigPersistentServlet; -import org.keycloak.testsuite.adapter.page.SalesPostSigServlet; -import org.keycloak.testsuite.adapter.page.SalesPostSigTransientServlet; +import org.keycloak.testsuite.adapter.page.*; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.auth.page.login.Login; import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin; import org.keycloak.testsuite.page.AbstractPage; import org.keycloak.testsuite.util.*; +import org.keycloak.testsuite.util.SamlClient.Binding; import org.openqa.selenium.By; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -129,6 +107,7 @@ import static org.keycloak.testsuite.util.IOUtil.loadXML; import static org.keycloak.testsuite.util.IOUtil.modifyDocElementAttribute; import static org.keycloak.testsuite.util.Matchers.bodyHC; import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; +import static org.keycloak.testsuite.util.SamlClient.Binding.POST; import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLogin; import static org.keycloak.testsuite.util.SamlClient.login; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; @@ -144,6 +123,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Page protected BadRealmSalesPostSigServlet badRealmSalesPostSigServletPage; + @Page + protected EmployeeAcsServlet employeeAcsServletPage; + @Page protected Employee2Servlet employee2ServletPage; @@ -227,6 +209,11 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd return samlServletDeployment(BadRealmSalesPostSigServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); } + @Deployment(name = EmployeeAcsServlet.DEPLOYMENT_NAME) + protected static WebArchive employeeAssertionConsumerServiceUrlSet() { + return samlServletDeployment(EmployeeAcsServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); + } + @Deployment(name = Employee2Servlet.DEPLOYMENT_NAME) protected static WebArchive employee2() { return samlServletDeployment(Employee2Servlet.DEPLOYMENT_NAME, SendUsernameServlet.class); @@ -467,6 +454,20 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd testSuccessfulAndUnauthorizedLogin(employeeSigServletPage, testRealmSAMLRedirectLoginPage); } + @Test + public void employeeAcsTest() { + SAMLDocumentHolder samlResponse = new SamlClient(employeeAcsServletPage.buildUri()).getSamlResponse(Binding.POST, (client, context, strategy) -> { + strategy.setRedirectable(false); + return client.execute(new HttpGet(employeeAcsServletPage.buildUri()), context); + }); + + assertThat(samlResponse.getSamlObject(), instanceOf(AuthnRequestType.class)); + assertThat(((AuthnRequestType) samlResponse.getSamlObject()).getAssertionConsumerServiceURL(), notNullValue()); + assertThat(((AuthnRequestType) samlResponse.getSamlObject()).getAssertionConsumerServiceURL().getPath(), is("/employee-acs/a/different/endpoint/for/saml")); + + assertSuccessfulLogin(employeeAcsServletPage, bburkeUser, testRealmSAMLPostLoginPage, "principal=bburke"); + } + private static final KeyPair NEW_KEY_PAIR = KeyUtils.generateRsaKeyPair(1024); private static final String NEW_KEY_PRIVATE_KEY_PEM = PemUtils.encodeKey(NEW_KEY_PAIR.getPrivate()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-acs/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-acs/WEB-INF/keycloak-saml.xml new file mode 100644 index 0000000000..4803920530 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-acs/WEB-INF/keycloak-saml.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + \ No newline at end of file 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 87a4fc8d67..25e1f21d29 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 @@ -454,6 +454,56 @@ } ] }, + { + "clientId": "http://localhost:8081/employee-acs/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/employee-acs", + "redirectUris": [ + "http://localhost:8080/employee-acs/*" + ], + "adminUrl": "http://localhost:8080/employee-acs", + "attributes": { + "saml.authnstatement": "true" + }, + "protocolMappers": [ + { + "name": "email", + "protocol": "saml", + "protocolMapper": "saml-user-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "friendly.name": "email", + "attribute.name": "urn:oid:1.2.840.113549.1.9.1", + "attribute.nameformat": "URI Reference" + } + }, + { + "name": "phone", + "protocol": "saml", + "protocolMapper": "saml-user-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "phone", + "attribute.name": "phone", + "attribute.nameformat": "Basic" + } + }, + { + "name": "role-list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "attribute.name": "Role", + "attribute.nameformat": "Basic", + "single": "false" + } + } + ] + }, { "clientId": "http://localhost:8081/employee2/", "enabled": true,