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 3b4b5db6e0..9830482471 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 94a6fdb375..20a79971c1 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 @@
http://localhost:8081/auth/realms/saml-demo
-
+
+ text contents
+
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 0000000000..e9b4ac66d8
--- /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 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 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 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 b66e728962..7ff72a5347 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;
/**
@@ -48,6 +47,15 @@ public class Matchers {
return new ResponseStatusCodeMatcher(matcher);
}
+ /**
+ * Matcher on HTTP status code of a {@link Response} instance (HttpClient variant).
+ * @param matcher
+ * @return
+ */
+ public static Matcher 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
@@ -57,6 +65,15 @@ public class Matchers {
return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode.getStatusCode()));
}
+ /**
+ * 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 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
@@ -66,6 +83,15 @@ public class Matchers {
return new ResponseStatusCodeMatcher(org.hamcrest.Matchers.is(expectedStatusCode));
}
+ /**
+ * 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 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
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 0000000000..7af34f8a9a
--- /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 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 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 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 <AuthnRequest> description for more details.
+ * @param issuer
+ * @param assertionConsumerURL
+ * @param destination
+ * @return
+ */
+ public static AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, URI destination) {
+ try {
+ SAML2Request samlReq = new SAML2Request();
+ AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, destination.toString(), issuer);
+
+ return loginReq;
+ } catch (ConfigurationException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+}
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 0000000000..78d5b3f434
--- /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 {
+
+ 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);
+ }
+
+}