keycloak-aplcache

Changes

Details

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 2bfa41f..cb8a348 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/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java
index 00160e6..335a72d 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/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index e0ac524..589dde3 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/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java
index c3e0135..9fb585e 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 bbbbc47..8fca98d 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 8afc49b..5e17779 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 8a453a7..a0ee823 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());
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 79f241b..b2c06ad 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -79,6 +79,11 @@
             <scope>compile</scope>
         </dependency>
         <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-all</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
             <groupId>org.subethamail</groupId>
             <artifactId>subethasmtp</artifactId>
             <scope>compile</scope>
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 0272a1b..e967a33 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<String, String> 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/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 0000000..aa85821
--- /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<AuthnRequestType, CreateAuthnRequestStepBuilder> {
+
+    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 0000000..ee594d0
--- /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<LogoutRequestType, CreateLogoutRequestStepBuilder> {
+
+    private final URI authServerSamlUrl;
+    private final String issuer;
+    private final Binding requestBinding;
+
+    private Supplier<String> sessionIndex = () -> null;
+    private Supplier<NameIDType> nameId = () -> null;
+    private Supplier<String> 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<String> 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<String> 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<NameIDType> 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 0000000..d119e64
--- /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 0000000..4e5713b
--- /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<NameValuePair> 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 0000000..e29091b
--- /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<SAML2Object, ModifySamlResponseStepBuilder> {
+
+    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<NameValuePair> params = URLEncodedUtils.parse(locationUri, "UTF-8");
+        for (Iterator<NameValuePair> 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<NameValuePair> 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<NameValuePair> 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<NameValuePair> 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<NameValuePair> 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 0000000..ee24b06
--- /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<NameValuePair> 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 0000000..8b8fde0
--- /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<T extends SAML2Object, This extends SamlDocumentStepBuilder<T, This>> implements Step {
+
+    @FunctionalInterface
+    public interface Saml2ObjectTransformer<T extends SAML2Object> {
+        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<T> 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: <null>", 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/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 0000000..89d3092
--- /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<Step> steps = new LinkedList<>();
+
+    public SamlClient execute(Consumer<CloseableHttpResponse> resultConsumer) {
+        final SamlClient samlClient = new SamlClient();
+        samlClient.executeAndTransform(r -> {
+            resultConsumer.accept(r);
+            return null;
+        }, steps);
+        return samlClient;
+    }
+
+    public <T> T executeAndTransform(ResultExtractor<T> resultTransformer) {
+        return new SamlClient().executeAndTransform(resultTransformer, steps);
+    }
+
+    public List<Step> getSteps() {
+        return steps;
+    }
+
+    public <T extends Step> 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/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java
index 26c398c..77f6281 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 ffa5651..484b9cc 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<String> 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);
-
-        Document samlRequest = SAML2Request.convert(loginRep);
-        SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding);
+        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()
+
+          .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 78cf93d..abfd001 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 31cc14d..13370ac 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 cec6e1a..5c9ee3e 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<ClientRepresentation> 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<ConditionAbstractType> conditions = conditionsType.getConditions();
+            final List<ConditionAbstractType> conditions = conditionsType.getConditions();
 
-        final Collection<ConditionAbstractType> oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
+            final Collection<ConditionAbstractType> 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 7870eba..eb0888b 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<NameIDType> nameIdRef = new AtomicReference<>();
+    private final AtomicReference<String> 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,186 +74,192 @@ 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();
-
-        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();
-
-        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();
-
-        return new SAML2LogoutRequestBuilder()
-          .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
-          .issuer(SAML_CLIENT_ID_SALES_POST)
-          .sessionIndex(firstAssertionStatement.getSessionIndex())
-          .userPrincipal(nameId.getValue(), nameId.getFormat().toString())
-          .buildDocument();
+    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));
+
+            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()
+
+          .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 {
+    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
         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)
+          .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
             .build());
 
-        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+        SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+          .clearCookies()
+
+          .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+            .nameId(nameIdRef::get)
+            .sessionIndex(sessionIndexRef::get)
+            .build()
 
-        samlClient.logout(logoutDoc, null, POST, POST);
+          .getSamlResponse(POST);
+
+        assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
     }
 
     @Test
-    public void testLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
-        // 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
+    public void testFrontchannelLogoutInSameBrowser() {
         adminClient.realm(REALM_NAME)
           .clients().get(sales2Rep.getId())
           .update(ClientBuilder.edit(sales2Rep)
-            .frontchannelLogout(false)
+            .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, HttpClientContext.create());
-            assertThat(response, statusCodeIsHC(Response.Status.OK));
-            return response;
-        });
+          .getSamlResponse(POST);
+
+        assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
     }
 
     @Test
-    public void testFrontchannelLogoutInSameBrowser() 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()
+
+          .getSamlResponse(POST);
 
-        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;
-        });
+        assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
     }
 
     @Test
-    public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() 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)
-            .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
             .build());
 
-        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+        SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+          .clearCookies()
+
+          .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+            .nameId(nameIdRef::get)
+            .sessionIndex(sessionIndexRef::get)
+            .build()
+
+          .getSamlResponse(POST);
 
-        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;
-        });
+        assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
     }
 
     @Test
-    public void testFrontchannelLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+    public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() {
         adminClient.realm(REALM_NAME)
-          .clients().get(sales2Rep.getId())
-          .update(ClientBuilder.edit(sales2Rep)
+          .clients().get(salesRep.getId())
+          .update(ClientBuilder.edit(salesRep)
             .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, "http://url")
             .build());
 
-        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
-
-        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;
-        });
-    }
-
-    @Test
-    public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() 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)
-            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
+            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+            .attribute(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, 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 resp = samlClient.getSamlResponse(REDIRECT, (client, context, strategy) -> {
-            strategy.setRedirectable(false);
-            HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
-            return client.execute(post, context);
-        });
-
-        // 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
-        resp = samlClient.getSamlResponse(POST, (client, context, strategy) -> {
-            strategy.setRedirectable(false);
-            HttpUriRequest post = POST.createSamlUnsignedResponse(getAuthServerSamlEndpoint(REALM_NAME), null, logoutRespDoc);
-            return client.execute(post, context);
-        });
+        SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps()
+          .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST)
+            .nameId(nameIdRef::get)
+            .sessionIndex(sessionIndexRef::get)
+            .build()
+
+          .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"));
+
+              // 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()
+
+          .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 3fcf0c3..bd30eea 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);
-
-        assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<dsig:Signature")); // KEYCLOAK-4262
-        assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), not(containsString("<samlp:LogoutResponse"))); // KEYCLOAK-4261
-        assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:Response")); // KEYCLOAK-4261
-        assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:Status")); // KEYCLOAK-4181
-        assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:RequestDenied\"")); // KEYCLOAK-4181
+        SAMLDocumentHolder documentHolder = new SamlClientBuilder()
+          .idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post-enc").build()
+          .login().user(bburkeUser).build()
+          .consentRequired().approveConsent(false).build()
+          .getSamlResponse(Binding.POST);
+
+        final String samlDocumentString = IOUtil.documentToString(documentHolder.getSamlDocument());
+        assertThat(samlDocumentString, containsString("<dsig:Signature")); // KEYCLOAK-4262
+        assertThat(samlDocumentString, not(containsString("<samlp:LogoutResponse"))); // KEYCLOAK-4261
+        assertThat(samlDocumentString, containsString("<samlp:Response")); // KEYCLOAK-4261
+        assertThat(samlDocumentString, containsString("<samlp:Status")); // KEYCLOAK-4181
+        assertThat(samlDocumentString, containsString("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:RequestDenied\"")); // KEYCLOAK-4181
     }
 }
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 e190129..aed0231 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 486da62..62c011c 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 @@
     <parent>
         <groupId>org.keycloak.testsuite</groupId>
         <artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
-        <version>3.2.0.CR1-SNAPSHOT</version>
+        <version>3.3.0.CR1-SNAPSHOT</version>
     </parent>
 
-    <artifactId>integration-arquillian-tests-adapters-wildfly</artifactId>
+    <artifactId>integration-arquillian-tests-adapters-wildfly10</artifactId>
 
     <name>Adapter Tests - JBoss - Wildfly 10</name>