keycloak-uncached

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 3b4b5db..9830482 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
@@ -243,7 +243,7 @@ public class SAML2Request {
      *
      * @throws ConfigurationException
      */
-    public LogoutRequestType createLogoutRequest(String issuer) throws ConfigurationException {
+    public static LogoutRequestType createLogoutRequest(String issuer) throws ConfigurationException {
         LogoutRequestType lrt = new LogoutRequestType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
 
         // Create an issuer
@@ -266,7 +266,7 @@ public class SAML2Request {
      * @throws ParsingException
      * @throws ConfigurationException
      */
-    public Document convert(RequestAbstractType rat) throws ProcessingException, ConfigurationException, ParsingException {
+    public static Document convert(RequestAbstractType rat) throws ProcessingException, ConfigurationException, ParsingException {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
 
         SAMLRequestWriter writer = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(bos));
@@ -290,7 +290,7 @@ public class SAML2Request {
      * @throws ParsingException
      * @throws ConfigurationException
      */
-    public Document convert(ResponseType responseType) throws ProcessingException, ParsingException, ConfigurationException {
+    public static Document convert(ResponseType responseType) throws ProcessingException, ParsingException, ConfigurationException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         SAMLResponseWriter writer = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(baos));
         writer.write(responseType);
@@ -307,7 +307,7 @@ public class SAML2Request {
      *
      * @throws ProcessingException
      */
-    public void marshall(RequestAbstractType requestType, OutputStream os) throws ProcessingException {
+    public static void marshall(RequestAbstractType requestType, OutputStream os) throws ProcessingException {
         SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(os));
         if (requestType instanceof AuthnRequestType) {
             samlRequestWriter.write((AuthnRequestType) requestType);
@@ -325,7 +325,7 @@ public class SAML2Request {
      *
      * @throws ProcessingException
      */
-    public void marshall(RequestAbstractType requestType, Writer writer) throws ProcessingException {
+    public static void marshall(RequestAbstractType requestType, Writer writer) throws ProcessingException {
         SAMLRequestWriter samlRequestWriter = new SAMLRequestWriter(StaxUtil.getXMLStreamWriter(writer));
         if (requestType instanceof AuthnRequestType) {
             samlRequestWriter.write((AuthnRequestType) requestType);
diff --git a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
index 94a6fdb..20a7997 100644
--- a/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
+++ b/saml-core/src/test/resources/org/keycloak/saml/processing/core/parsers/saml/saml20-encrypted-signed-redirect-response-two-extensions.xml
@@ -2,7 +2,9 @@
     <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8081/auth/realms/saml-demo</saml:Issuer>
     <samlp:Extensions>
         <kckey:KeyInfo xmlns:kckey="urn:keycloak:ext:key:1.0" MessageSigningKeyId="FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"/>
-        <what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever"/>
+        <what:ever xmlns:what="urn:keycloak:ext:what:1.0" what="ever">
+            <nested><element>text contents</element></nested>
+        </what:ever>
     </samlp:Extensions>
     <samlp:Status>
         <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
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
new file mode 100644
index 0000000..e9b4ac6
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.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 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;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class AuthnRequestNameIdFormatTest extends AbstractAuthTest {
+
+    private static final String REALM_NAME = "demo";
+
+    private static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
+    private static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
+
+    public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
+      Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
+        CloseableHttpResponse response = null;
+        SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
+        try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
+            HttpClientContext context = HttpClientContext.create();
+
+            HttpUriRequest post = requestBinding.createSamlRequest(samlEndpoint, relayState, samlRequest);
+            response = client.execute(post, context);
+
+            assertThat(response, statusCodeIsHC(Response.Status.OK));
+            String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
+            response.close();
+
+            assertThat(loginPageText, containsString("login"));
+
+            HttpUriRequest loginRequest = handleLoginPage(user, loginPageText);
+
+            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) { }
+            }
+        }
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
+    }
+
+    public AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, String realmName) {
+        return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
+    }
+
+    private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
+        return RealmsResource
+          .protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
+          .build(realm, SamlProtocol.LOGIN_PROTOCOL);
+    }
+
+    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);
+
+        assertThat(res.getSamlObject(), notNullValue());
+        assertThat(res.getSamlObject(), instanceOf(ResponseType.class));
+
+        ResponseType rt = (ResponseType) res.getSamlObject();
+        assertThat(rt.getAssertions(), not(empty()));
+        assertThat(rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class));
+        NameIDType nameId = (NameIDType) rt.getAssertions().get(0).getAssertion().getSubject().getSubType().getBaseID();
+        assertThat(nameId.getValue(), nameIdMatcher);
+    }
+
+    @Test
+    public void testPostLoginNameIdPolicyUnspecified() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get()));
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, is("bburke"));
+    }
+
+    @Test
+    public void testPostLoginNameIdPolicyEmail() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()));
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, is("bburke@redhat.com"));
+    }
+
+    @Test
+    public void testPostLoginNameIdPolicyPersistent() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()));
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, nameIdPolicy, startsWith("G-"));
+    }
+
+    @Test
+    public void testPostLoginNoNameIdPolicyUnset() throws Exception {
+        testLoginWithNameIdPolicy(Binding.POST, Binding.POST, null, is("bburke"));
+    }
+
+    @Test
+    public void testRedirectLoginNameIdPolicyUnspecified() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get()));
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, is("bburke"));
+    }
+
+    @Test
+    public void testRedirectLoginNameIdPolicyEmail() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()));
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, is("bburke@redhat.com"));
+    }
+
+    @Test
+    public void testRedirectLoginNameIdPolicyPersistent() throws Exception {
+        NameIDPolicyType nameIdPolicy = new NameIDPolicyType();
+        nameIdPolicy.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()));
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, nameIdPolicy, startsWith("G-"));
+    }
+
+    @Test
+    public void testRedirectLoginNoNameIdPolicyUnset() throws Exception {
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.REDIRECT, null, is("bburke"));
+    }
+
+    @Test
+    public void testRedirectLoginNoNameIdPolicyForcePostBinding() throws Exception {
+        ClientsResource clients = adminClient.realm(REALM_NAME).clients();
+        List<ClientRepresentation> foundClients = clients.findByClientId("http://localhost:8081/sales-post/");
+        assertThat(foundClients, hasSize(1));
+        ClientResource clientRes = clients.get(foundClients.get(0).getId());
+        ClientRepresentation client = clientRes.toRepresentation();
+        client.getAttributes().put(SamlConfigAttributes.SAML_FORCE_POST_BINDING, "true");
+        clientRes.update(client);
+
+        testLoginWithNameIdPolicy(Binding.REDIRECT, Binding.POST, null, is("bburke"));
+    }
+
+}
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 b66e728..7ff72a5 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,11 @@
  */
 package org.keycloak.testsuite.util;
 
-import org.keycloak.testsuite.util.matchers.ResponseBodyMatcher;
-import org.keycloak.testsuite.util.matchers.ResponseHeaderMatcher;
-import org.keycloak.testsuite.util.matchers.ResponseStatusCodeMatcher;
+import org.keycloak.testsuite.util.matchers.*;
 
 import java.util.Map;
 import javax.ws.rs.core.Response;
+import org.apache.http.HttpResponse;
 import org.hamcrest.Matcher;
 
 /**
@@ -49,6 +48,15 @@ public class Matchers {
     }
 
     /**
+     * Matcher on HTTP status code of a {@link Response} instance (HttpClient variant).
+     * @param matcher
+     * @return
+     */
+    public static Matcher<HttpResponse> statusCodeHC(Matcher<? extends Number> matcher) {
+        return new HttpResponseStatusCodeMatcher(matcher);
+    }
+
+    /**
      * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
      * @param expectedStatusCode
      * @return
@@ -58,6 +66,15 @@ public class Matchers {
     }
 
     /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code (HttpClient variant).
+     * @param expectedStatusCode
+     * @return
+     */
+    public static Matcher<HttpResponse> statusCodeIsHC(Response.Status expectedStatusCode) {
+        return new HttpResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
+    }
+
+    /**
      * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
      * @param expectedStatusCode
      * @return
@@ -67,6 +84,15 @@ public class Matchers {
     }
 
     /**
+     * Matches when the HTTP status code of a {@link Response} instance is equal to the given code (HttpClient variant).
+     * @param expectedStatusCode
+     * @return
+     */
+    public static Matcher<HttpResponse> statusCodeIsHC(int expectedStatusCode) {
+        return new HttpResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
+    }
+
+    /**
      * Matches when the HTTP status code of a {@link Response} instance is equal to the given code.
      * @param expectedStatusCode
      * @return
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
new file mode 100644
index 0000000..78d5b3f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.util.matchers;
+
+import javax.ws.rs.core.Response;
+import org.apache.http.HttpResponse;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Matcher for matching status code of {@link Response} instance.
+ * @author hmlnarik
+ */
+public class HttpResponseStatusCodeMatcher extends BaseMatcher<HttpResponse> {
+
+    private final Matcher<? extends Number> matcher;
+
+    public HttpResponseStatusCodeMatcher(Matcher<? extends Number> matcher) {
+        this.matcher = matcher;
+    }
+
+    @Override
+    public boolean matches(Object item) {
+        return (item instanceof HttpResponse) && this.matcher.matches(((HttpResponse) item).getStatusLine().getStatusCode());
+    }
+
+    @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/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
new file mode 100644
index 0000000..7af34f8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java
@@ -0,0 +1,266 @@
+/*
+ * 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.dom.saml.v2.protocol.AuthnRequestType;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.saml.BaseSAML2BindingBuilder;
+import org.keycloak.saml.SAMLRequestParser;
+import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.exceptions.ConfigurationException;
+import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
+import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+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.utils.URLEncodedUtils;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import org.w3c.dom.Document;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertThat;
+import static org.keycloak.testsuite.admin.Users.getPasswordOf;
+import static org.keycloak.testsuite.util.Matchers.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class SamlClient {
+
+    /**
+     * SAML bindings and related HttpClient methods.
+     */
+    public enum Binding {
+        POST {
+            @Override
+            public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
+                assertThat(response, statusCodeIsHC(Response.Status.OK));
+                String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
+                response.close();
+                return extractSamlResponseFromForm(responsePage);
+            }
+
+            @Override
+            public HttpPost createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest) {
+                HttpPost post = new HttpPost(samlEndpoint);
+
+                List<NameValuePair> parameters = new LinkedList<>();
+                try {
+                    parameters.add(
+                      new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY,
+                      new BaseSAML2BindingBuilder()
+                        .postBinding(samlRequest)
+                        .encoded())
+                    );
+                } catch (ProcessingException | ConfigurationException | IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+                if (relayState != null) {
+                    parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayState));
+                }
+
+                UrlEncodedFormEntity formEntity;
+                try {
+                    formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    throw new RuntimeException(e);
+                }
+                post.setEntity(formEntity);
+
+                return post;
+            }
+
+            @Override
+            public URI getBindingUri() {
+                return URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
+            }
+        },
+
+        REDIRECT {
+            @Override
+            public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
+                assertThat(response, statusCodeIsHC(Response.Status.FOUND));
+                String location = response.getFirstHeader("Location").getValue();
+                response.close();
+                return extractSamlResponseFromRedirect(location);
+            }
+
+            @Override
+            public HttpGet createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest) {
+                try {
+                    URI requestURI = new BaseSAML2BindingBuilder()
+                      .relayState(relayState)
+                      .redirectBinding(samlRequest)
+                      .requestURI(samlEndpoint.toString());
+                    return new HttpGet(requestURI);
+                } catch (ProcessingException | ConfigurationException | IOException ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+
+            @Override
+            public URI getBindingUri() {
+                return URI.create(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
+            }
+        };
+
+        public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException;
+        public abstract HttpUriRequest createSamlRequest(URI samlEndpoint, String relayState, Document samlRequest);
+        public abstract URI getBindingUri();
+    }
+
+    public static class RedirectStrategyWithSwitchableFollowRedirect extends LaxRedirectStrategy {
+
+        public boolean redirectable = true;
+
+        @Override
+        protected boolean isRedirectable(String method) {
+            return redirectable && super.isRedirectable(method);
+        }
+
+        public void setRedirectable(boolean redirectable) {
+            this.redirectable = redirectable;
+        }
+    }
+
+    /**
+     * Extracts and parses value of SAMLResponse input field of a form present in the given page.
+     * @param responsePage HTML code of the page
+     * @return
+     */
+    public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
+        org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
+        Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
+        assertThat("Checking uniqueness of SAMLResponse input field in the page", samlResponses, hasSize(1));
+
+        Element respElement = samlResponses.first();
+
+        return SAMLRequestParser.parseResponsePostBinding(respElement.val());
+    }
+
+    /**
+     * Extracts and parses value of SAMLResponse query parameter from the given URI.
+     * @param responseUri
+     * @return
+     */
+    public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
+        List<NameValuePair> params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
+
+        String samlResponse = null;
+        for (NameValuePair param : params) {
+            if ("SAMLResponse".equals(param.getName())) {
+                assertThat(samlResponse, nullValue());
+                samlResponse = param.getValue();
+            }
+        }
+
+        return SAMLRequestParser.parseResponseRedirectBinding(samlResponse);
+    }
+
+    /**
+     * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected
+     * to have at least input fields with id "username" and "password".
+     * @param user
+     * @param loginPage
+     * @return
+     */
+    public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) {
+        String username = user.getUsername();
+        String password = getPasswordOf(user);
+        org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage);
+
+        List<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);
+    }
+
+    /**
+     * Creates a SAML login request document with the given parameters. See SAML &lt;AuthnRequest&gt; description for more details.
+     * @param issuer
+     * @param assertionConsumerURL
+     * @param destination
+     * @return
+     */
+    public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) {
+        try {
+            SAML2Request samlReq = new SAML2Request();
+            AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer);
+
+            return loginReq;
+        } catch (ConfigurationException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+}