keycloak-uncached

Details

diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 6759fb6..cabbcb4 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -270,9 +270,7 @@ public class SamlProtocol implements LoginProtocol {
 
         if (logoutPostUrl == null || logoutPostUrl.trim().isEmpty()) {
             // if we don't have a redirect uri either, return true and default to the admin url + POST binding
-            if (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty())
-                return true;
-            return false;
+            return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
         }
 
         if (samlClient.forcePostBinding()) {
@@ -285,11 +283,8 @@ public class SamlProtocol implements LoginProtocol {
         if (SAML_POST_BINDING.equals(bindingType))
             return true;
 
-        if (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty())
-            return true; // we don't have a redirect binding url, so use post binding
-
-        return false; // redirect binding
-
+        // true if we don't have a redirect binding url, so use post binding, false for redirect binding
+        return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
     }
 
     protected String getNameIdFormat(SamlClient samlClient, ClientSessionModel clientSession) {
@@ -530,15 +525,20 @@ public class SamlProtocol implements LoginProtocol {
         if (!(client instanceof ClientModel))
             return null;
         try {
-            if (isLogoutPostBindingForClient(clientSession)) {
-                String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
+            boolean postBinding = isLogoutPostBindingForClient(clientSession);
+            String bindingUri = getLogoutServiceUrl(uriInfo, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING);
+            if (bindingUri == null) {
+                logger.warnf("Failed to logout client %s, skipping this client.  Please configure the logout service url in the admin console for your client applications.", client.getClientId());
+                return null;
+            }
+
+            if (postBinding) {
                 SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
                 // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
                 JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
                 return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri);
             } else {
                 logger.debug("frontchannel redirect binding");
-                String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
                 SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
                 if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
                     KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
@@ -621,7 +621,7 @@ public class SamlProtocol implements LoginProtocol {
         SamlClient samlClient = new SamlClient(client);
         String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
         if (logoutUrl == null) {
-            logger.warnv("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: {1}", client.getClientId());
+            logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s", client.getClientId());
             return;
         }
         SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AbstractSamlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AbstractSamlTest.java
index b9a2547..6f68908 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AbstractSamlTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AbstractSamlTest.java
@@ -26,6 +26,9 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
     protected static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
     protected static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
 
+    protected static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST2 = "http://localhost:8080/sales-post2/";
+    protected static final String SAML_CLIENT_ID_SALES_POST2 = "http://localhost:8081/sales-post2/";
+
     protected static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC = "http://localhost:8080/sales-post-enc/";
     protected static final String SAML_CLIENT_ID_SALES_POST_ENC = "http://localhost:8081/sales-post-enc/";
     protected static final String SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
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
new file mode 100644
index 0000000..149d15f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.saml;
+
+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.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.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.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.util.Matchers.*;
+import static org.keycloak.testsuite.util.SamlClient.Binding.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class LogoutTest extends AbstractSamlTest {
+
+    private ClientRepresentation salesRep;
+    private ClientRepresentation sales2Rep;
+
+    private SamlClient samlClient;
+
+    @Before
+    public void setup() {
+        salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
+        sales2Rep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST2).get(0);
+
+        adminClient.realm(REALM_NAME)
+          .clients().get(salesRep.getId())
+          .update(ClientBuilder.edit(salesRep)
+            .frontchannelLogout(true)
+            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
+            .build());
+
+        samlClient = new SamlClient(getAuthServerSamlEndpoint(REALM_NAME));
+    }
+
+    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();
+    }
+
+    @Test
+    public void testLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+        adminClient.realm(REALM_NAME)
+          .clients().get(sales2Rep.getId())
+          .update(ClientBuilder.edit(sales2Rep)
+            .frontchannelLogout(false)
+            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+            .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+            .build());
+
+        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+
+        samlClient.logout(logoutDoc, null, POST, POST);
+    }
+
+    @Test
+    public void testLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+        // 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)
+            .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 testFrontchannelLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+        adminClient.realm(REALM_NAME)
+          .clients().get(sales2Rep.getId())
+          .update(ClientBuilder.edit(sales2Rep)
+            .frontchannelLogout(true)
+            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+            .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+            .build());
+
+        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+
+        samlClient.execute((client, context, strategy) -> {
+            HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
+            CloseableHttpResponse response = client.execute(post, context);
+            assertThat(response, statusCodeIsHC(Response.Status.OK));
+            return response;
+        });
+    }
+
+    @Test
+    public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+        adminClient.realm(REALM_NAME)
+          .clients().get(sales2Rep.getId())
+          .update(ClientBuilder.edit(sales2Rep)
+            .frontchannelLogout(true)
+            .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE)
+            .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+            .build());
+
+        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+
+        samlClient.execute((client, context, strategy) -> {
+            HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc);
+            CloseableHttpResponse response = client.execute(post, context);
+            assertThat(response, statusCodeIsHC(Response.Status.OK));
+            return response;
+        });
+    }
+
+    @Test
+    public void testFrontchannelLogoutDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException {
+        adminClient.realm(REALM_NAME)
+          .clients().get(sales2Rep.getId())
+          .update(ClientBuilder.edit(sales2Rep)
+            .frontchannelLogout(true)
+            .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "")
+            .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE)
+            .build());
+
+        Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
+
+        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")
+            .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 testLogoutWithPostBindingUnsetRedirectBindingSet() throws ParsingException, ConfigurationException, ProcessingException {
+        // 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")
+            .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);
+        });
+
+        // Expect final successful logout response from auth server signalling final successful logout
+        assertThat(resp.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
index 7530275..397edf3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
@@ -91,6 +91,11 @@ public class ClientBuilder {
         return this;
     }
 
+    public ClientBuilder frontchannelLogout(Boolean frontchannelLogout) {
+        rep.setFrontchannelLogout(frontchannelLogout);
+        return this;
+    }
+
     public ClientBuilder secret(String secret) {
         rep.setSecret(secret);
         return this;
@@ -115,6 +120,15 @@ public class ClientBuilder {
         return this;
     }
 
+    public ClientBuilder removeAttribute(String name) {
+        Map<String, String> attributes = rep.getAttributes();
+        if (attributes != null) {
+            attributes.remove(name);
+            rep.setAttributes(attributes);
+        }
+        return this;
+    }
+
     public ClientBuilder authenticatorType(String providerId) {
         rep.setClientAuthenticatorType(providerId);
         return this;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
index f88cfdf..0ab3a7b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java
@@ -16,12 +16,20 @@
  */
 package org.keycloak.testsuite.util;
 
+import org.keycloak.dom.saml.v2.SAML2Object;
+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.testsuite.util.matchers.SamlResponseTypeMatcher;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
 import org.keycloak.testsuite.util.matchers.*;
 
+import java.net.URI;
 import java.util.Map;
 import javax.ws.rs.core.Response;
 import org.apache.http.HttpResponse;
 import org.hamcrest.Matcher;
+import static org.hamcrest.Matchers.*;
 
 /**
  * Additional hamcrest matchers for use in {@link org.junit.Assert#assertThat}.
@@ -109,4 +117,40 @@ public class Matchers {
     public static <T> Matcher<Response> header(Matcher<Map<String, T>> matcher) {
         return new ResponseHeaderMatcher(matcher);
     }
+
+    /**
+     * Matches when the SAML status code of a {@link ResponseType} instance is equal to the given code.
+     * @param expectedStatusCode
+     * @return
+     */
+    public static <T> Matcher<SAML2Object> isSamlResponse(JBossSAMLURIConstants expectedStatus) {
+        return allOf(
+          instanceOf(ResponseType.class),
+          new SamlResponseTypeMatcher(is(URI.create(expectedStatus.get())))
+        );
+    }
+
+    /**
+     * Matches when the destination of a SAML {@link LogoutRequestType} instance is equal to the given destination.
+     * @param expectedStatusCode
+     * @return
+     */
+    public static <T> Matcher<SAML2Object> isSamlLogoutRequest(String destination) {
+        return allOf(
+          instanceOf(LogoutRequestType.class),
+          new SamlLogoutRequestTypeMatcher(URI.create(destination))
+        );
+    }
+
+    /**
+     * Matches when the SAML status of a {@link StatusResponseType} instance is equal to the given code.
+     * @param expectedStatusCode
+     * @return
+     */
+    public static <T> Matcher<SAML2Object> isSamlStatusResponse(JBossSAMLURIConstants expectedStatus) {
+        return allOf(
+          instanceOf(StatusResponseType.class),
+          new SamlStatusResponseTypeMatcher(is(URI.create(expectedStatus.get())))
+        );
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
index 78d5b3f..d7b7230 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
@@ -16,8 +16,10 @@
  */
 package org.keycloak.testsuite.util.matchers;
 
+import java.io.IOException;
 import javax.ws.rs.core.Response;
 import org.apache.http.HttpResponse;
+import org.apache.http.util.EntityUtils;
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -40,6 +42,17 @@ public class HttpResponseStatusCodeMatcher extends BaseMatcher<HttpResponse> {
     }
 
     @Override
+    public void describeMismatch(Object item, Description description) {
+        Description d = description.appendText("was ").appendValue(item)
+          .appendText(" with entity ");
+        try {
+            d.appendText(EntityUtils.toString(((HttpResponse) item).getEntity()));
+        } catch (IOException e) {
+            d.appendText("<Cannot decode entity: " + e.getMessage() + ">");
+        }
+    }
+
+    @Override
     public void describeTo(Description description) {
         description.appendText("response status code matches ").appendDescriptionOf(this.matcher);
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java
new file mode 100644
index 0000000..10f7359
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java
@@ -0,0 +1,44 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.keycloak.testsuite.util.matchers;
+
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import java.net.URI;
+import org.hamcrest.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlLogoutRequestTypeMatcher extends BaseMatcher<SAML2Object> {
+
+    private final Matcher<URI> destinationMatcher;
+
+    public SamlLogoutRequestTypeMatcher(URI destination) {
+        this.destinationMatcher = is(destination);
+    }
+
+    public SamlLogoutRequestTypeMatcher(Matcher<URI> destinationMatcher) {
+        this.destinationMatcher = destinationMatcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return destinationMatcher.matches(((LogoutRequestType) item).getDestination());
+    }
+
+    @Override
+    public void describeMismatch(Object item, Description description) {
+        description.appendText("was ").appendValue(((LogoutRequestType) item).getDestination());
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("SAML logout request destination matches ").appendDescriptionOf(this.destinationMatcher);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java
new file mode 100644
index 0000000..46eedde
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java
@@ -0,0 +1,47 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.keycloak.testsuite.util.matchers;
+
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import java.net.URI;
+import org.hamcrest.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlResponseTypeMatcher extends BaseMatcher<SAML2Object> {
+
+    private final Matcher<URI> statusMatcher;
+
+    public SamlResponseTypeMatcher(JBossSAMLURIConstants expectedStatus) {
+        this.statusMatcher = is(URI.create(expectedStatus.get()));
+    }
+
+    public SamlResponseTypeMatcher(Matcher<URI> statusMatcher) {
+        this.statusMatcher = statusMatcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return statusMatcher.matches(((ResponseType) item).getStatus().getStatusCode().getValue());
+    }
+
+    @Override
+    public void describeMismatch(Object item, Description description) {
+        description.appendText("was ").appendValue(((ResponseType) item).getStatus().getStatusCode());
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("SAML response status code matches ").appendDescriptionOf(this.statusMatcher);
+    }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
new file mode 100644
index 0000000..ccd5377
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java
@@ -0,0 +1,45 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.keycloak.testsuite.util.matchers;
+
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import java.net.URI;
+import org.hamcrest.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlStatusResponseTypeMatcher extends BaseMatcher<SAML2Object> {
+
+    private final Matcher<URI> statusMatcher;
+
+    public SamlStatusResponseTypeMatcher(URI statusMatcher) {
+        this.statusMatcher = is(statusMatcher);
+    }
+
+    public SamlStatusResponseTypeMatcher(Matcher<URI> statusMatcher) {
+        this.statusMatcher = statusMatcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return statusMatcher.matches(((StatusResponseType) item).getStatus().getStatusCode().getValue());
+    }
+
+    @Override
+    public void describeMismatch(Object item, Description description) {
+        description.appendText("was ").appendValue(((StatusResponseType) item).getStatus().getStatusCode().getValue());
+    }
+
+    @Override
+    public void describeTo(Description description) {
+        description.appendText("SAML status response status matches ").appendDescriptionOf(this.statusMatcher);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
index ccaca6c..5d5675f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
@@ -58,11 +58,11 @@ import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 
+import org.apache.http.protocol.HttpContext;
 import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.assertThat;
 import static org.keycloak.testsuite.admin.Users.getPasswordOf;
 import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot;
-import static org.keycloak.testsuite.util.IOUtil.documentToString;
 import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
 
 /**
@@ -221,9 +221,11 @@ public class SamlClient {
     public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
         org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
         Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
-        assertThat("Checking uniqueness of SAMLResponse input field in the page", samlResponses, hasSize(1));
+        Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]");
+        int size = samlResponses.size() + samlRequests.size();
+        assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1));
 
-        Element respElement = samlResponses.first();
+        Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
 
         return SAMLRequestParser.parseResponsePostBinding(respElement.val());
     }
@@ -237,15 +239,15 @@ public class SamlClient {
     public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
         List<NameValuePair> params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
 
-        String samlResponse = null;
+        String samlDoc = null;
         for (NameValuePair param : params) {
-            if ("SAMLResponse".equals(param.getName())) {
-                assertThat(samlResponse, nullValue());
-                samlResponse = param.getValue();
+            if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
+                assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue());
+                samlDoc = param.getValue();
             }
         }
 
-        return SAMLRequestParser.parseResponseRedirectBinding(samlResponse);
+        return SAMLRequestParser.parseResponseRedirectBinding(samlDoc);
     }
 
     /**
@@ -386,23 +388,14 @@ public class SamlClient {
      */
     public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
                                            Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
-        return login(user, samlEndpoint, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true);
+        return new SamlClient(samlEndpoint).login(user, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true);
     }
 
-    /**
-     * Send request for login form and then login using user param. This method is designed for clients which requires consent
-     *
-     * @param user
-     * @param samlEndpoint
-     * @param samlRequest
-     * @param relayState
-     * @param requestBinding
-     * @param expectedResponseBinding
-     * @return
-     */
-    public static SAMLDocumentHolder loginWithRequiredConsent(UserRepresentation user, URI samlEndpoint,
-                                                              Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consent) {
-        return login(user, samlEndpoint, samlRequest, relayState, requestBinding, expectedResponseBinding, true, consent);
+    private final HttpClientContext context = HttpClientContext.create();
+    private final URI samlEndpoint;
+
+    public SamlClient(URI samlEndpoint) {
+        this.samlEndpoint = samlEndpoint;
     }
 
     /**
@@ -418,15 +411,11 @@ public class SamlClient {
      * @param consent
      * @return
      */
-    public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
-                                           Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
-        CloseableHttpResponse response = null;
-        SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
-        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
-            HttpClientContext context = HttpClientContext.create();
-
+    public SAMLDocumentHolder login(UserRepresentation user,
+                                    Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
+        return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
             HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
-            response = client.execute(post, context);
+            CloseableHttpResponse response = client.execute(post, context);
 
             assertThat(response, statusCodeIsHC(Response.Status.OK));
             String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
@@ -444,8 +433,90 @@ public class SamlClient {
             }
 
             strategy.setRedirectable(false);
-            response = client.execute(loginRequest, context);
-            
+            return client.execute(loginRequest, context);
+        });
+    }
+
+    /**
+     * Send request for login form once already logged in, hence login using SSO.
+     * Check whether client requires consent and handle consent page.
+     *
+     * @param user
+     * @param samlEndpoint
+     * @param samlRequest
+     * @param relayState
+     * @param requestBinding
+     * @param expectedResponseBinding
+     * @return
+     */
+    public SAMLDocumentHolder subsequentLoginViaSSO(Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
+        return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
+            strategy.setRedirectable(false);
+
+            HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
+            CloseableHttpResponse response = client.execute(post, context);
+            assertThat(response, statusCodeIsHC(Response.Status.FOUND));
+            String location = response.getFirstHeader("Location").getValue();
+
+            response = client.execute(new HttpGet(location), context);
+            assertThat(response, statusCodeIsHC(Response.Status.OK));
+            return response;
+        });
+    }
+
+    /**
+     * Send request for login form once already logged in, hence login using SSO.
+     * Check whether client requires consent and handle consent page.
+     *
+     * @param user
+     * @param samlEndpoint
+     * @param samlRequest
+     * @param relayState
+     * @param requestBinding
+     * @param expectedResponseBinding
+     * @return
+     */
+    public SAMLDocumentHolder logout(Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
+        return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
+            strategy.setRedirectable(false);
+
+            HttpUriRequest post = requestBinding.createSamlUnsignedRequest(samlEndpoint, relayState, samlRequest);
+            CloseableHttpResponse response = client.execute(post, context);
+            assertThat(response, statusCodeIsHC(Response.Status.OK));
+            return response;
+        });
+    }
+
+    @FunctionalInterface
+    public interface HttpClientProcessor {
+        public CloseableHttpResponse process(CloseableHttpClient client, HttpContext context, RedirectStrategyWithSwitchableFollowRedirect strategy) throws Exception;
+    }
+
+    public void execute(HttpClientProcessor body) {
+        CloseableHttpResponse response = null;
+        RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect();
+
+        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
+            response = body.process(client, context, strategy);
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            if (response != null) {
+                EntityUtils.consumeQuietly(response.getEntity());
+                try {
+                    response.close();
+                } catch (IOException ex) {
+                }
+            }
+        }
+    }
+
+    public SAMLDocumentHolder getSamlResponse(Binding expectedResponseBinding, HttpClientProcessor body) {
+        CloseableHttpResponse response = null;
+        RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect();
+        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
+            response = body.process(client, context, strategy);
+
             return expectedResponseBinding.extractResponse(response);
         } catch (Exception ex) {
             throw new RuntimeException(ex);
@@ -469,7 +540,7 @@ public class SamlClient {
      * @return
      */
     public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) {
-        return idpInitiatedLogin(user, idpInitiatedURI, expectedResponseBinding, false, true);
+        return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, false, true);
     }
 
     /**
@@ -482,29 +553,24 @@ public class SamlClient {
      * @return
      */
     public static SAMLDocumentHolder idpInitiatedLoginWithRequiredConsent(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consent) {
-        return idpInitiatedLogin(user, idpInitiatedURI, expectedResponseBinding, true, consent);
+        return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, true, consent);
     }
 
     /**
      * Send request for login form and then login using user param. Checks whether client requires consent and handle consent page.
      *
      * @param user
-     * @param idpInitiatedURI
+     * @param samlEndpoint
      * @param expectedResponseBinding
      * @param consent
      * @return
      */
-    public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
-        CloseableHttpResponse response = null;
-        SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
-        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
-
-            HttpGet get = new HttpGet(idpInitiatedURI);
-            response = client.execute(get);
+    public SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
+        return getSamlResponse(expectedResponseBinding, (client, context, strategy) -> {
+            HttpGet get = new HttpGet(samlEndpoint);
+            CloseableHttpResponse response = client.execute(get);
             assertThat(response, statusCodeIsHC(Response.Status.OK));
 
-            HttpClientContext context = HttpClientContext.create();
-
             String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
             response.close();
 
@@ -520,20 +586,8 @@ public class SamlClient {
             }
 
             strategy.setRedirectable(false);
-            response = client.execute(loginRequest, context);
-
-            return expectedResponseBinding.extractResponse(response);
-        } catch (Exception ex) {
-            throw new RuntimeException(ex);
-        } finally {
-            if (response != null) {
-                EntityUtils.consumeQuietly(response.getEntity());
-                try {
-                    response.close();
-                } catch (IOException ex) {
-                }
-            }
-        }
+            return client.execute(loginRequest, context);
+        });
     }