diff --git a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
index 00160e6e53..335a72d11e 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
@@ -56,10 +56,8 @@ public class SAMLRequestParser {
is = new ByteArrayInputStream(message.getBytes(GeneralConstants.SAML_CHARSET));
}
- SAML2Request saml2Request = new SAML2Request();
try {
- saml2Request.getSAML2ObjectFromStream(is);
- return saml2Request.getSamlDocumentHolder();
+ return SAML2Request.getSAML2ObjectFromStream(is);
} catch (Exception e) {
logger.samlBase64DecodingError(e);
}
@@ -76,10 +74,8 @@ public class SAMLRequestParser {
log.debug(str);
}
is = new ByteArrayInputStream(samlBytes);
- SAML2Request saml2Request = new SAML2Request();
try {
- saml2Request.getSAML2ObjectFromStream(is);
- return saml2Request.getSamlDocumentHolder();
+ return SAML2Request.getSAML2ObjectFromStream(is);
} catch (Exception e) {
logger.samlBase64DecodingError(e);
}
diff --git a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
index 2bfa41f9db..cb8a348631 100755
--- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
+++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java
@@ -156,7 +156,7 @@ public class SAML2Request {
* @throws IOException
* @throws ParsingException
*/
- public SAML2Object getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException,
+ public static SAMLDocumentHolder getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException,
ProcessingException {
if (is == null)
throw logger.nullArgumentError("InputStream");
@@ -167,8 +167,7 @@ public class SAML2Request {
JAXPValidationUtil.checkSchemaValidation(samlDocument);
SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument);
- samlDocumentHolder = new SAMLDocumentHolder(requestType, samlDocument);
- return requestType;
+ return new SAMLDocumentHolder(requestType, samlDocument);
}
/**
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 e0ac524b8e..589dde3e6b 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -138,6 +138,13 @@ public class SamlService extends AuthorizationEndpointBase {
protected Response handleSamlResponse(String samlResponse, String relayState) {
event.event(EventType.LOGOUT);
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
+
+ if (! (holder.getSamlObject() instanceof StatusResponseType)) {
+ event.detail(Details.REASON, "invalid_saml_response");
+ event.error(Errors.INVALID_SAML_RESPONSE);
+ return ErrorPage.error(session, Messages.INVALID_REQUEST);
+ }
+
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
// validate destination
if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
@@ -178,6 +185,12 @@ public class SamlService extends AuthorizationEndpointBase {
SAML2Object samlObject = documentHolder.getSamlObject();
+ if (! (samlObject instanceof RequestAbstractType)) {
+ event.event(EventType.LOGIN);
+ event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
+ return ErrorPage.error(session, Messages.INVALID_REQUEST);
+ }
+
RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.getClientByClientId(issuer);
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 79f241b129..b2c06ad85d 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -78,6 +78,11 @@
junit
compile
+
+ org.hamcrest
+ hamcrest-all
+ compile
+
org.subethamail
subethasmtp
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java
index 0272a1bd1a..e967a33042 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java
@@ -4,7 +4,6 @@ import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.representations.idm.ClientRepresentation;
import java.io.Closeable;
import java.util.HashMap;
-import java.util.Map;
/**
*
@@ -12,14 +11,14 @@ import java.util.Map;
*/
public class ClientAttributeUpdater {
- private final Map originalAttributes = new HashMap<>();
-
private final ClientResource clientResource;
private final ClientRepresentation rep;
+ private final ClientRepresentation origRep;
public ClientAttributeUpdater(ClientResource clientResource) {
this.clientResource = clientResource;
+ this.origRep = clientResource.toRepresentation();
this.rep = clientResource.toRepresentation();
if (this.rep.getAttributes() == null) {
this.rep.setAttributes(new HashMap<>());
@@ -27,29 +26,23 @@ public class ClientAttributeUpdater {
}
public ClientAttributeUpdater setAttribute(String name, String value) {
- if (! originalAttributes.containsKey(name)) {
- this.originalAttributes.put(name, this.rep.getAttributes().put(name, value));
- } else {
- this.rep.getAttributes().put(name, value);
- }
+ this.rep.getAttributes().put(name, value);
return this;
}
public ClientAttributeUpdater removeAttribute(String name) {
- if (! originalAttributes.containsKey(name)) {
- this.originalAttributes.put(name, this.rep.getAttributes().put(name, null));
- } else {
- this.rep.getAttributes().put(name, null);
- }
+ this.rep.getAttributes().put(name, null);
+ return this;
+ }
+
+ public ClientAttributeUpdater setFrontchannelLogout(Boolean frontchannelLogout) {
+ rep.setFrontchannelLogout(frontchannelLogout);
return this;
}
public Closeable update() {
clientResource.update(rep);
- return () -> {
- rep.getAttributes().putAll(originalAttributes);
- clientResource.update(rep);
- };
+ return () -> clientResource.update(origRep);
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/KeyUtils.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java
new file mode 100644
index 0000000000..06609b1f63
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2017 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.util;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.keycloak.common.util.KeyUtils;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.saml.BaseSAML2BindingBuilder;
+import org.keycloak.saml.SAMLRequestParser;
+import org.keycloak.saml.SignatureAlgorithm;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.exceptions.ConfigurationException;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.w3c.dom.Document;
+
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import org.jboss.logging.Logger;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+/**
+ * @author hmlnarik
+ */
+public class SamlClient {
+
+ @FunctionalInterface
+ public interface Step {
+ HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface ResultExtractor {
+ T extract(CloseableHttpResponse response) throws Exception;
+ }
+
+ public static final class DoNotFollowRedirectStep implements Step {
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI uri, CloseableHttpResponse response, HttpClientContext context) throws Exception {
+ return null;
+ }
+ }
+
+ public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy {
+
+ public boolean redirectable = true;
+
+ @Override
+ protected boolean isRedirectable(String method) {
+ return redirectable && super.isRedirectable(method);
+ }
+
+ public void setRedirectable(boolean redirectable) {
+ this.redirectable = redirectable;
+ }
+ }
+
+ /**
+ * SAML bindings and related HttpClient methods.
+ */
+ public enum Binding {
+ POST {
+ @Override
+ public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
+ assertThat(response, statusCodeIsHC(Response.Status.OK));
+ String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
+ response.close();
+ return extractSamlResponseFromForm(responsePage);
+ }
+
+ @Override
+ public HttpPost createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) {
+ return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, null, null);
+ }
+
+ @Override
+ public HttpPost createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) {
+ return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_RESPONSE_KEY, null, null);
+ }
+
+ @Override
+ public HttpPost createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
+ return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, realmPrivateKey, realmPublicKey);
+ }
+
+ private HttpPost createSamlPostMessage(URI samlEndpoint, String relayState, Document samlRequest, String messageType, String privateKeyStr, String publicKeyStr) {
+ HttpPost post = new HttpPost(samlEndpoint);
+
+ List parameters = new LinkedList<>();
+
+
+ try {
+ BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
+
+ if (privateKeyStr != null && publicKeyStr != null) {
+ PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr);
+ PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr);
+ binding
+ .signatureAlgorithm(SignatureAlgorithm.RSA_SHA256)
+ .signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey)
+ .signDocument();
+ }
+
+ parameters.add(
+ new BasicNameValuePair(messageType,
+ binding
+ .postBinding(samlRequest)
+ .encoded())
+ );
+ } catch (IOException | ConfigurationException | ProcessingException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ if (relayState != null) {
+ parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState));
+ }
+
+ UrlEncodedFormEntity formEntity;
+
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+
+ post.setEntity(formEntity);
+
+ return post;
+ }
+
+ @Override
+ public URI getBindingUri() {
+ return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
+ }
+ },
+
+ REDIRECT {
+ @Override
+ public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
+ assertThat(response, statusCodeIsHC(Response.Status.FOUND));
+ String location = response.getFirstHeader("Location").getValue();
+ response.close();
+ return extractSamlResponseFromRedirect(location);
+ }
+
+ @Override
+ public HttpGet createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) {
+ try {
+ URI requestURI = new BaseSAML2BindingBuilder()
+ .relayState(relayState)
+ .redirectBinding(samlRequest)
+ .requestURI(samlEndpoint.toString());
+ return new HttpGet(requestURI);
+ } catch (ProcessingException | ConfigurationException | IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public URI getBindingUri() {
+ return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
+ }
+
+ @Override
+ public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) {
+ return null;
+ }
+
+ @Override
+ public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
+ return null;
+ }
+ };
+
+ public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException;
+
+ public abstract HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest);
+
+ public abstract HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey);
+
+ public abstract URI getBindingUri();
+
+ public abstract HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest);
+ }
+
+ private static final Logger LOG = Logger.getLogger(SamlClient.class);
+
+ private final HttpClientContext context = HttpClientContext.create();
+
+ private final RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect();
+
+ /**
+ * Extracts and parses value of SAMLResponse input field of a form present in the given page.
+ *
+ * @param responsePage HTML code of the page
+ * @return
+ */
+ public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
+ org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
+ Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
+ Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
+ int size = samlResponses.size() + samlRequests.size();
+ assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
+
+ Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
+
+ return SAMLRequestParser.parseResponsePostBinding(respElement.val());
+ }
+
+ /**
+ * Extracts and parses value of SAMLResponse query parameter from the given URI.
+ *
+ * @param responseUri
+ * @return
+ */
+ public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
+ List params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
+
+ String samlDoc = null;
+ for (NameValuePair param : params) {
+ if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
+ assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue());
+ samlDoc = param.getValue();
+ }
+ }
+
+ return SAMLRequestParser.parseResponseRedirectBinding(samlDoc);
+ }
+
+ /**
+ * Creates a SAML login request document with the given parameters. See SAML <AuthnRequest> description for more details.
+ *
+ * @param issuer
+ * @param assertionConsumerURL
+ * @param destination
+ * @return
+ */
+ public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) {
+ try {
+ SAML2Request samlReq = new SAML2Request();
+ AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer);
+
+ return loginReq;
+ } catch (ConfigurationException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public T executeAndTransform(ResultExtractor resultTransformer, List steps) {
+ CloseableHttpResponse currentResponse = null;
+ URI currentUri = URI.create("about:blank");
+ strategy.setRedirectable(true);
+
+ try (CloseableHttpClient client = createHttpClientBuilderInstance().setRedirectStrategy(strategy).build()) {
+ for (int i = 0; i < steps.size(); i ++) {
+ Step s = steps.get(i);
+ LOG.infof("Running step %d: %s", i, s.getClass());
+
+ CloseableHttpResponse origResponse = currentResponse;
+
+ HttpUriRequest request = s.perform(client, currentUri, origResponse, context);
+ if (request == null) {
+ LOG.info("Last step returned no request, continuing with next step.");
+ continue;
+ }
+
+ // Setting of follow redirects has to be set before executing the final request of the current step
+ if (i < steps.size() - 1 && steps.get(i + 1) instanceof DoNotFollowRedirectStep) {
+ LOG.debugf("Disabling following redirects");
+ strategy.setRedirectable(false);
+ i++;
+ } else {
+ strategy.setRedirectable(true);
+ }
+
+ LOG.infof("Executing HTTP request to %s", request.getURI());
+ currentResponse = client.execute(request, context);
+
+ currentUri = request.getURI();
+ List locations = context.getRedirectLocations();
+ if (locations != null && ! locations.isEmpty()) {
+ currentUri = locations.get(locations.size() - 1);
+ }
+
+ LOG.infof("Landed to %s", currentUri);
+
+ if (currentResponse != origResponse && origResponse != null) {
+ origResponse.close();
+ }
+ }
+
+ LOG.info("Going to extract response");
+
+ return resultTransformer.extract(currentResponse);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public HttpClientContext getContext() {
+ return context;
+ }
+
+ protected HttpClientBuilder createHttpClientBuilderInstance() {
+ return HttpClientBuilder.create();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
new file mode 100644
index 0000000000..89d309249c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 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.util;
+
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep;
+import org.keycloak.testsuite.util.SamlClient.ResultExtractor;
+import org.keycloak.testsuite.util.SamlClient.Step;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.keycloak.testsuite.util.saml.CreateAuthnRequestStepBuilder;
+import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder;
+import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder;
+import org.keycloak.testsuite.util.saml.LoginBuilder;
+import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder;
+import org.keycloak.testsuite.util.saml.RequiredConsentBuilder;
+import org.w3c.dom.Document;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlClientBuilder {
+
+ private final List steps = new LinkedList<>();
+
+ public SamlClient execute(Consumer resultConsumer) {
+ final SamlClient samlClient = new SamlClient();
+ samlClient.executeAndTransform(r -> {
+ resultConsumer.accept(r);
+ return null;
+ }, steps);
+ return samlClient;
+ }
+
+ public T executeAndTransform(ResultExtractor resultTransformer) {
+ return new SamlClient().executeAndTransform(resultTransformer, steps);
+ }
+
+ public List getSteps() {
+ return steps;
+ }
+
+ public T addStep(T step) {
+ steps.add(step);
+ return step;
+ }
+
+ public SamlClientBuilder doNotFollowRedirects() {
+ this.steps.add(new DoNotFollowRedirectStep());
+ return this;
+ }
+
+ public SamlClientBuilder clearCookies() {
+ this.steps.add((client, currentURI, currentResponse, context) -> {
+ context.getCookieStore().clear();
+ return null;
+ });
+ return this;
+ }
+
+ /** Creates fresh and issues an AuthnRequest to the SAML endpoint */
+ public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) {
+ return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this));
+ }
+
+ /** Issues the given AuthnRequest to the SAML endpoint */
+ public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) {
+ return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this));
+ }
+
+ /** Issues the given AuthnRequest to the SAML endpoint */
+ public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) {
+ return addStep(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this));
+ }
+
+ /** Handles login page */
+ public LoginBuilder login() {
+ return addStep(new LoginBuilder(this));
+ }
+
+ /** Starts IdP-initiated flow for the given client */
+ public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) {
+ return addStep(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this));
+ }
+
+ /** Handles "Requires consent" page */
+ public RequiredConsentBuilder consentRequired() {
+ return addStep(new RequiredConsentBuilder(this));
+ }
+
+ /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */
+ public SAMLDocumentHolder getSamlResponse(Binding responseBinding) {
+ return
+ doNotFollowRedirects()
+ .executeAndTransform(responseBinding::extractResponse);
+ }
+
+ /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */
+ public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) {
+ return
+ doNotFollowRedirects()
+ .addStep(new ModifySamlResponseStepBuilder(responseBinding, this));
+ }
+
+ public SamlClientBuilder navigateTo(String httpGetUri) {
+ steps.add((client, currentURI, currentResponse, context) -> {
+ return new HttpGet(httpGetUri);
+ });
+ return this;
+ }
+
+ public SamlClientBuilder navigateTo(URI httpGetUri) {
+ steps.add((client, currentURI, currentResponse, context) -> {
+ return new HttpGet(httpGetUri);
+ });
+ return this;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java
similarity index 100%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
similarity index 95%
rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
index ccd5377865..d76a6dd56c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
@@ -6,7 +6,6 @@
package org.keycloak.testsuite.util.matchers;
import org.keycloak.dom.saml.v2.SAML2Object;
-import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import java.net.URI;
import org.hamcrest.*;
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java
new file mode 100644
index 0000000000..aa85821179
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.saml.common.exceptions.ConfigurationException;
+import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.common.util.DocumentUtil;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import java.net.URI;
+import java.util.UUID;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.w3c.dom.Document;
+
+
+public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder {
+
+ private final String issuer;
+ private final URI authServerSamlUrl;
+ private final Binding requestBinding;
+ private final String assertionConsumerURL;
+
+ private final Document forceLoginRequestDocument;
+
+ private String relayState;
+
+ public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.issuer = issuer;
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.requestBinding = requestBinding;
+ this.assertionConsumerURL = assertionConsumerURL;
+
+ this.forceLoginRequestDocument = null;
+ }
+
+ public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, Document loginRequestDocument, Binding requestBinding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.forceLoginRequestDocument = loginRequestDocument;
+
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.requestBinding = requestBinding;
+
+ this.issuer = null;
+ this.assertionConsumerURL = null;
+ }
+
+ public String assertionConsumerURL() {
+ return assertionConsumerURL;
+ }
+
+ public String relayState() {
+ return relayState;
+ }
+
+ public void relayState(String relayState) {
+ this.relayState = relayState;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ Document doc = createLoginRequestDocument();
+
+ String documentAsString = DocumentUtil.getDocumentAsString(doc);
+ String transformed = getTransformer().transform(documentAsString);
+
+ if (transformed == null) {
+ return null;
+ }
+
+ return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, DocumentUtil.getDocument(transformed));
+ }
+
+ protected Document createLoginRequestDocument() {
+ if (this.forceLoginRequestDocument != null) {
+ return this.forceLoginRequestDocument;
+ }
+
+ try {
+ SAML2Request samlReq = new SAML2Request();
+ AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer);
+
+ return SAML2Request.convert(loginReq);
+ } catch (ConfigurationException | ParsingException | ProcessingException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
new file mode 100644
index 0000000000..ee594d0a22
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import org.keycloak.saml.SAML2LogoutRequestBuilder;
+import org.keycloak.saml.common.util.DocumentUtil;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import java.net.URI;
+import java.util.function.Supplier;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder {
+
+ private final URI authServerSamlUrl;
+ private final String issuer;
+ private final Binding requestBinding;
+
+ private Supplier sessionIndex = () -> null;
+ private Supplier nameId = () -> null;
+ private Supplier relayState = () -> null;
+
+ public CreateLogoutRequestStepBuilder(URI authServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.issuer = issuer;
+ this.requestBinding = requestBinding;
+ }
+
+ public String sessionIndex() {
+ return sessionIndex.get();
+ }
+
+ public CreateLogoutRequestStepBuilder sessionIndex(String sessionIndex) {
+ this.sessionIndex = () -> sessionIndex;
+ return this;
+ }
+
+ public CreateLogoutRequestStepBuilder sessionIndex(Supplier sessionIndex) {
+ this.sessionIndex = sessionIndex;
+ return this;
+ }
+
+ public String relayState() {
+ return relayState.get();
+ }
+
+ public CreateLogoutRequestStepBuilder relayState(String relayState) {
+ this.relayState = () -> relayState;
+ return this;
+ }
+
+ public CreateLogoutRequestStepBuilder relayState(Supplier relayState) {
+ this.relayState = relayState;
+ return this;
+ }
+
+ public NameIDType nameId() {
+ return nameId.get();
+ }
+
+ public CreateLogoutRequestStepBuilder nameId(NameIDType nameId) {
+ this.nameId = () -> nameId;
+ return this;
+ }
+
+ public CreateLogoutRequestStepBuilder nameId(Supplier nameId) {
+ this.nameId = nameId;
+ return this;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder()
+ .destination(authServerSamlUrl.toString())
+ .issuer(issuer)
+ .sessionIndex(sessionIndex());
+
+ if (nameId() != null) {
+ builder = builder.userPrincipal(nameId().getValue(), nameId().getFormat().toString());
+ }
+
+ String documentAsString = DocumentUtil.getDocumentAsString(builder.buildDocument());
+ String transformed = getTransformer().transform(documentAsString);
+
+ if (transformed == null) {
+ return null;
+ }
+
+ return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java
new file mode 100644
index 0000000000..d119e64772
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClient.Step;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.net.URI;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class IdPInitiatedLoginBuilder implements Step {
+
+ private final SamlClientBuilder clientBuilder;
+ private final URI authServerSamlUrl;
+ private final String clientId;
+
+ public IdPInitiatedLoginBuilder(URI authServerSamlUrl, String clientId, SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ this.authServerSamlUrl = authServerSamlUrl;
+ this.clientId = clientId;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ return new HttpGet(authServerSamlUrl.toString() + "/clients/" + this.clientId);
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java
new file mode 100644
index 0000000000..4e5713b403
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.util.SamlClient.Step;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.admin.Users.getPasswordOf;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class LoginBuilder implements Step {
+
+ private final SamlClientBuilder clientBuilder;
+ private UserRepresentation user;
+ private boolean sso = false;
+
+ public LoginBuilder(SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ if (sso) {
+ return null; // skip this step
+ } else {
+ assertThat(currentResponse, statusCodeIsHC(Response.Status.OK));
+ String loginPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8");
+ assertThat(loginPageText, containsString("login"));
+
+ return handleLoginPage(loginPageText);
+ }
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+
+ public LoginBuilder user(UserRepresentation user) {
+ this.user = user;
+ return this;
+ }
+
+ public LoginBuilder sso(boolean sso) {
+ this.sso = sso;
+ return this;
+ }
+
+ /**
+ * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected
+ * to have at least input fields with id "username" and "password".
+ *
+ * @param user
+ * @param loginPage
+ * @return
+ */
+ private HttpUriRequest handleLoginPage(String loginPage) {
+ return handleLoginPage(user, loginPage);
+ }
+
+ public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) {
+ String username = user.getUsername();
+ String password = getPasswordOf(user);
+ org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage);
+
+ List parameters = new LinkedList<>();
+ for (Element form : theLoginPage.getElementsByTag("form")) {
+ String method = form.attr("method");
+ String action = form.attr("action");
+ boolean isPost = method != null && "post".equalsIgnoreCase(method);
+
+ for (Element input : form.getElementsByTag("input")) {
+ if (Objects.equals(input.id(), "username")) {
+ parameters.add(new BasicNameValuePair(input.attr("name"), username));
+ } else if (Objects.equals(input.id(), "password")) {
+ parameters.add(new BasicNameValuePair(input.attr("name"), password));
+ } else {
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
+ }
+ }
+
+ if (isPost) {
+ HttpPost res = new HttpPost(action);
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ res.setEntity(formEntity);
+
+ return res;
+ } else {
+ UriBuilder b = UriBuilder.fromPath(action);
+ for (NameValuePair parameter : parameters) {
+ b.queryParam(parameter.getName(), parameter.getValue());
+ }
+ return new HttpGet(b.build());
+ }
+ }
+
+ throw new IllegalArgumentException("Invalid login form: " + loginPage);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java
new file mode 100644
index 0000000000..e29091bf86
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.processing.web.util.PostBindingUtil;
+import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import javax.ws.rs.core.Response.Status;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+
+public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder {
+
+ private final Binding binding;
+
+ private URI targetUri;
+ private String targetAttribute;
+ private Binding targetBinding;
+
+ public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) {
+ super(clientBuilder);
+ this.binding = binding;
+ this.targetBinding = binding;
+ }
+
+ // TODO: support for signing
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ switch (binding) {
+ case REDIRECT:
+ return handleRedirectBinding(currentResponse);
+
+ case POST:
+ return handlePostBinding(currentResponse);
+ }
+
+ throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName());
+ }
+
+ public Binding targetBinding() {
+ return targetBinding;
+ }
+
+ public ModifySamlResponseStepBuilder targetBinding(Binding targetBinding) {
+ this.targetBinding = targetBinding;
+ return this;
+ }
+
+ public String targetAttribute() {
+ return targetAttribute;
+ }
+
+ public ModifySamlResponseStepBuilder targetAttribute(String attribute) {
+ targetAttribute = attribute;
+ return this;
+ }
+
+ public ModifySamlResponseStepBuilder targetAttributeSamlRequest() {
+ return targetAttribute(GeneralConstants.SAML_REQUEST_KEY);
+ }
+
+ public ModifySamlResponseStepBuilder targetAttributeSamlResponse() {
+ return targetAttribute(GeneralConstants.SAML_RESPONSE_KEY);
+ }
+
+ public URI targetUri() {
+ return targetUri;
+ }
+
+ public ModifySamlResponseStepBuilder targetUri(URI forceUri) {
+ this.targetUri = forceUri;
+ return this;
+ }
+
+ protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException {
+ NameValuePair samlParam = null;
+
+ assertThat(currentResponse, statusCodeIsHC(Status.FOUND));
+ String location = currentResponse.getFirstHeader("Location").getValue();
+ URI locationUri = URI.create(location);
+
+ List params = URLEncodedUtils.parse(locationUri, "UTF-8");
+ for (Iterator it = params.iterator(); it.hasNext();) {
+ NameValuePair param = it.next();
+ if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
+ assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue());
+ samlParam = param;
+ it.remove();
+ }
+ }
+
+ assertThat(samlParam, notNullValue());
+
+ String base64EncodedSamlDoc = samlParam.getValue();
+ InputStream decoded = RedirectBindingUtil.base64DeflateDecode(base64EncodedSamlDoc);
+ String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
+ IOUtils.closeQuietly(decoded);
+
+ String transformed = getTransformer().transform(samlDoc);
+ if (transformed == null) {
+ return null;
+ }
+
+ final String attrName = this.targetAttribute != null ? this.targetAttribute : samlParam.getName();
+
+ return createRequest(locationUri, attrName, transformed, params);
+ }
+
+ private HttpUriRequest handlePostBinding(CloseableHttpResponse currentResponse) throws Exception {
+ assertThat(currentResponse, statusCodeIsHC(Status.OK));
+
+ org.jsoup.nodes.Document theResponsePage = Jsoup.parse(EntityUtils.toString(currentResponse.getEntity()));
+ Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
+ Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
+ Elements forms = theResponsePage.select("form");
+ Elements relayStates = theResponsePage.select("input[name=RelayState]");
+ int size = samlResponses.size() + samlRequests.size();
+ assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
+ assertThat("Checking uniqueness of forms in the page", forms, hasSize(1));
+
+ Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
+ Element form = forms.first();
+
+ String base64EncodedSamlDoc = respElement.val();
+ InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc);
+ String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET);
+ IOUtils.closeQuietly(decoded);
+
+ String transformed = getTransformer().transform(samlDoc);
+ if (transformed == null) {
+ return null;
+ }
+
+ final String attributeName = this.targetAttribute != null
+ ? this.targetAttribute
+ : respElement.attr("name");
+ List parameters = new LinkedList<>();
+
+ if (! relayStates.isEmpty()) {
+ parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayStates.first().val()));
+ }
+ URI locationUri = this.targetUri != null
+ ? this.targetUri
+ : URI.create(form.attr("action"));
+
+ return createRequest(locationUri, attributeName, transformed, parameters);
+ }
+
+ protected HttpUriRequest createRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException, URISyntaxException {
+ switch (this.targetBinding) {
+ case POST:
+ return createPostRequest(locationUri, attributeName, transformed, parameters);
+ case REDIRECT:
+ return createRedirectRequest(locationUri, attributeName, transformed, parameters);
+ }
+ throw new RuntimeException("Unknown target binding for " + ModifySamlResponseStepBuilder.class.getName());
+ }
+
+ protected HttpUriRequest createRedirectRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException, URISyntaxException {
+ final byte[] responseBytes = transformed.getBytes(GeneralConstants.SAML_CHARSET);
+ parameters.add(new BasicNameValuePair(attributeName, RedirectBindingUtil.deflateBase64Encode(responseBytes)));
+
+ if (this.targetUri != null) {
+ locationUri = this.targetUri;
+ }
+
+ URI target = new URIBuilder(locationUri).setParameters(parameters).build();
+
+ return new HttpGet(target);
+ }
+
+ protected HttpUriRequest createPostRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException {
+ HttpPost post = new HttpPost(locationUri);
+
+ parameters.add(new BasicNameValuePair(attributeName, PostBindingUtil.base64Encode(transformed)));
+
+ UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, GeneralConstants.SAML_CHARSET);
+ post.setEntity(formEntity);
+
+ return post;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java
new file mode 100644
index 0000000000..ee24b0670d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClient.Step;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class RequiredConsentBuilder implements Step {
+
+ private final SamlClientBuilder clientBuilder;
+ private boolean approveConsent = true;
+
+ public RequiredConsentBuilder(SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ }
+
+ @Override
+ public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception {
+ assertThat(currentResponse, statusCodeIsHC(Response.Status.OK));
+ String consentPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8");
+ assertThat(consentPageText, containsString("consent"));
+
+ return handleConsentPage(consentPageText, currentURI);
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+
+ public RequiredConsentBuilder approveConsent(boolean shouldApproveConsent) {
+ this.approveConsent = shouldApproveConsent;
+ return this;
+ }
+
+ /**
+ * Prepares a GET/POST request for consent granting . The consent page is expected
+ * to have at least input fields with id "kc-login" and "kc-cancel".
+ *
+ * @param consentPage
+ * @param consent
+ * @return
+ */
+ public HttpUriRequest handleConsentPage(String consentPage, URI currentURI) {
+ org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage);
+
+ List parameters = new LinkedList<>();
+ for (Element form : theLoginPage.getElementsByTag("form")) {
+ String method = form.attr("method");
+ String action = form.attr("action");
+ boolean isPost = method != null && "post".equalsIgnoreCase(method);
+
+ for (Element input : form.getElementsByTag("input")) {
+ if (Objects.equals(input.id(), "kc-login")) {
+ if (approveConsent)
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
+ } else if (Objects.equals(input.id(), "kc-cancel")) {
+ if (!approveConsent)
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
+ } else {
+ parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
+ }
+ }
+
+ if (isPost) {
+ HttpPost res = new HttpPost(currentURI.resolve(action));
+
+ UrlEncodedFormEntity formEntity;
+ try {
+ formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ res.setEntity(formEntity);
+
+ return res;
+ } else {
+ UriBuilder b = UriBuilder.fromPath(action);
+ for (NameValuePair parameter : parameters) {
+ b.queryParam(parameter.getName(), parameter.getValue());
+ }
+ return new HttpGet(b.build());
+ }
+ }
+
+ throw new IllegalArgumentException("Invalid consent page: " + consentPage);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java
new file mode 100644
index 0000000000..8b8fde083c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2017 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.util.saml;
+
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
+import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
+import org.keycloak.dom.saml.v2.protocol.AttributeQueryType;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.util.DocumentUtil;
+import org.keycloak.saml.common.util.StaxUtil;
+import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
+import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
+import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
+import org.keycloak.testsuite.util.SamlClient.Step;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import javax.xml.stream.XMLStreamWriter;
+import org.junit.Assert;
+import org.w3c.dom.Document;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class SamlDocumentStepBuilder> implements Step {
+
+ @FunctionalInterface
+ public interface Saml2ObjectTransformer {
+ public T transform(T original) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface Saml2DocumentTransformer {
+ public Document transform(Document original) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface StringTransformer {
+ public String transform(String original) throws Exception;
+ }
+
+ private final SamlClientBuilder clientBuilder;
+
+ private StringTransformer transformer = t -> t;
+
+ public SamlDocumentStepBuilder(SamlClientBuilder clientBuilder) {
+ this.clientBuilder = clientBuilder;
+ }
+
+ @SuppressWarnings("unchecked")
+ public This transformObject(Saml2ObjectTransformer tr) {
+ final StringTransformer original = this.transformer;
+ this.transformer = s -> {
+ final String originalTransformed = original.transform(s);
+
+ if (originalTransformed == null) {
+ return null;
+ }
+
+ final ByteArrayInputStream baos = new ByteArrayInputStream(originalTransformed.getBytes());
+ final T saml2Object = (T) new SAMLParser().parse(baos);
+ final T transformed = tr.transform(saml2Object);
+
+ if (transformed == null) {
+ return null;
+ }
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
+
+ if (saml2Object instanceof AuthnRequestType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) saml2Object);
+ } else if (saml2Object instanceof LogoutRequestType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((LogoutRequestType) saml2Object);
+ } else if (saml2Object instanceof ArtifactResolveType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((ArtifactResolveType) saml2Object);
+ } else if (saml2Object instanceof AttributeQueryType) {
+ new SAMLRequestWriter(xmlStreamWriter).write((AttributeQueryType) saml2Object);
+ } else if (saml2Object instanceof ResponseType) {
+ new SAMLResponseWriter(xmlStreamWriter).write((ResponseType) saml2Object);
+ } else if (saml2Object instanceof ArtifactResponseType) {
+ new SAMLResponseWriter(xmlStreamWriter).write((ArtifactResponseType) saml2Object);
+ } else {
+ Assert.assertNotNull("Unknown type: ", saml2Object);
+ Assert.fail("Unknown type: " + saml2Object.getClass().getName());
+ }
+ return new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET);
+ };
+ return (This) this;
+ }
+
+ public This transformDocument(Saml2DocumentTransformer tr) {
+ final StringTransformer original = this.transformer;
+ this.transformer = s -> {
+ final String originalTransformed = original.transform(s);
+
+ if (originalTransformed == null) {
+ return null;
+ }
+
+ final Document transformed = tr.transform(DocumentUtil.getDocument(originalTransformed));
+ return transformed == null ? null : DocumentUtil.getDocumentAsString(transformed);
+ };
+ return (This) this;
+ }
+
+ public This transformString(StringTransformer tr) {
+ final StringTransformer original = this.transformer;
+ this.transformer = s -> {
+ final String originalTransformed = original.transform(s);
+
+ if (originalTransformed == null) {
+ return null;
+ }
+
+ return tr.transform(originalTransformed);
+ };
+ return (This) this;
+ }
+
+ public SamlClientBuilder build() {
+ return this.clientBuilder;
+ }
+
+ public StringTransformer getTransformer() {
+ return transformer;
+ }
+}
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 26c398ca7e..77f6281056 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
@@ -54,7 +54,6 @@ import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2ErrorResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
-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;
@@ -66,6 +65,7 @@ import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.util.*;
import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import org.openqa.selenium.By;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -107,8 +107,6 @@ 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.idpInitiatedLogin;
-import static org.keycloak.testsuite.util.SamlClient.login;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.WaitUtils.*;
@@ -471,10 +469,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
@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);
- });
+ SAMLDocumentHolder samlResponse = new SamlClientBuilder()
+ .navigateTo(employeeAcsServletPage.buildUri())
+ .getSamlResponse(Binding.POST);
assertThat(samlResponse.getSamlObject(), instanceOf(AuthnRequestType.class));
assertThat(((AuthnRequestType) samlResponse.getSamlObject()).getAssertionConsumerServiceURL(), notNullValue());
@@ -1029,58 +1026,44 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd
@Test
//KEYCLOAK-4020
public void testBooleanAttribute() throws Exception {
- AuthnRequestType req = SamlClient.createLoginRequestDocument("http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), getAuthServerSamlEndpoint(SAMLSERVLETDEMO));
- Document doc = SAML2Request.convert(req);
+ new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .processSamlResponse(Binding.POST)
+ .transformDocument(responseDoc -> {
+ Element attribute = responseDoc.createElement("saml:Attribute");
+ attribute.setAttribute("Name", "boolean-attribute");
+ attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
- SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(SAMLSERVLETDEMO), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST);
- Document responseDoc = res.getSamlDocument();
+ Element attributeValue = responseDoc.createElement("saml:AttributeValue");
+ attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
+ attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
+ attributeValue.setAttribute("xsi:type", "xs:boolean");
+ attributeValue.setTextContent("true");
- Element attribute = responseDoc.createElement("saml:Attribute");
- attribute.setAttribute("Name", "boolean-attribute");
- attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
+ attribute.appendChild(attributeValue);
+ IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute);
- Element attributeValue = responseDoc.createElement("saml:AttributeValue");
- attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
- attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
- attributeValue.setAttribute("xsi:type", "xs:boolean");
- attributeValue.setTextContent("true");
+ return responseDoc;
+ })
+ .build()
- attribute.appendChild(attributeValue);
- IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute);
+ .navigateTo(employee2ServletPage.toString() + "/getAttributes")
- CloseableHttpResponse response = null;
- try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
- HttpClientContext context = HttpClientContext.create();
-
- HttpUriRequest post = SamlClient.Binding.POST.createSamlUnsignedResponse(getAppServerSamlEndpoint(employee2ServletPage), null, responseDoc);
- response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.FOUND));
- response.close();
-
- HttpGet get = new HttpGet(employee2ServletPage.toString() + "/getAttributes");
- response = client.execute(get);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- assertThat(response, bodyHC(containsString("boolean-attribute: true")));
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- } finally {
- if (response != null) {
- EntityUtils.consumeQuietly(response.getEntity());
- try { response.close(); } catch (IOException ex) { }
- }
- }
+ .execute(r -> {
+ assertThat(r, statusCodeIsHC(Response.Status.OK));
+ assertThat(r, bodyHC(containsString("boolean-attribute: true")));
+ });
}
// KEYCLOAK-4329
@Test
public void testEmptyKeyInfoElement() {
- samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO);
- samlidpInitiatedLoginPage.setUrlName("sales-post-sig-email");
- System.out.println(samlidpInitiatedLoginPage.toString());
- URI idpInitiatedLoginPage = URI.create(samlidpInitiatedLoginPage.toString());
-
log.debug("Log in using idp initiated login");
- SAMLDocumentHolder documentHolder = idpInitiatedLogin(bburkeUser, idpInitiatedLoginPage, SamlClient.Binding.POST);
+ SAMLDocumentHolder documentHolder = new SamlClientBuilder()
+ .idpInitiatedLogin(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "sales-post-sig-email").build()
+ .login().user(bburkeUser).build()
+ .getSamlResponse(Binding.POST);
log.debug("Removing KeyInfo from Keycloak response");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
index ffa565117e..484b9cc698 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
@@ -19,42 +19,21 @@ package org.keycloak.testsuite.saml;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
-import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
-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.AbstractAuthTest;
-import org.keycloak.testsuite.util.SamlClient;
-
-import java.io.IOException;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import java.net.URI;
import java.util.List;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriBuilderException;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.util.EntityUtils;
import org.hamcrest.Matcher;
import org.junit.Test;
-import org.w3c.dom.Document;
import static org.hamcrest.Matchers.*;
import static org.keycloak.testsuite.util.SamlClient.*;
import static org.junit.Assert.assertThat;
-import static org.keycloak.testsuite.util.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
/**
*
@@ -63,12 +42,18 @@ import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
public class AuthnRequestNameIdFormatTest extends AbstractSamlTest {
private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher nameIdMatcher) throws Exception {
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
- loginRep.setProtocolBinding(requestBinding.getBindingUri());
- loginRep.setNameIDPolicy(nameIDPolicy);
+ SAMLDocumentHolder res = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, requestBinding)
+ .transformObject(so -> {
+ so.setProtocolBinding(requestBinding.getBindingUri());
+ so.setNameIDPolicy(nameIDPolicy);
+ return so;
+ })
+ .build()
- Document samlRequest = SAML2Request.convert(loginRep);
- SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding);
+ .login().user(bburkeUser).build()
+
+ .getSamlResponse(responseBinding);
assertThat(res.getSamlObject(), notNullValue());
assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java
index 78cf93d510..abfd00160f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java
@@ -12,6 +12,7 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -25,10 +26,10 @@ import org.w3c.dom.Document;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME;
import static org.keycloak.testsuite.util.IOUtil.documentToString;
import static org.keycloak.testsuite.util.IOUtil.setDocElementAttributeValue;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
-import static org.keycloak.testsuite.util.SamlClient.login;
/**
* @author mhajas
@@ -38,13 +39,15 @@ public class BasicSamlTest extends AbstractSamlTest {
// KEYCLOAK-4160
@Test
public void testPropertyValueInAssertion() throws ParsingException, ConfigurationException, ProcessingException {
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
-
- Document doc = SAML2Request.convert(loginRep);
-
- setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" );
-
- SAMLDocumentHolder document = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST);
+ SAMLDocumentHolder document = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST)
+ .transformDocument(doc -> {
+ setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" );
+ return doc;
+ })
+ .build()
+ .login().user(bburkeUser).build()
+ .getSamlResponse(Binding.POST);
assertThat(documentToString(document.getSamlDocument()), not(containsString("InResponseTo=\"" + System.getProperty("java.version") + "\"")));
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
index 31cc14dc71..13370acd4d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java
@@ -22,6 +22,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.testsuite.util.SamlClient;
+import org.keycloak.testsuite.util.saml.LoginBuilder;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
@@ -90,7 +91,7 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
response.close();
- HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
+ HttpUriRequest loginRequest = LoginBuilder.handleLoginPage(user, loginPageText);
strategy.setRedirectable(false);
response = client.execute(loginRequest, context);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java
index cec6e1a6bb..5c9ee3e296 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java
@@ -23,24 +23,21 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType;
import org.keycloak.dom.saml.v2.assertion.ConditionsType;
import org.keycloak.dom.saml.v2.assertion.OneTimeUseType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.saml.common.exceptions.ConfigurationException;
-import org.keycloak.saml.common.exceptions.ParsingException;
-import org.keycloak.saml.common.exceptions.ProcessingException;
-import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
-import org.keycloak.testsuite.util.SamlClient;
-import org.w3c.dom.Document;
+import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.io.Closeable;
+import java.io.IOException;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
-import static org.keycloak.testsuite.util.SamlClient.login;
/**
* KEYCLOAK-4360
@@ -60,38 +57,38 @@ public class IncludeOneTimeUseConditionTest extends AbstractSamlTest
testOneTimeUseConditionIncluded(Boolean.FALSE);
}
- private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws ProcessingException, ConfigurationException, ParsingException
+ private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws IOException
{
ClientsResource clients = adminClient.realm(REALM_NAME).clients();
List foundClients = clients.findByClientId(SAML_CLIENT_ID_SALES_POST);
assertThat(foundClients, hasSize(1));
ClientResource clientRes = clients.get(foundClients.get(0).getId());
- ClientRepresentation client = clientRes.toRepresentation();
- client.getAttributes().put(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString());
- clientRes.update(client);
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
- loginRep.setProtocolBinding(SamlClient.Binding.POST.getBindingUri());
+ try (Closeable c = new ClientAttributeUpdater(clientRes)
+ .setAttribute(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString())
+ .update()) {
- Document samlRequest = SAML2Request.convert(loginRep);
- SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, SamlClient.Binding.POST,
- SamlClient.Binding.POST);
+ SAMLDocumentHolder res = new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST).build()
+ .login().user(bburkeUser).build()
+ .getSamlResponse(Binding.POST);
- assertThat(res.getSamlObject(), notNullValue());
- assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
+ assertThat(res.getSamlObject(), notNullValue());
+ assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
- ResponseType rt = (ResponseType) res.getSamlObject();
- assertThat(rt.getAssertions(), not(empty()));
- final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions();
- assertThat(conditionsType, notNullValue());
- assertThat(conditionsType.getConditions(), not(empty()));
+ ResponseType rt = (ResponseType) res.getSamlObject();
+ assertThat(rt.getAssertions(), not(empty()));
+ final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions();
+ assertThat(conditionsType, notNullValue());
+ assertThat(conditionsType.getConditions(), not(empty()));
- final List conditions = conditionsType.getConditions();
+ final List conditions = conditionsType.getConditions();
- final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
+ final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
- final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty();
- assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded));
+ final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty();
+ assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded));
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
index 7870ebaa18..eb0888b886 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
@@ -16,34 +16,27 @@
*/
package org.keycloak.testsuite.saml;
+import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
-import org.keycloak.saml.common.exceptions.ConfigurationException;
-import org.keycloak.saml.common.exceptions.ParsingException;
-import org.keycloak.saml.common.exceptions.ProcessingException;
-import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.ClientBuilder;
-import org.keycloak.testsuite.util.Matchers;
-import org.keycloak.testsuite.util.SamlClient;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilderException;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.protocol.HttpClientContext;
+import org.keycloak.testsuite.util.SamlClientBuilder;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.xml.transform.dom.DOMSource;
import org.junit.Before;
import org.junit.Test;
-import org.w3c.dom.Document;
import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.Matchers.*;
import static org.keycloak.testsuite.util.SamlClient.Binding.*;
@@ -57,7 +50,8 @@ public class LogoutTest extends AbstractSamlTest {
private ClientRepresentation salesRep;
private ClientRepresentation sales2Rep;
- private SamlClient samlClient;
+ private final AtomicReference nameIdRef = new AtomicReference<>();
+ private final AtomicReference sessionIndexRef = new AtomicReference<>();
@Before
public void setup() {
@@ -71,7 +65,8 @@ public class LogoutTest extends AbstractSamlTest {
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
.build());
- samlClient = new SamlClient(getAuthServerSamlEndpoint(REALM_NAME));
+ nameIdRef.set(null);
+ sessionIndexRef.set(null);
}
@Override
@@ -79,49 +74,35 @@ public class LogoutTest extends AbstractSamlTest {
return true;
}
- private Document prepareLogoutFromSalesAfterLoggingIntoTwoApps() throws ParsingException, IllegalArgumentException, UriBuilderException, ConfigurationException, ProcessingException {
- AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
- Document doc = SAML2Request.convert(loginRep);
- SAMLDocumentHolder resp = samlClient.login(bburkeUser, doc, null, POST, POST, false, true);
- assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
- ResponseType loginResp1 = (ResponseType) resp.getSamlObject();
+ private SamlClientBuilder prepareLogIntoTwoApps() {
+ return new SamlClientBuilder()
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build()
+ .login().user(bburkeUser).build()
+ .processSamlResponse(POST).transformObject(so -> {
+ assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ ResponseType loginResp1 = (ResponseType) so;
+ final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
+ assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue());
+ assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
- loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REALM_NAME);
- doc = SAML2Request.convert(loginRep);
- resp = samlClient.subsequentLoginViaSSO(doc, null, POST, POST);
- assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
- ResponseType loginResp2 = (ResponseType) resp.getSamlObject();
+ NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
+ AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next();
- AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion();
- assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
- NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID();
- AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next();
+ nameIdRef.set(nameId);
+ sessionIndexRef.set(firstAssertionStatement.getSessionIndex());
+ return null; // Do not follow the redirect to the app from the returned response
+ }).build()
- return new SAML2LogoutRequestBuilder()
- .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
- .issuer(SAML_CLIENT_ID_SALES_POST)
- .sessionIndex(firstAssertionStatement.getSessionIndex())
- .userPrincipal(nameId.getValue(), nameId.getFormat().toString())
- .buildDocument();
+ .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST).build()
+ .login().sso(true).build() // This is a formal step
+ .processSamlResponse(POST).transformObject(so -> {
+ assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ return null; // Do not follow the redirect to the app from the returned response
+ }).build();
}
@Test
- public void testLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
- adminClient.realm(REALM_NAME)
- .clients().get(sales2Rep.getId())
- .update(ClientBuilder.edit(sales2Rep)
- .frontchannelLogout(false)
- .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
- .build());
-
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
-
- samlClient.logout(logoutDoc, null, POST, POST);
- }
-
- @Test
- public void testLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testLogoutDifferentBrowser() {
// This is in fact the same as admin logging out a session from admin console.
// This always succeeds as it is essentially the same as backend logout which
// does not report errors to client but only to the server log
@@ -130,135 +111,155 @@ public class LogoutTest extends AbstractSamlTest {
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(false)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+ .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .clearCookies()
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, HttpClientContext.create());
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(POST);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testFrontchannelLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testFrontchannelLogoutInSameBrowser() {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ .getSamlResponse(POST);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
- adminClient.realm(REALM_NAME)
- .clients().get(sales2Rep.getId())
- .update(ClientBuilder.edit(sales2Rep)
- .frontchannelLogout(true)
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE)
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
- .build());
-
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
-
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
- }
-
- @Test
- public void testFrontchannelLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, HttpClientContext.create());
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ .getSamlResponse(POST);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testFrontchannelLogoutDifferentBrowser() {
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
- .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+ .build());
+
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .clearCookies()
+
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(POST);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ }
+
+ @Test
+ public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() {
+ adminClient.realm(REALM_NAME)
+ .clients().get(salesRep.getId())
+ .update(ClientBuilder.edit(salesRep)
+ .frontchannelLogout(true)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ adminClient.realm(REALM_NAME)
+ .clients().get(sales2Rep.getId())
+ .update(ClientBuilder.edit(sales2Rep)
+ .frontchannelLogout(true)
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "")
+ .build());
- samlClient.execute((client, context, strategy) -> {
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- CloseableHttpResponse response = client.execute(post, HttpClientContext.create());
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .clearCookies()
+
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
+
+ .getSamlResponse(REDIRECT);
+
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
}
@Test
- public void testLogoutWithPostBindingUnsetRedirectBindingSet() throws ParsingException, ConfigurationException, ProcessingException {
+ public void testLogoutWithPostBindingUnsetRedirectBindingSet() {
// https://issues.jboss.org/browse/KEYCLOAK-4779
adminClient.realm(REALM_NAME)
.clients().get(sales2Rep.getId())
.update(ClientBuilder.edit(sales2Rep)
.frontchannelLogout(true)
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
- .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
+ .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url-to-sales-2")
.build());
- Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+ SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+ .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+ .nameId(nameIdRef::get)
+ .sessionIndex(sessionIndexRef::get)
+ .build()
- SAMLDocumentHolder resp = samlClient.getSamlResponse(REDIRECT, (client, context, strategy) -> {
- strategy.setRedirectable(false);
- HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
- return client.execute(post, context);
- });
+ .processSamlResponse(REDIRECT)
+ .transformDocument(doc -> {
+ // Expect logout request for sales-post2
+ SAML2Object so = (SAML2Object) new SAMLParser().parse(new DOMSource(doc));
+ assertThat(so, isSamlLogoutRequest("http://url-to-sales-2"));
- // Expect logout request for sales-post2
- assertThat(resp.getSamlObject(), isSamlLogoutRequest("http://url"));
- Document logoutRespDoc = new SAML2LogoutResponseBuilder()
- .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
- .issuer(SAML_CLIENT_ID_SALES_POST2)
- .logoutRequestID(((LogoutRequestType) resp.getSamlObject()).getID())
- .buildDocument();
+ // Emulate successful logout response from sales-post2 logout
+ return new SAML2LogoutResponseBuilder()
+ .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
+ .issuer(SAML_CLIENT_ID_SALES_POST2)
+ .logoutRequestID(((LogoutRequestType) so).getID())
+ .buildDocument();
+ })
+ .targetAttributeSamlResponse()
+ .targetUri(getAuthServerSamlEndpoint(REALM_NAME))
+ .build()
- // Emulate successful logout response from sales-post2 logout
- resp = samlClient.getSamlResponse(POST, (client, context, strategy) -> {
- strategy.setRedirectable(false);
- HttpUriRequest post = POST.createSamlUnsignedResponse(getAuthServerSamlEndpoint(REALM_NAME), null, logoutRespDoc);
- return client.execute(post, context);
- });
+ .getSamlResponse(POST);
// Expect final successful logout response from auth server signalling final successful logout
- assertThat(resp.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+ assertThat(((StatusResponseType) samlResponse.getSamlObject()).getDestination(), is("http://url"));
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java
index 3fcf0c36d6..bd30eea540 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java
@@ -9,16 +9,15 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.IOUtil;
-import org.keycloak.testsuite.util.SamlClient;
-import java.net.URI;
+import org.keycloak.testsuite.util.SamlClient.Binding;
+import org.keycloak.testsuite.util.SamlClientBuilder;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLoginWithRequiredConsent;
/**
* @author mhajas
@@ -48,13 +47,17 @@ public class SamlConsentTest extends AbstractSamlTest {
.build());
log.debug("Log in using idp initiated login");
- String idpInitiatedLogin = getAuthServerRoot() + "realms/" + REALM_NAME + "/protocol/saml/clients/sales-post-enc";
- SAMLDocumentHolder documentHolder = idpInitiatedLoginWithRequiredConsent(bburkeUser, URI.create(idpInitiatedLogin), SamlClient.Binding.POST, false);
+ SAMLDocumentHolder documentHolder = new SamlClientBuilder()
+ .idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post-enc").build()
+ .login().user(bburkeUser).build()
+ .consentRequired().approveConsent(false).build()
+ .getSamlResponse(Binding.POST);
- assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString(" parameters = new LinkedList<>();
-
-
- try {
- BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
-
- if (privateKeyStr != null && publicKeyStr != null) {
- PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr);
- PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr);
- binding
- .signatureAlgorithm(SignatureAlgorithm.RSA_SHA256)
- .signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey)
- .signDocument();
- }
-
- parameters.add(
- new BasicNameValuePair(messageType,
- binding
- .postBinding(samlRequest)
- .encoded())
- );
- } catch (IOException | ConfigurationException | ProcessingException ex) {
- throw new RuntimeException(ex);
- }
-
- if (relayState != null) {
- parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState));
- }
-
- UrlEncodedFormEntity formEntity;
-
- try {
- formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
-
- post.setEntity(formEntity);
-
- return post;
- }
-
- @Override
- public URI getBindingUri() {
- return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
- }
- },
-
- REDIRECT {
- @Override
- public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
- assertThat(response, statusCodeIsHC(Response.Status.FOUND));
- String location = response.getFirstHeader("Location").getValue();
- response.close();
- return extractSamlResponseFromRedirect(location);
- }
-
- @Override
- public HttpGet createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) {
- try {
- URI requestURI = new BaseSAML2BindingBuilder()
- .relayState(relayState)
- .redirectBinding(samlRequest)
- .requestURI(samlEndpoint.toString());
- return new HttpGet(requestURI);
- } catch (ProcessingException | ConfigurationException | IOException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- @Override
- public URI getBindingUri() {
- return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
- }
-
- @Override
- public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) {
- return null;
- }
-
- @Override
- public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) {
- return null;
- }
- };
-
- public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException;
-
- public abstract HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest);
-
- public abstract HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey);
-
- public abstract URI getBindingUri();
-
- public abstract HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest);
- }
-
- public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy {
-
- public boolean redirectable = true;
-
- @Override
- protected boolean isRedirectable(String method) {
- return redirectable && super.isRedirectable(method);
- }
-
- public void setRedirectable(boolean redirectable) {
- this.redirectable = redirectable;
- }
- }
-
- /**
- * Extracts and parses value of SAMLResponse input field of a form present in the given page.
- *
- * @param responsePage HTML code of the page
- * @return
- */
- public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
- org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
- Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
- Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
- int size = samlResponses.size() + samlRequests.size();
- assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
-
- Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
-
- return SAMLRequestParser.parseResponsePostBinding(respElement.val());
- }
-
- /**
- * Extracts and parses value of SAMLResponse query parameter from the given URI.
- *
- * @param responseUri
- * @return
- */
- public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
- List params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
-
- String samlDoc = null;
- for (NameValuePair param : params) {
- if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
- assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue());
- samlDoc = param.getValue();
- }
- }
-
- return SAMLRequestParser.parseResponseRedirectBinding(samlDoc);
- }
-
- /**
- * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected
- * to have at least input fields with id "username" and "password".
- *
- * @param user
- * @param loginPage
- * @return
- */
- public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) {
- String username = user.getUsername();
- String password = getPasswordOf(user);
- org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage);
-
- List parameters = new LinkedList<>();
- for (Element form : theLoginPage.getElementsByTag("form")) {
- String method = form.attr("method");
- String action = form.attr("action");
- boolean isPost = method != null && "post".equalsIgnoreCase(method);
-
- for (Element input : form.getElementsByTag("input")) {
- if (Objects.equals(input.id(), "username")) {
- parameters.add(new BasicNameValuePair(input.attr("name"), username));
- } else if (Objects.equals(input.id(), "password")) {
- parameters.add(new BasicNameValuePair(input.attr("name"), password));
- } else {
- parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
- }
- }
-
- if (isPost) {
- HttpPost res = new HttpPost(action);
-
- UrlEncodedFormEntity formEntity;
- try {
- formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
- res.setEntity(formEntity);
-
- return res;
- } else {
- UriBuilder b = UriBuilder.fromPath(action);
- for (NameValuePair parameter : parameters) {
- b.queryParam(parameter.getName(), parameter.getValue());
- }
- return new HttpGet(b.build());
- }
- }
-
- throw new IllegalArgumentException("Invalid login form: " + loginPage);
- }
-
- /**
- * Prepares a GET/POST request for consent granting . The consent page is expected
- * to have at least input fields with id "kc-login" and "kc-cancel".
- *
- * @param consentPage
- * @param consent
- * @return
- */
- public static HttpUriRequest handleConsentPage(String consentPage, boolean consent) {
- org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage);
-
- List parameters = new LinkedList<>();
- for (Element form : theLoginPage.getElementsByTag("form")) {
- String method = form.attr("method");
- String action = form.attr("action");
- boolean isPost = method != null && "post".equalsIgnoreCase(method);
-
- for (Element input : form.getElementsByTag("input")) {
- if (Objects.equals(input.id(), "kc-login")) {
- if (consent)
- parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
- } else if (Objects.equals(input.id(), "kc-cancel")) {
- if (!consent)
- parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value")));
- } else {
- parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
- }
- }
-
- if (isPost) {
- HttpPost res = new HttpPost(getAuthServerContextRoot() + action);
-
- UrlEncodedFormEntity formEntity;
- try {
- formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
- res.setEntity(formEntity);
-
- return res;
- } else {
- UriBuilder b = UriBuilder.fromPath(action);
- for (NameValuePair parameter : parameters) {
- b.queryParam(parameter.getName(), parameter.getValue());
- }
- return new HttpGet(b.build());
- }
- }
-
- throw new IllegalArgumentException("Invalid consent page: " + consentPage);
- }
-
- /**
- * Creates a SAML login request document with the given parameters. See SAML <AuthnRequest> description for more details.
- *
- * @param issuer
- * @param assertionConsumerURL
- * @param destination
- * @return
- */
- public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) {
- try {
- SAML2Request samlReq = new SAML2Request();
- AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer);
-
- return loginReq;
- } catch (ConfigurationException ex) {
- throw new RuntimeException(ex);
- }
- }
-
- /**
- * Send request for login form and then login using user param. This method is designed for clients without required consent
- *
- * @param user
- * @param samlEndpoint
- * @param samlRequest
- * @param relayState
- * @param requestBinding
- * @param expectedResponseBinding
- * @return
- */
- public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
- Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
- return new SamlClient(samlEndpoint).login(user, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true);
- }
-
- private final HttpClientContext context = HttpClientContext.create();
- private final URI samlEndpoint;
-
- public SamlClient(URI samlEndpoint) {
- this.samlEndpoint = samlEndpoint;
- }
-
- public HttpClientContext getContext() {
- return context;
- }
-
- public URI getSamlEndpoint() {
- return samlEndpoint;
- }
-
- /**
- * Send request for login form and then login using user param. Check whether client requires consent and handle consent page.
- *
- * @param user
- * @param samlEndpoint
- * @param samlRequest
- * @param relayState
- * @param requestBinding
- * @param expectedResponseBinding
- * @param consentRequired
- * @param consent
- * @return
- */
- public SAMLDocumentHolder login(UserRepresentation user,
- Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
- return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
- HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
- String loginPageText;
-
- try (CloseableHttpResponse response = client.execute(post, context)) {
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
- assertThat(loginPageText, containsString("login"));
- }
-
- HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
-
- if (consentRequired) {
- // Client requires consent
- try (CloseableHttpResponse response = client.execute(loginRequest, context)) {
- String consentPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
- loginRequest = handleConsentPage(consentPageText, consent);
- }
- }
-
- strategy.setRedirectable(false);
- return client.execute(loginRequest, context);
- });
- }
-
- /**
- * Send request for login form once already logged in, hence login using SSO.
- * Check whether client requires consent and handle consent page.
- *
- * @param user
- * @param samlEndpoint
- * @param samlRequest
- * @param relayState
- * @param requestBinding
- * @param expectedResponseBinding
- * @return
- */
- public SAMLDocumentHolder subsequentLoginViaSSO(Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
- return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
- strategy.setRedirectable(false);
-
- HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
- CloseableHttpResponse response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.FOUND));
- String location = response.getFirstHeader("Location").getValue();
-
- response = client.execute(new HttpGet(location), context);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
- }
-
- /**
- * Send request for login form once already logged in, hence login using SSO.
- * Check whether client requires consent and handle consent page.
- *
- * @param user
- * @param samlEndpoint
- * @param samlRequest
- * @param relayState
- * @param requestBinding
- * @param expectedResponseBinding
- * @return
- */
- public SAMLDocumentHolder logout(Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
- return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
- strategy.setRedirectable(false);
-
- HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
- CloseableHttpResponse response = client.execute(post, context);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
- return response;
- });
- }
-
- @FunctionalInterface
- public interface HttpClientProcessor {
- public CloseableHttpResponse process(CloseableHttpClient client, HttpContext context, RedirectStrategyWithSwitchableFollowRedirect strategy) throws Exception;
- }
-
- public void execute(HttpClientProcessor body) {
- CloseableHttpResponse response = null;
- RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect();
-
- try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
- response = body.process(client, context, strategy);
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- } finally {
- if (response != null) {
- EntityUtils.consumeQuietly(response.getEntity());
- try {
- response.close();
- } catch (IOException ex) {
- }
- }
- }
- }
-
- public SAMLDocumentHolder getSamlResponse(Binding expectedResponseBinding, HttpClientProcessor body) {
- CloseableHttpResponse response = null;
- RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect();
- try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
- response = body.process(client, context, strategy);
-
- return expectedResponseBinding.extractResponse(response);
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- } finally {
- if (response != null) {
- EntityUtils.consumeQuietly(response.getEntity());
- try {
- response.close();
- } catch (IOException ex) {
- }
- }
- }
- }
-
- /**
- * Send request for login form and then login using user param for clients which doesn't require consent
- *
- * @param user
- * @param idpInitiatedURI
- * @param expectedResponseBinding
- * @return
- */
- public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) {
- return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, false, true);
- }
-
- /**
- * Send request for login form and then login using user param. For clients which requires consent
- *
- * @param user
- * @param idpInitiatedURI
- * @param expectedResponseBinding
- * @param consent
- * @return
- */
- public static SAMLDocumentHolder idpInitiatedLoginWithRequiredConsent(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consent) {
- return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, true, consent);
- }
-
- /**
- * Send request for login form and then login using user param. Checks whether client requires consent and handle consent page.
- *
- * @param user
- * @param samlEndpoint
- * @param expectedResponseBinding
- * @param consent
- * @return
- */
- public SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
- return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
- HttpGet get = new HttpGet(samlEndpoint);
- CloseableHttpResponse response = client.execute(get);
- assertThat(response, statusCodeIsHC(Response.Status.OK));
-
- String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
- response.close();
-
- assertThat(loginPageText, containsString("login"));
-
- HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
-
- if (consentRequired) {
- // Client requires consent
- response = client.execute(loginRequest, context);
- String consentPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
- loginRequest = handleConsentPage(consentPageText, consent);
- }
-
- strategy.setRedirectable(false);
- return client.execute(loginRequest, context);
- });
- }
-
-
-}
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 e1901298d2..aed0231baf 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
@@ -8,6 +8,7 @@
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
+ "passwordPolicy": "hashIterations(1)",
"defaultRoles": [ "user" ],
"smtpServer": {
"from": "auto@keycloak.org",
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml
index 486da62ab3..62c011cf01 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml
@@ -24,10 +24,10 @@
org.keycloak.testsuite
integration-arquillian-tests-adapters-jboss
- 3.2.0.CR1-SNAPSHOT
+ 3.3.0.CR1-SNAPSHOT
- integration-arquillian-tests-adapters-wildfly
+ integration-arquillian-tests-adapters-wildfly10
Adapter Tests - JBoss - Wildfly 10
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
index c3e013549a..9fb585e55e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
@@ -91,9 +91,9 @@ public class OIDCBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
//.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8")));
assertNotNull(responseType);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java
index bbbbc479d1..8fca98dc63 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java
@@ -90,9 +90,9 @@ public class SAMLBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
//.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8")));
assertNotNull(responseType);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
index 8afc49b692..5e177796df 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
@@ -93,9 +93,9 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
//.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8")));
assertNotNull(responseType);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java
index 8a453a7fe6..a0ee823d9b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java
@@ -98,9 +98,9 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractKeycloakI
@Override
protected void doAssertTokenRetrieval(String pageSource) {
try {
- SAML2Request saml2Request = new SAML2Request();
- ResponseType responseType = (ResponseType) saml2Request
- .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource));
+ ResponseType responseType = (ResponseType) SAML2Request
+ .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource))
+ .getSamlObject();
assertNotNull(responseType);
assertFalse(responseType.getAssertions().isEmpty());