From 40a8f0a2432d73bbbea318b7afac0dba76e7da38 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Mon, 12 Dec 2016 13:56:09 +0100 Subject: [PATCH] KEYCLOAK-3950 - Tests for SAML Name ID format variants in AuthnRequest --- .../api/saml/v2/request/SAML2Request.java | 10 +- ...igned-redirect-response-two-extensions.xml | 4 +- .../saml/AuthnRequestNameIdFormatTest.java | 200 +++++++++++++ .../org/keycloak/testsuite/util/Matchers.java | 32 ++- .../keycloak/testsuite/util/SamlClient.java | 266 ++++++++++++++++++ .../HttpResponseStatusCodeMatcher.java | 47 ++++ 6 files changed, 550 insertions(+), 9 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/SamlClient.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java 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 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 matcher; + + public HttpResponseStatusCodeMatcher(Matcher 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); + } + +}