diff --git a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java index 00160e6e53..335a72d11e 100755 --- a/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java +++ b/saml-core/src/main/java/org/keycloak/saml/SAMLRequestParser.java @@ -56,10 +56,8 @@ public class SAMLRequestParser { is = new ByteArrayInputStream(message.getBytes(GeneralConstants.SAML_CHARSET)); } - SAML2Request saml2Request = new SAML2Request(); try { - saml2Request.getSAML2ObjectFromStream(is); - return saml2Request.getSamlDocumentHolder(); + return SAML2Request.getSAML2ObjectFromStream(is); } catch (Exception e) { logger.samlBase64DecodingError(e); } @@ -76,10 +74,8 @@ public class SAMLRequestParser { log.debug(str); } is = new ByteArrayInputStream(samlBytes); - SAML2Request saml2Request = new SAML2Request(); try { - saml2Request.getSAML2ObjectFromStream(is); - return saml2Request.getSamlDocumentHolder(); + return SAML2Request.getSAML2ObjectFromStream(is); } catch (Exception e) { logger.samlBase64DecodingError(e); } diff --git a/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 2bfa41f9db..cb8a348631 100755 --- a/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java +++ b/saml-core/src/main/java/org/keycloak/saml/processing/api/saml/v2/request/SAML2Request.java @@ -156,7 +156,7 @@ public class SAML2Request { * @throws IOException * @throws ParsingException */ - public SAML2Object getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException, + public static SAMLDocumentHolder getSAML2ObjectFromStream(InputStream is) throws ConfigurationException, ParsingException, ProcessingException { if (is == null) throw logger.nullArgumentError("InputStream"); @@ -167,8 +167,7 @@ public class SAML2Request { JAXPValidationUtil.checkSchemaValidation(samlDocument); SAML2Object requestType = (SAML2Object) samlParser.parse(samlDocument); - samlDocumentHolder = new SAMLDocumentHolder(requestType, samlDocument); - return requestType; + return new SAMLDocumentHolder(requestType, samlDocument); } /** diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java index e0ac524b8e..589dde3e6b 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -138,6 +138,13 @@ public class SamlService extends AuthorizationEndpointBase { protected Response handleSamlResponse(String samlResponse, String relayState) { event.event(EventType.LOGOUT); SAMLDocumentHolder holder = extractResponseDocument(samlResponse); + + if (! (holder.getSamlObject() instanceof StatusResponseType)) { + event.detail(Details.REASON, "invalid_saml_response"); + event.error(Errors.INVALID_SAML_RESPONSE); + return ErrorPage.error(session, Messages.INVALID_REQUEST); + } + StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject(); // validate destination if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) { @@ -178,6 +185,12 @@ public class SamlService extends AuthorizationEndpointBase { SAML2Object samlObject = documentHolder.getSamlObject(); + if (! (samlObject instanceof RequestAbstractType)) { + event.event(EventType.LOGIN); + event.error(Errors.INVALID_SAML_AUTHN_REQUEST); + return ErrorPage.error(session, Messages.INVALID_REQUEST); + } + RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject; String issuer = requestAbstractType.getIssuer().getValue(); ClientModel client = realm.getClientByClientId(issuer); diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 79f241b129..b2c06ad85d 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -78,6 +78,11 @@ junit compile + + org.hamcrest + hamcrest-all + compile + org.subethamail subethasmtp diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java index 0272a1bd1a..e967a33042 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/ClientAttributeUpdater.java @@ -4,7 +4,6 @@ import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.representations.idm.ClientRepresentation; import java.io.Closeable; import java.util.HashMap; -import java.util.Map; /** * @@ -12,14 +11,14 @@ import java.util.Map; */ public class ClientAttributeUpdater { - private final Map originalAttributes = new HashMap<>(); - private final ClientResource clientResource; private final ClientRepresentation rep; + private final ClientRepresentation origRep; public ClientAttributeUpdater(ClientResource clientResource) { this.clientResource = clientResource; + this.origRep = clientResource.toRepresentation(); this.rep = clientResource.toRepresentation(); if (this.rep.getAttributes() == null) { this.rep.setAttributes(new HashMap<>()); @@ -27,29 +26,23 @@ public class ClientAttributeUpdater { } public ClientAttributeUpdater setAttribute(String name, String value) { - if (! originalAttributes.containsKey(name)) { - this.originalAttributes.put(name, this.rep.getAttributes().put(name, value)); - } else { - this.rep.getAttributes().put(name, value); - } + this.rep.getAttributes().put(name, value); return this; } public ClientAttributeUpdater removeAttribute(String name) { - if (! originalAttributes.containsKey(name)) { - this.originalAttributes.put(name, this.rep.getAttributes().put(name, null)); - } else { - this.rep.getAttributes().put(name, null); - } + this.rep.getAttributes().put(name, null); + return this; + } + + public ClientAttributeUpdater setFrontchannelLogout(Boolean frontchannelLogout) { + rep.setFrontchannelLogout(frontchannelLogout); return this; } public Closeable update() { clientResource.update(rep); - return () -> { - rep.getAttributes().putAll(originalAttributes); - clientResource.update(rep); - }; + return () -> clientResource.update(origRep); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/KeyUtils.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/Matchers.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/Matchers.java diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java new file mode 100644 index 0000000000..06609b1f63 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClient.java @@ -0,0 +1,349 @@ +/* + * 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.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +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.keycloak.common.util.KeyUtils; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.saml.BaseSAML2BindingBuilder; +import org.keycloak.saml.SAMLRequestParser; +import org.keycloak.saml.SignatureAlgorithm; +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 org.w3c.dom.Document; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import org.jboss.logging.Logger; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + +/** + * @author hmlnarik + */ +public class SamlClient { + + @FunctionalInterface + public interface Step { + HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception; + } + + @FunctionalInterface + public interface ResultExtractor { + T extract(CloseableHttpResponse response) throws Exception; + } + + public static final class DoNotFollowRedirectStep implements Step { + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI uri, CloseableHttpResponse response, HttpClientContext context) throws Exception { + return null; + } + } + + 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; + } + } + + /** + * 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 createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest) { + return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, null, null); + } + + @Override + public HttpPost createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { + return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_RESPONSE_KEY, null, null); + } + + @Override + public HttpPost createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { + return createSamlPostMessage(samlEndpoint, relayState, samlRequest, GeneralConstants.SAML_REQUEST_KEY, realmPrivateKey, realmPublicKey); + } + + private HttpPost createSamlPostMessage(URI samlEndpoint, String relayState, Document samlRequest, String messageType, String privateKeyStr, String publicKeyStr) { + HttpPost post = new HttpPost(samlEndpoint); + + List parameters = new LinkedList<>(); + + + try { + BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder(); + + if (privateKeyStr != null && publicKeyStr != null) { + PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr); + PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr); + binding + .signatureAlgorithm(SignatureAlgorithm.RSA_SHA256) + .signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey) + .signDocument(); + } + + parameters.add( + new BasicNameValuePair(messageType, + binding + .postBinding(samlRequest) + .encoded()) + ); + } catch (IOException | ConfigurationException | ProcessingException 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 createSamlUnsignedRequest(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()); + } + + @Override + public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { + return null; + } + + @Override + public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { + return null; + } + }; + + public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException; + + public abstract HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest); + + public abstract HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey); + + public abstract URI getBindingUri(); + + public abstract HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest); + } + + private static final Logger LOG = Logger.getLogger(SamlClient.class); + + private final HttpClientContext context = HttpClientContext.create(); + + private final RedirectStrategyWithSwitchableFollowRedirect strategy = new RedirectStrategyWithSwitchableFollowRedirect(); + + /** + * 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]"); + 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.isEmpty() ? samlRequests.first() : 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 samlDoc = null; + for (NameValuePair param : params) { + if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) { + assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue()); + samlDoc = param.getValue(); + } + } + + return SAMLRequestParser.parseResponseRedirectBinding(samlDoc); + } + + /** + * 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); + } + } + + public T executeAndTransform(ResultExtractor resultTransformer, List steps) { + CloseableHttpResponse currentResponse = null; + URI currentUri = URI.create("about:blank"); + strategy.setRedirectable(true); + + try (CloseableHttpClient client = createHttpClientBuilderInstance().setRedirectStrategy(strategy).build()) { + for (int i = 0; i < steps.size(); i ++) { + Step s = steps.get(i); + LOG.infof("Running step %d: %s", i, s.getClass()); + + CloseableHttpResponse origResponse = currentResponse; + + HttpUriRequest request = s.perform(client, currentUri, origResponse, context); + if (request == null) { + LOG.info("Last step returned no request, continuing with next step."); + continue; + } + + // Setting of follow redirects has to be set before executing the final request of the current step + if (i < steps.size() - 1 && steps.get(i + 1) instanceof DoNotFollowRedirectStep) { + LOG.debugf("Disabling following redirects"); + strategy.setRedirectable(false); + i++; + } else { + strategy.setRedirectable(true); + } + + LOG.infof("Executing HTTP request to %s", request.getURI()); + currentResponse = client.execute(request, context); + + currentUri = request.getURI(); + List locations = context.getRedirectLocations(); + if (locations != null && ! locations.isEmpty()) { + currentUri = locations.get(locations.size() - 1); + } + + LOG.infof("Landed to %s", currentUri); + + if (currentResponse != origResponse && origResponse != null) { + origResponse.close(); + } + } + + LOG.info("Going to extract response"); + + return resultTransformer.extract(currentResponse); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public HttpClientContext getContext() { + return context; + } + + protected HttpClientBuilder createHttpClientBuilderInstance() { + return HttpClientBuilder.create(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java new file mode 100644 index 0000000000..89d309249c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/SamlClientBuilder.java @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util; + +import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClient.DoNotFollowRedirectStep; +import org.keycloak.testsuite.util.SamlClient.ResultExtractor; +import org.keycloak.testsuite.util.SamlClient.Step; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.keycloak.testsuite.util.saml.CreateAuthnRequestStepBuilder; +import org.keycloak.testsuite.util.saml.CreateLogoutRequestStepBuilder; +import org.keycloak.testsuite.util.saml.IdPInitiatedLoginBuilder; +import org.keycloak.testsuite.util.saml.LoginBuilder; +import org.keycloak.testsuite.util.saml.ModifySamlResponseStepBuilder; +import org.keycloak.testsuite.util.saml.RequiredConsentBuilder; +import org.w3c.dom.Document; + +/** + * + * @author hmlnarik + */ +public class SamlClientBuilder { + + private final List steps = new LinkedList<>(); + + public SamlClient execute(Consumer resultConsumer) { + final SamlClient samlClient = new SamlClient(); + samlClient.executeAndTransform(r -> { + resultConsumer.accept(r); + return null; + }, steps); + return samlClient; + } + + public T executeAndTransform(ResultExtractor resultTransformer) { + return new SamlClient().executeAndTransform(resultTransformer, steps); + } + + public List getSteps() { + return steps; + } + + public T addStep(T step) { + steps.add(step); + return step; + } + + public SamlClientBuilder doNotFollowRedirects() { + this.steps.add(new DoNotFollowRedirectStep()); + return this; + } + + public SamlClientBuilder clearCookies() { + this.steps.add((client, currentURI, currentResponse, context) -> { + context.getCookieStore().clear(); + return null; + }); + return this; + } + + /** Creates fresh and issues an AuthnRequest to the SAML endpoint */ + public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding) { + return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, issuer, assertionConsumerURL, requestBinding, this)); + } + + /** Issues the given AuthnRequest to the SAML endpoint */ + public CreateAuthnRequestStepBuilder authnRequest(URI authServerSamlUrl, Document authnRequestDocument, Binding requestBinding) { + return addStep(new CreateAuthnRequestStepBuilder(authServerSamlUrl, authnRequestDocument, requestBinding, this)); + } + + /** Issues the given AuthnRequest to the SAML endpoint */ + public CreateLogoutRequestStepBuilder logoutRequest(URI authServerSamlUrl, String issuer, Binding requestBinding) { + return addStep(new CreateLogoutRequestStepBuilder(authServerSamlUrl, issuer, requestBinding, this)); + } + + /** Handles login page */ + public LoginBuilder login() { + return addStep(new LoginBuilder(this)); + } + + /** Starts IdP-initiated flow for the given client */ + public IdPInitiatedLoginBuilder idpInitiatedLogin(URI authServerSamlUrl, String clientId) { + return addStep(new IdPInitiatedLoginBuilder(authServerSamlUrl, clientId, this)); + } + + /** Handles "Requires consent" page */ + public RequiredConsentBuilder consentRequired() { + return addStep(new RequiredConsentBuilder(this)); + } + + /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */ + public SAMLDocumentHolder getSamlResponse(Binding responseBinding) { + return + doNotFollowRedirects() + .executeAndTransform(responseBinding::extractResponse); + } + + /** Returns SAML request or response as replied from server. Note that the redirects are disabled for this to work. */ + public ModifySamlResponseStepBuilder processSamlResponse(Binding responseBinding) { + return + doNotFollowRedirects() + .addStep(new ModifySamlResponseStepBuilder(responseBinding, this)); + } + + public SamlClientBuilder navigateTo(String httpGetUri) { + steps.add((client, currentURI, currentResponse, context) -> { + return new HttpGet(httpGetUri); + }); + return this; + } + + public SamlClientBuilder navigateTo(URI httpGetUri) { + steps.add((client, currentURI, currentResponse, context) -> { + return new HttpGet(httpGetUri); + }); + return this; + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseBodyMatcher.java 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/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/HttpResponseStatusCodeMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseBodyMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseHeaderMatcher.java diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/ResponseStatusCodeMatcher.java 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/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlLogoutRequestTypeMatcher.java 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/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java similarity index 100% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlResponseTypeMatcher.java 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/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java similarity index 95% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java rename to testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java index ccd5377865..d76a6dd56c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/matchers/SamlStatusResponseTypeMatcher.java @@ -6,7 +6,6 @@ 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.*; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java new file mode 100644 index 0000000000..aa85821179 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateAuthnRequestStepBuilder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.testsuite.util.SamlClient.Binding; +import java.net.URI; +import java.util.UUID; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.w3c.dom.Document; + + +public class CreateAuthnRequestStepBuilder extends SamlDocumentStepBuilder { + + private final String issuer; + private final URI authServerSamlUrl; + private final Binding requestBinding; + private final String assertionConsumerURL; + + private final Document forceLoginRequestDocument; + + private String relayState; + + public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, String issuer, String assertionConsumerURL, Binding requestBinding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.issuer = issuer; + this.authServerSamlUrl = authServerSamlUrl; + this.requestBinding = requestBinding; + this.assertionConsumerURL = assertionConsumerURL; + + this.forceLoginRequestDocument = null; + } + + public CreateAuthnRequestStepBuilder(URI authServerSamlUrl, Document loginRequestDocument, Binding requestBinding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.forceLoginRequestDocument = loginRequestDocument; + + this.authServerSamlUrl = authServerSamlUrl; + this.requestBinding = requestBinding; + + this.issuer = null; + this.assertionConsumerURL = null; + } + + public String assertionConsumerURL() { + return assertionConsumerURL; + } + + public String relayState() { + return relayState; + } + + public void relayState(String relayState) { + this.relayState = relayState; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + Document doc = createLoginRequestDocument(); + + String documentAsString = DocumentUtil.getDocumentAsString(doc); + String transformed = getTransformer().transform(documentAsString); + + if (transformed == null) { + return null; + } + + return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState, DocumentUtil.getDocument(transformed)); + } + + protected Document createLoginRequestDocument() { + if (this.forceLoginRequestDocument != null) { + return this.forceLoginRequestDocument; + } + + try { + SAML2Request samlReq = new SAML2Request(); + AuthnRequestType loginReq = samlReq.createAuthnRequestType(UUID.randomUUID().toString(), assertionConsumerURL, this.authServerSamlUrl.toString(), issuer); + + return SAML2Request.convert(loginReq); + } catch (ConfigurationException | ParsingException | ProcessingException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java new file mode 100644 index 0000000000..ee594d0a22 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/CreateLogoutRequestStepBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.assertion.NameIDType; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.saml.SAML2LogoutRequestBuilder; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.testsuite.util.SamlClient.Binding; +import java.net.URI; +import java.util.function.Supplier; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; + +/** + * + * @author hmlnarik + */ +public class CreateLogoutRequestStepBuilder extends SamlDocumentStepBuilder { + + private final URI authServerSamlUrl; + private final String issuer; + private final Binding requestBinding; + + private Supplier sessionIndex = () -> null; + private Supplier nameId = () -> null; + private Supplier relayState = () -> null; + + public CreateLogoutRequestStepBuilder(URI authServerSamlUrl, String issuer, Binding requestBinding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.authServerSamlUrl = authServerSamlUrl; + this.issuer = issuer; + this.requestBinding = requestBinding; + } + + public String sessionIndex() { + return sessionIndex.get(); + } + + public CreateLogoutRequestStepBuilder sessionIndex(String sessionIndex) { + this.sessionIndex = () -> sessionIndex; + return this; + } + + public CreateLogoutRequestStepBuilder sessionIndex(Supplier sessionIndex) { + this.sessionIndex = sessionIndex; + return this; + } + + public String relayState() { + return relayState.get(); + } + + public CreateLogoutRequestStepBuilder relayState(String relayState) { + this.relayState = () -> relayState; + return this; + } + + public CreateLogoutRequestStepBuilder relayState(Supplier relayState) { + this.relayState = relayState; + return this; + } + + public NameIDType nameId() { + return nameId.get(); + } + + public CreateLogoutRequestStepBuilder nameId(NameIDType nameId) { + this.nameId = () -> nameId; + return this; + } + + public CreateLogoutRequestStepBuilder nameId(Supplier nameId) { + this.nameId = nameId; + return this; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + SAML2LogoutRequestBuilder builder = new SAML2LogoutRequestBuilder() + .destination(authServerSamlUrl.toString()) + .issuer(issuer) + .sessionIndex(sessionIndex()); + + if (nameId() != null) { + builder = builder.userPrincipal(nameId().getValue(), nameId().getFormat().toString()); + } + + String documentAsString = DocumentUtil.getDocumentAsString(builder.buildDocument()); + String transformed = getTransformer().transform(documentAsString); + + if (transformed == null) { + return null; + } + + return requestBinding.createSamlUnsignedRequest(authServerSamlUrl, relayState(), DocumentUtil.getDocument(transformed)); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java new file mode 100644 index 0000000000..d119e64772 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/IdPInitiatedLoginBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClient.Step; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.net.URI; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; + +/** + * + * @author hmlnarik + */ +public class IdPInitiatedLoginBuilder implements Step { + + private final SamlClientBuilder clientBuilder; + private final URI authServerSamlUrl; + private final String clientId; + + public IdPInitiatedLoginBuilder(URI authServerSamlUrl, String clientId, SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + this.authServerSamlUrl = authServerSamlUrl; + this.clientId = clientId; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + return new HttpGet(authServerSamlUrl.toString() + "/clients/" + this.clientId); + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java new file mode 100644 index 0000000000..4e5713b403 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/LoginBuilder.java @@ -0,0 +1,144 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.SamlClient.Step; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.admin.Users.getPasswordOf; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + +/** + * + * @author hmlnarik + */ +public class LoginBuilder implements Step { + + private final SamlClientBuilder clientBuilder; + private UserRepresentation user; + private boolean sso = false; + + public LoginBuilder(SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + if (sso) { + return null; // skip this step + } else { + assertThat(currentResponse, statusCodeIsHC(Response.Status.OK)); + String loginPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8"); + assertThat(loginPageText, containsString("login")); + + return handleLoginPage(loginPageText); + } + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } + + public LoginBuilder user(UserRepresentation user) { + this.user = user; + return this; + } + + public LoginBuilder sso(boolean sso) { + this.sso = sso; + return this; + } + + /** + * Prepares a GET/POST request for logging the given user into the given login page. The login page is expected + * to have at least input fields with id "username" and "password". + * + * @param user + * @param loginPage + * @return + */ + private HttpUriRequest handleLoginPage(String loginPage) { + return handleLoginPage(user, loginPage); + } + + public static HttpUriRequest handleLoginPage(UserRepresentation user, String loginPage) { + String username = user.getUsername(); + String password = getPasswordOf(user); + org.jsoup.nodes.Document theLoginPage = Jsoup.parse(loginPage); + + List parameters = new LinkedList<>(); + for (Element form : theLoginPage.getElementsByTag("form")) { + String method = form.attr("method"); + String action = form.attr("action"); + boolean isPost = method != null && "post".equalsIgnoreCase(method); + + for (Element input : form.getElementsByTag("input")) { + if (Objects.equals(input.id(), "username")) { + parameters.add(new BasicNameValuePair(input.attr("name"), username)); + } else if (Objects.equals(input.id(), "password")) { + parameters.add(new BasicNameValuePair(input.attr("name"), password)); + } else { + parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); + } + } + + if (isPost) { + HttpPost res = new HttpPost(action); + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + res.setEntity(formEntity); + + return res; + } else { + UriBuilder b = UriBuilder.fromPath(action); + for (NameValuePair parameter : parameters) { + b.queryParam(parameter.getName(), parameter.getValue()); + } + return new HttpGet(b.build()); + } + } + + throw new IllegalArgumentException("Invalid login form: " + loginPage); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java new file mode 100644 index 0000000000..e29091bf86 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/ModifySamlResponseStepBuilder.java @@ -0,0 +1,227 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.SAML2Object; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.processing.web.util.PostBindingUtil; +import org.keycloak.saml.processing.web.util.RedirectBindingUtil; +import org.keycloak.testsuite.util.SamlClient.Binding; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import javax.ws.rs.core.Response.Status; +import org.apache.commons.io.IOUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + + +public class ModifySamlResponseStepBuilder extends SamlDocumentStepBuilder { + + private final Binding binding; + + private URI targetUri; + private String targetAttribute; + private Binding targetBinding; + + public ModifySamlResponseStepBuilder(Binding binding, SamlClientBuilder clientBuilder) { + super(clientBuilder); + this.binding = binding; + this.targetBinding = binding; + } + + // TODO: support for signing + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + switch (binding) { + case REDIRECT: + return handleRedirectBinding(currentResponse); + + case POST: + return handlePostBinding(currentResponse); + } + + throw new RuntimeException("Unknown binding for " + ModifySamlResponseStepBuilder.class.getName()); + } + + public Binding targetBinding() { + return targetBinding; + } + + public ModifySamlResponseStepBuilder targetBinding(Binding targetBinding) { + this.targetBinding = targetBinding; + return this; + } + + public String targetAttribute() { + return targetAttribute; + } + + public ModifySamlResponseStepBuilder targetAttribute(String attribute) { + targetAttribute = attribute; + return this; + } + + public ModifySamlResponseStepBuilder targetAttributeSamlRequest() { + return targetAttribute(GeneralConstants.SAML_REQUEST_KEY); + } + + public ModifySamlResponseStepBuilder targetAttributeSamlResponse() { + return targetAttribute(GeneralConstants.SAML_RESPONSE_KEY); + } + + public URI targetUri() { + return targetUri; + } + + public ModifySamlResponseStepBuilder targetUri(URI forceUri) { + this.targetUri = forceUri; + return this; + } + + protected HttpUriRequest handleRedirectBinding(CloseableHttpResponse currentResponse) throws Exception, IOException, URISyntaxException { + NameValuePair samlParam = null; + + assertThat(currentResponse, statusCodeIsHC(Status.FOUND)); + String location = currentResponse.getFirstHeader("Location").getValue(); + URI locationUri = URI.create(location); + + List params = URLEncodedUtils.parse(locationUri, "UTF-8"); + for (Iterator it = params.iterator(); it.hasNext();) { + NameValuePair param = it.next(); + if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) { + assertThat("Only one SAMLRequest/SAMLResponse check", samlParam, nullValue()); + samlParam = param; + it.remove(); + } + } + + assertThat(samlParam, notNullValue()); + + String base64EncodedSamlDoc = samlParam.getValue(); + InputStream decoded = RedirectBindingUtil.base64DeflateDecode(base64EncodedSamlDoc); + String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET); + IOUtils.closeQuietly(decoded); + + String transformed = getTransformer().transform(samlDoc); + if (transformed == null) { + return null; + } + + final String attrName = this.targetAttribute != null ? this.targetAttribute : samlParam.getName(); + + return createRequest(locationUri, attrName, transformed, params); + } + + private HttpUriRequest handlePostBinding(CloseableHttpResponse currentResponse) throws Exception { + assertThat(currentResponse, statusCodeIsHC(Status.OK)); + + org.jsoup.nodes.Document theResponsePage = Jsoup.parse(EntityUtils.toString(currentResponse.getEntity())); + Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]"); + Elements samlRequests = theResponsePage.select("input[name=SAMLRequest]"); + Elements forms = theResponsePage.select("form"); + Elements relayStates = theResponsePage.select("input[name=RelayState]"); + int size = samlResponses.size() + samlRequests.size(); + assertThat("Checking uniqueness of SAMLResponse/SAMLRequest input field in the page", size, is(1)); + assertThat("Checking uniqueness of forms in the page", forms, hasSize(1)); + + Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first(); + Element form = forms.first(); + + String base64EncodedSamlDoc = respElement.val(); + InputStream decoded = PostBindingUtil.base64DecodeAsStream(base64EncodedSamlDoc); + String samlDoc = IOUtils.toString(decoded, GeneralConstants.SAML_CHARSET); + IOUtils.closeQuietly(decoded); + + String transformed = getTransformer().transform(samlDoc); + if (transformed == null) { + return null; + } + + final String attributeName = this.targetAttribute != null + ? this.targetAttribute + : respElement.attr("name"); + List parameters = new LinkedList<>(); + + if (! relayStates.isEmpty()) { + parameters.add(new BasicNameValuePair(GeneralConstants.RELAY_STATE, relayStates.first().val())); + } + URI locationUri = this.targetUri != null + ? this.targetUri + : URI.create(form.attr("action")); + + return createRequest(locationUri, attributeName, transformed, parameters); + } + + protected HttpUriRequest createRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException, URISyntaxException { + switch (this.targetBinding) { + case POST: + return createPostRequest(locationUri, attributeName, transformed, parameters); + case REDIRECT: + return createRedirectRequest(locationUri, attributeName, transformed, parameters); + } + throw new RuntimeException("Unknown target binding for " + ModifySamlResponseStepBuilder.class.getName()); + } + + protected HttpUriRequest createRedirectRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException, URISyntaxException { + final byte[] responseBytes = transformed.getBytes(GeneralConstants.SAML_CHARSET); + parameters.add(new BasicNameValuePair(attributeName, RedirectBindingUtil.deflateBase64Encode(responseBytes))); + + if (this.targetUri != null) { + locationUri = this.targetUri; + } + + URI target = new URIBuilder(locationUri).setParameters(parameters).build(); + + return new HttpGet(target); + } + + protected HttpUriRequest createPostRequest(URI locationUri, String attributeName, String transformed, List parameters) throws IOException { + HttpPost post = new HttpPost(locationUri); + + parameters.add(new BasicNameValuePair(attributeName, PostBindingUtil.base64Encode(transformed))); + + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, GeneralConstants.SAML_CHARSET); + post.setEntity(formEntity); + + return post; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java new file mode 100644 index 0000000000..ee24b0670d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/RequiredConsentBuilder.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClient.Step; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; + +/** + * + * @author hmlnarik + */ +public class RequiredConsentBuilder implements Step { + + private final SamlClientBuilder clientBuilder; + private boolean approveConsent = true; + + public RequiredConsentBuilder(SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public HttpUriRequest perform(CloseableHttpClient client, URI currentURI, CloseableHttpResponse currentResponse, HttpClientContext context) throws Exception { + assertThat(currentResponse, statusCodeIsHC(Response.Status.OK)); + String consentPageText = EntityUtils.toString(currentResponse.getEntity(), "UTF-8"); + assertThat(consentPageText, containsString("consent")); + + return handleConsentPage(consentPageText, currentURI); + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } + + public RequiredConsentBuilder approveConsent(boolean shouldApproveConsent) { + this.approveConsent = shouldApproveConsent; + return this; + } + + /** + * Prepares a GET/POST request for consent granting . The consent page is expected + * to have at least input fields with id "kc-login" and "kc-cancel". + * + * @param consentPage + * @param consent + * @return + */ + public HttpUriRequest handleConsentPage(String consentPage, URI currentURI) { + org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage); + + List parameters = new LinkedList<>(); + for (Element form : theLoginPage.getElementsByTag("form")) { + String method = form.attr("method"); + String action = form.attr("action"); + boolean isPost = method != null && "post".equalsIgnoreCase(method); + + for (Element input : form.getElementsByTag("input")) { + if (Objects.equals(input.id(), "kc-login")) { + if (approveConsent) + parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); + } else if (Objects.equals(input.id(), "kc-cancel")) { + if (!approveConsent) + parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); + } else { + parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); + } + } + + if (isPost) { + HttpPost res = new HttpPost(currentURI.resolve(action)); + + UrlEncodedFormEntity formEntity; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + res.setEntity(formEntity); + + return res; + } else { + UriBuilder b = UriBuilder.fromPath(action); + for (NameValuePair parameter : parameters) { + b.queryParam(parameter.getName(), parameter.getValue()); + } + return new HttpGet(b.build()); + } + } + + throw new IllegalArgumentException("Invalid consent page: " + consentPage); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java new file mode 100644 index 0000000000..8b8fde083c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/saml/SamlDocumentStepBuilder.java @@ -0,0 +1,147 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.util.saml; + +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.dom.saml.v2.SAML2Object; +import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType; +import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType; +import org.keycloak.dom.saml.v2.protocol.AttributeQueryType; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.common.util.StaxUtil; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter; +import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter; +import org.keycloak.testsuite.util.SamlClient.Step; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import javax.xml.stream.XMLStreamWriter; +import org.junit.Assert; +import org.w3c.dom.Document; + +/** + * + * @author hmlnarik + */ +public abstract class SamlDocumentStepBuilder> implements Step { + + @FunctionalInterface + public interface Saml2ObjectTransformer { + public T transform(T original) throws Exception; + } + + @FunctionalInterface + public interface Saml2DocumentTransformer { + public Document transform(Document original) throws Exception; + } + + @FunctionalInterface + public interface StringTransformer { + public String transform(String original) throws Exception; + } + + private final SamlClientBuilder clientBuilder; + + private StringTransformer transformer = t -> t; + + public SamlDocumentStepBuilder(SamlClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @SuppressWarnings("unchecked") + public This transformObject(Saml2ObjectTransformer tr) { + final StringTransformer original = this.transformer; + this.transformer = s -> { + final String originalTransformed = original.transform(s); + + if (originalTransformed == null) { + return null; + } + + final ByteArrayInputStream baos = new ByteArrayInputStream(originalTransformed.getBytes()); + final T saml2Object = (T) new SAMLParser().parse(baos); + final T transformed = tr.transform(saml2Object); + + if (transformed == null) { + return null; + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos); + + if (saml2Object instanceof AuthnRequestType) { + new SAMLRequestWriter(xmlStreamWriter).write((AuthnRequestType) saml2Object); + } else if (saml2Object instanceof LogoutRequestType) { + new SAMLRequestWriter(xmlStreamWriter).write((LogoutRequestType) saml2Object); + } else if (saml2Object instanceof ArtifactResolveType) { + new SAMLRequestWriter(xmlStreamWriter).write((ArtifactResolveType) saml2Object); + } else if (saml2Object instanceof AttributeQueryType) { + new SAMLRequestWriter(xmlStreamWriter).write((AttributeQueryType) saml2Object); + } else if (saml2Object instanceof ResponseType) { + new SAMLResponseWriter(xmlStreamWriter).write((ResponseType) saml2Object); + } else if (saml2Object instanceof ArtifactResponseType) { + new SAMLResponseWriter(xmlStreamWriter).write((ArtifactResponseType) saml2Object); + } else { + Assert.assertNotNull("Unknown type: ", saml2Object); + Assert.fail("Unknown type: " + saml2Object.getClass().getName()); + } + return new String(bos.toByteArray(), GeneralConstants.SAML_CHARSET); + }; + return (This) this; + } + + public This transformDocument(Saml2DocumentTransformer tr) { + final StringTransformer original = this.transformer; + this.transformer = s -> { + final String originalTransformed = original.transform(s); + + if (originalTransformed == null) { + return null; + } + + final Document transformed = tr.transform(DocumentUtil.getDocument(originalTransformed)); + return transformed == null ? null : DocumentUtil.getDocumentAsString(transformed); + }; + return (This) this; + } + + public This transformString(StringTransformer tr) { + final StringTransformer original = this.transformer; + this.transformer = s -> { + final String originalTransformed = original.transform(s); + + if (originalTransformed == null) { + return null; + } + + return tr.transform(originalTransformed); + }; + return (This) this; + } + + public SamlClientBuilder build() { + return this.clientBuilder; + } + + public StringTransformer getTransformer() { + return transformer; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java index 26c398ca7e..77f6281056 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractSAMLServletsAdapterTest.java @@ -54,7 +54,6 @@ import org.keycloak.saml.BaseSAML2BindingBuilder; import org.keycloak.saml.SAML2ErrorResponseBuilder; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; @@ -66,6 +65,7 @@ import org.keycloak.testsuite.page.AbstractPage; import org.keycloak.testsuite.util.*; import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; import org.openqa.selenium.By; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -107,8 +107,6 @@ import static org.keycloak.testsuite.util.IOUtil.loadXML; import static org.keycloak.testsuite.util.IOUtil.modifyDocElementAttribute; import static org.keycloak.testsuite.util.Matchers.bodyHC; import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; -import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLogin; -import static org.keycloak.testsuite.util.SamlClient.login; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import static org.keycloak.testsuite.util.WaitUtils.*; @@ -471,10 +469,9 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Test public void employeeAcsTest() { - SAMLDocumentHolder samlResponse = new SamlClient(employeeAcsServletPage.buildUri()).getSamlResponse(Binding.POST, (client, context, strategy) -> { - strategy.setRedirectable(false); - return client.execute(new HttpGet(employeeAcsServletPage.buildUri()), context); - }); + SAMLDocumentHolder samlResponse = new SamlClientBuilder() + .navigateTo(employeeAcsServletPage.buildUri()) + .getSamlResponse(Binding.POST); assertThat(samlResponse.getSamlObject(), instanceOf(AuthnRequestType.class)); assertThat(((AuthnRequestType) samlResponse.getSamlObject()).getAssertionConsumerServiceURL(), notNullValue()); @@ -1029,58 +1026,44 @@ public abstract class AbstractSAMLServletsAdapterTest extends AbstractServletsAd @Test //KEYCLOAK-4020 public void testBooleanAttribute() throws Exception { - AuthnRequestType req = SamlClient.createLoginRequestDocument("http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), getAuthServerSamlEndpoint(SAMLSERVLETDEMO)); - Document doc = SAML2Request.convert(req); + new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "http://localhost:8081/employee2/", getAppServerSamlEndpoint(employee2ServletPage).toString(), Binding.POST).build() + .login().user(bburkeUser).build() + .processSamlResponse(Binding.POST) + .transformDocument(responseDoc -> { + Element attribute = responseDoc.createElement("saml:Attribute"); + attribute.setAttribute("Name", "boolean-attribute"); + attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"); - SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(SAMLSERVLETDEMO), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST); - Document responseDoc = res.getSamlDocument(); + Element attributeValue = responseDoc.createElement("saml:AttributeValue"); + attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema"); + attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + attributeValue.setAttribute("xsi:type", "xs:boolean"); + attributeValue.setTextContent("true"); - Element attribute = responseDoc.createElement("saml:Attribute"); - attribute.setAttribute("Name", "boolean-attribute"); - attribute.setAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"); + attribute.appendChild(attributeValue); + IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute); - Element attributeValue = responseDoc.createElement("saml:AttributeValue"); - attributeValue.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema"); - attributeValue.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); - attributeValue.setAttribute("xsi:type", "xs:boolean"); - attributeValue.setTextContent("true"); + return responseDoc; + }) + .build() - attribute.appendChild(attributeValue); - IOUtil.appendChildInDocument(responseDoc, "samlp:Response/saml:Assertion/saml:AttributeStatement", attribute); + .navigateTo(employee2ServletPage.toString() + "/getAttributes") - CloseableHttpResponse response = null; - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { - HttpClientContext context = HttpClientContext.create(); - - HttpUriRequest post = SamlClient.Binding.POST.createSamlUnsignedResponse(getAppServerSamlEndpoint(employee2ServletPage), null, responseDoc); - response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.FOUND)); - response.close(); - - HttpGet get = new HttpGet(employee2ServletPage.toString() + "/getAttributes"); - response = client.execute(get); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - assertThat(response, bodyHC(containsString("boolean-attribute: true"))); - } catch (Exception ex) { - throw new RuntimeException(ex); - } finally { - if (response != null) { - EntityUtils.consumeQuietly(response.getEntity()); - try { response.close(); } catch (IOException ex) { } - } - } + .execute(r -> { + assertThat(r, statusCodeIsHC(Response.Status.OK)); + assertThat(r, bodyHC(containsString("boolean-attribute: true"))); + }); } // KEYCLOAK-4329 @Test public void testEmptyKeyInfoElement() { - samlidpInitiatedLoginPage.setAuthRealm(SAMLSERVLETDEMO); - samlidpInitiatedLoginPage.setUrlName("sales-post-sig-email"); - System.out.println(samlidpInitiatedLoginPage.toString()); - URI idpInitiatedLoginPage = URI.create(samlidpInitiatedLoginPage.toString()); - log.debug("Log in using idp initiated login"); - SAMLDocumentHolder documentHolder = idpInitiatedLogin(bburkeUser, idpInitiatedLoginPage, SamlClient.Binding.POST); + SAMLDocumentHolder documentHolder = new SamlClientBuilder() + .idpInitiatedLogin(getAuthServerSamlEndpoint(SAMLSERVLETDEMO), "sales-post-sig-email").build() + .login().user(bburkeUser).build() + .getSamlResponse(Binding.POST); log.debug("Removing KeyInfo from Keycloak response"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java index ffa565117e..484b9cc698 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/AuthnRequestNameIdFormatTest.java @@ -19,42 +19,21 @@ package org.keycloak.testsuite.saml; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.dom.saml.v2.assertion.NameIDType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.protocol.saml.SamlConfigAttributes; -import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; -import org.keycloak.services.resources.RealmsResource; -import org.keycloak.testsuite.AbstractAuthTest; -import org.keycloak.testsuite.util.SamlClient; - -import java.io.IOException; +import org.keycloak.testsuite.util.SamlClientBuilder; import java.net.URI; import java.util.List; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriBuilderException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; import org.hamcrest.Matcher; import org.junit.Test; -import org.w3c.dom.Document; import static org.hamcrest.Matchers.*; import static org.keycloak.testsuite.util.SamlClient.*; import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.util.IOUtil.loadRealm; -import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; /** * @@ -63,12 +42,18 @@ import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; public class AuthnRequestNameIdFormatTest extends AbstractSamlTest { private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher 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); + SAMLDocumentHolder res = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, requestBinding) + .transformObject(so -> { + so.setProtocolBinding(requestBinding.getBindingUri()); + so.setNameIDPolicy(nameIDPolicy); + return so; + }) + .build() - Document samlRequest = SAML2Request.convert(loginRep); - SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, requestBinding, responseBinding); + .login().user(bburkeUser).build() + + .getSamlResponse(responseBinding); assertThat(res.getSamlObject(), notNullValue()); assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java index 78cf93d510..abfd00160f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java @@ -12,6 +12,7 @@ import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect; +import org.keycloak.testsuite.util.SamlClientBuilder; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import org.apache.http.client.methods.CloseableHttpResponse; @@ -25,10 +26,10 @@ import org.w3c.dom.Document; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_NAME; import static org.keycloak.testsuite.util.IOUtil.documentToString; import static org.keycloak.testsuite.util.IOUtil.setDocElementAttributeValue; import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC; -import static org.keycloak.testsuite.util.SamlClient.login; /** * @author mhajas @@ -38,13 +39,15 @@ public class BasicSamlTest extends AbstractSamlTest { // KEYCLOAK-4160 @Test public void testPropertyValueInAssertion() throws ParsingException, ConfigurationException, ProcessingException { - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - - Document doc = SAML2Request.convert(loginRep); - - setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" ); - - SAMLDocumentHolder document = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), doc, null, SamlClient.Binding.POST, SamlClient.Binding.POST); + SAMLDocumentHolder document = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST) + .transformDocument(doc -> { + setDocElementAttributeValue(doc, "samlp:AuthnRequest", "ID", "${java.version}" ); + return doc; + }) + .build() + .login().user(bburkeUser).build() + .getSamlResponse(Binding.POST); assertThat(documentToString(document.getSamlDocument()), not(containsString("InResponseTo=\"" + System.getProperty("java.version") + "\""))); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java index 31cc14dc71..13370acd4d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/ConcurrentAuthnRequestTest.java @@ -22,6 +22,7 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.testsuite.util.SamlClient; +import org.keycloak.testsuite.util.saml.LoginBuilder; import java.io.IOException; import java.net.URI; import java.util.Collection; @@ -90,7 +91,7 @@ public class ConcurrentAuthnRequestTest extends AbstractSamlTest { String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); response.close(); - HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); + HttpUriRequest loginRequest = LoginBuilder.handleLoginPage(user, loginPageText); strategy.setRedirectable(false); response = client.execute(loginRequest, context); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java index cec6e1a6bb..5c9ee3e296 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/IncludeOneTimeUseConditionTest.java @@ -23,24 +23,21 @@ import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.dom.saml.v2.assertion.ConditionAbstractType; import org.keycloak.dom.saml.v2.assertion.ConditionsType; import org.keycloak.dom.saml.v2.assertion.OneTimeUseType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ParsingException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; -import org.keycloak.testsuite.util.SamlClient; -import org.w3c.dom.Document; +import org.keycloak.testsuite.updaters.ClientAttributeUpdater; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.io.Closeable; +import java.io.IOException; import java.util.Collection; import java.util.List; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.util.SamlClient.login; /** * KEYCLOAK-4360 @@ -60,38 +57,38 @@ public class IncludeOneTimeUseConditionTest extends AbstractSamlTest testOneTimeUseConditionIncluded(Boolean.FALSE); } - private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws ProcessingException, ConfigurationException, ParsingException + private void testOneTimeUseConditionIncluded(Boolean oneTimeUseConditionShouldBeIncluded) throws IOException { ClientsResource clients = adminClient.realm(REALM_NAME).clients(); List foundClients = clients.findByClientId(SAML_CLIENT_ID_SALES_POST); assertThat(foundClients, hasSize(1)); ClientResource clientRes = clients.get(foundClients.get(0).getId()); - ClientRepresentation client = clientRes.toRepresentation(); - client.getAttributes().put(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString()); - clientRes.update(client); - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - loginRep.setProtocolBinding(SamlClient.Binding.POST.getBindingUri()); + try (Closeable c = new ClientAttributeUpdater(clientRes) + .setAttribute(SamlConfigAttributes.SAML_ONETIMEUSE_CONDITION, oneTimeUseConditionShouldBeIncluded.toString()) + .update()) { - Document samlRequest = SAML2Request.convert(loginRep); - SAMLDocumentHolder res = login(bburkeUser, getAuthServerSamlEndpoint(REALM_NAME), samlRequest, null, SamlClient.Binding.POST, - SamlClient.Binding.POST); + SAMLDocumentHolder res = new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST).build() + .login().user(bburkeUser).build() + .getSamlResponse(Binding.POST); - assertThat(res.getSamlObject(), notNullValue()); - assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); + assertThat(res.getSamlObject(), notNullValue()); + assertThat(res.getSamlObject(), instanceOf(ResponseType.class)); - ResponseType rt = (ResponseType) res.getSamlObject(); - assertThat(rt.getAssertions(), not(empty())); - final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions(); - assertThat(conditionsType, notNullValue()); - assertThat(conditionsType.getConditions(), not(empty())); + ResponseType rt = (ResponseType) res.getSamlObject(); + assertThat(rt.getAssertions(), not(empty())); + final ConditionsType conditionsType = rt.getAssertions().get(0).getAssertion().getConditions(); + assertThat(conditionsType, notNullValue()); + assertThat(conditionsType.getConditions(), not(empty())); - final List conditions = conditionsType.getConditions(); + final List conditions = conditionsType.getConditions(); - final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType); + final Collection oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType); - final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty(); - assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded)); + final boolean oneTimeUseConditionAdded = !oneTimeUseConditions.isEmpty(); + assertThat(oneTimeUseConditionAdded, is(oneTimeUseConditionShouldBeIncluded)); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java index 7870ebaa18..eb0888b886 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/LogoutTest.java @@ -16,34 +16,27 @@ */ package org.keycloak.testsuite.saml; +import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.NameIDType; -import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.saml.SAML2LogoutRequestBuilder; import org.keycloak.saml.SAML2LogoutResponseBuilder; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ParsingException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.testsuite.util.ClientBuilder; -import org.keycloak.testsuite.util.Matchers; -import org.keycloak.testsuite.util.SamlClient; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilderException; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.protocol.HttpClientContext; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.util.concurrent.atomic.AtomicReference; +import javax.xml.transform.dom.DOMSource; import org.junit.Before; import org.junit.Test; -import org.w3c.dom.Document; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.util.Matchers.*; import static org.keycloak.testsuite.util.SamlClient.Binding.*; @@ -57,7 +50,8 @@ public class LogoutTest extends AbstractSamlTest { private ClientRepresentation salesRep; private ClientRepresentation sales2Rep; - private SamlClient samlClient; + private final AtomicReference nameIdRef = new AtomicReference<>(); + private final AtomicReference sessionIndexRef = new AtomicReference<>(); @Before public void setup() { @@ -71,7 +65,8 @@ public class LogoutTest extends AbstractSamlTest { .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url") .build()); - samlClient = new SamlClient(getAuthServerSamlEndpoint(REALM_NAME)); + nameIdRef.set(null); + sessionIndexRef.set(null); } @Override @@ -79,49 +74,35 @@ public class LogoutTest extends AbstractSamlTest { return true; } - private Document prepareLogoutFromSalesAfterLoggingIntoTwoApps() throws ParsingException, IllegalArgumentException, UriBuilderException, ConfigurationException, ProcessingException { - AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); - Document doc = SAML2Request.convert(loginRep); - SAMLDocumentHolder resp = samlClient.login(bburkeUser, doc, null, POST, POST, false, true); - assertThat(resp.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); - ResponseType loginResp1 = (ResponseType) resp.getSamlObject(); + private SamlClientBuilder prepareLogIntoTwoApps() { + return new SamlClientBuilder() + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST).build() + .login().user(bburkeUser).build() + .processSamlResponse(POST).transformObject(so -> { + assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + ResponseType loginResp1 = (ResponseType) so; + final AssertionType firstAssertion = loginResp1.getAssertions().get(0).getAssertion(); + assertThat(firstAssertion, org.hamcrest.Matchers.notNullValue()); + assertThat(firstAssertion.getSubject().getSubType().getBaseID(), instanceOf(NameIDType.class)); - 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(); + NameIDType nameId = (NameIDType) firstAssertion.getSubject().getSubType().getBaseID(); + AuthnStatementType firstAssertionStatement = (AuthnStatementType) firstAssertion.getStatements().iterator().next(); - 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(); + nameIdRef.set(nameId); + sessionIndexRef.set(firstAssertionStatement.getSessionIndex()); + return null; // Do not follow the redirect to the app from the returned response + }).build() - return new SAML2LogoutRequestBuilder() - .destination(getAuthServerSamlEndpoint(REALM_NAME).toString()) - .issuer(SAML_CLIENT_ID_SALES_POST) - .sessionIndex(firstAssertionStatement.getSessionIndex()) - .userPrincipal(nameId.getValue(), nameId.getFormat().toString()) - .buildDocument(); + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST).build() + .login().sso(true).build() // This is a formal step + .processSamlResponse(POST).transformObject(so -> { + assertThat(so, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + return null; // Do not follow the redirect to the app from the returned response + }).build(); } @Test - public void testLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException { - 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 { + public void testLogoutDifferentBrowser() { // This is in fact the same as admin logging out a session from admin console. // This always succeeds as it is essentially the same as backend logout which // does not report errors to client but only to the server log @@ -130,135 +111,155 @@ public class LogoutTest extends AbstractSamlTest { .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(false) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) + .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .clearCookies() - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, HttpClientContext.create()); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() + + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testFrontchannelLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException { + public void testFrontchannelLogoutInSameBrowser() { 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(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, context); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @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 { + public void testFrontchannelLogoutNoLogoutServiceUrlSetInSameBrowser() { adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE) + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "") .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, HttpClientContext.create()); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() throws ParsingException, ConfigurationException, ProcessingException { + public void testFrontchannelLogoutDifferentBrowser() { adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) - .removeAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE) + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") + .build()); + + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .clearCookies() + + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() + + .getSamlResponse(POST); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + } + + @Test + public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() { + adminClient.realm(REALM_NAME) + .clients().get(salesRep.getId()) + .update(ClientBuilder.edit(salesRep) + .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(); + 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, "") + .build()); - samlClient.execute((client, context, strategy) -> { - HttpUriRequest post = POST.createSamlUnsignedRequest(getAuthServerSamlEndpoint(REALM_NAME), null, logoutDoc); - CloseableHttpResponse response = client.execute(post, HttpClientContext.create()); - assertThat(response, statusCodeIsHC(Response.Status.OK)); - return response; - }); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .clearCookies() + + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() + + .getSamlResponse(REDIRECT); + + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); } @Test - public void testLogoutWithPostBindingUnsetRedirectBindingSet() throws ParsingException, ConfigurationException, ProcessingException { + public void testLogoutWithPostBindingUnsetRedirectBindingSet() { // https://issues.jboss.org/browse/KEYCLOAK-4779 adminClient.realm(REALM_NAME) .clients().get(sales2Rep.getId()) .update(ClientBuilder.edit(sales2Rep) .frontchannelLogout(true) .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "") - .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url") + .attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url-to-sales-2") .build()); - Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps(); + SAMLDocumentHolder samlResponse = prepareLogIntoTwoApps() + .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST) + .nameId(nameIdRef::get) + .sessionIndex(sessionIndexRef::get) + .build() - 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); - }); + .processSamlResponse(REDIRECT) + .transformDocument(doc -> { + // Expect logout request for sales-post2 + SAML2Object so = (SAML2Object) new SAMLParser().parse(new DOMSource(doc)); + assertThat(so, isSamlLogoutRequest("http://url-to-sales-2")); - // 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 + return new SAML2LogoutResponseBuilder() + .destination(getAuthServerSamlEndpoint(REALM_NAME).toString()) + .issuer(SAML_CLIENT_ID_SALES_POST2) + .logoutRequestID(((LogoutRequestType) so).getID()) + .buildDocument(); + }) + .targetAttributeSamlResponse() + .targetUri(getAuthServerSamlEndpoint(REALM_NAME)) + .build() - // 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); - }); + .getSamlResponse(POST); // Expect final successful logout response from auth server signalling final successful logout - assertThat(resp.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + assertThat(samlResponse.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + assertThat(((StatusResponseType) samlResponse.getSamlObject()).getDestination(), is("http://url")); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java index 3fcf0c36d6..bd30eea540 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SamlConsentTest.java @@ -9,16 +9,15 @@ import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.IOUtil; -import org.keycloak.testsuite.util.SamlClient; -import java.net.URI; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; import java.util.List; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.util.IOUtil.loadRealm; -import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLoginWithRequiredConsent; /** * @author mhajas @@ -48,13 +47,17 @@ public class SamlConsentTest extends AbstractSamlTest { .build()); log.debug("Log in using idp initiated login"); - String idpInitiatedLogin = getAuthServerRoot() + "realms/" + REALM_NAME + "/protocol/saml/clients/sales-post-enc"; - SAMLDocumentHolder documentHolder = idpInitiatedLoginWithRequiredConsent(bburkeUser, URI.create(idpInitiatedLogin), SamlClient.Binding.POST, false); + SAMLDocumentHolder documentHolder = new SamlClientBuilder() + .idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post-enc").build() + .login().user(bburkeUser).build() + .consentRequired().approveConsent(false).build() + .getSamlResponse(Binding.POST); - assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString(" parameters = new LinkedList<>(); - - - try { - BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder(); - - if (privateKeyStr != null && publicKeyStr != null) { - PrivateKey privateKey = org.keycloak.testsuite.util.KeyUtils.privateKeyFromString(privateKeyStr); - PublicKey publicKey = org.keycloak.testsuite.util.KeyUtils.publicKeyFromString(publicKeyStr); - binding - .signatureAlgorithm(SignatureAlgorithm.RSA_SHA256) - .signWith(KeyUtils.createKeyId(privateKey), privateKey, publicKey) - .signDocument(); - } - - parameters.add( - new BasicNameValuePair(messageType, - binding - .postBinding(samlRequest) - .encoded()) - ); - } catch (IOException | ConfigurationException | ProcessingException 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 createSamlUnsignedRequest(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()); - } - - @Override - public HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest) { - return null; - } - - @Override - public HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey) { - return null; - } - }; - - public abstract SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException; - - public abstract HttpUriRequest createSamlUnsignedRequest(URI samlEndpoint, String relayState, Document samlRequest); - - public abstract HttpUriRequest createSamlSignedRequest(URI samlEndpoint, String relayState, Document samlRequest, String realmPrivateKey, String realmPublicKey); - - public abstract URI getBindingUri(); - - public abstract HttpUriRequest createSamlUnsignedResponse(URI samlEndpoint, String relayState, Document samlRequest); - } - - 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]"); - 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.isEmpty() ? samlRequests.first() : 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 samlDoc = null; - for (NameValuePair param : params) { - if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) { - assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue()); - samlDoc = param.getValue(); - } - } - - return SAMLRequestParser.parseResponseRedirectBinding(samlDoc); - } - - /** - * 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); - } - - /** - * Prepares a GET/POST request for consent granting . The consent page is expected - * to have at least input fields with id "kc-login" and "kc-cancel". - * - * @param consentPage - * @param consent - * @return - */ - public static HttpUriRequest handleConsentPage(String consentPage, boolean consent) { - org.jsoup.nodes.Document theLoginPage = Jsoup.parse(consentPage); - - 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(), "kc-login")) { - if (consent) - parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); - } else if (Objects.equals(input.id(), "kc-cancel")) { - if (!consent) - parameters.add(new BasicNameValuePair(input.attr("name"), input.attr("value"))); - } else { - parameters.add(new BasicNameValuePair(input.attr("name"), input.val())); - } - } - - if (isPost) { - HttpPost res = new HttpPost(getAuthServerContextRoot() + action); - - UrlEncodedFormEntity formEntity; - try { - formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - res.setEntity(formEntity); - - return res; - } else { - UriBuilder b = UriBuilder.fromPath(action); - for (NameValuePair parameter : parameters) { - b.queryParam(parameter.getName(), parameter.getValue()); - } - return new HttpGet(b.build()); - } - } - - throw new IllegalArgumentException("Invalid consent page: " + consentPage); - } - - /** - * 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); - } - } - - /** - * Send request for login form and then login using user param. This method is designed for clients without required consent - * - * @param user - * @param samlEndpoint - * @param samlRequest - * @param relayState - * @param requestBinding - * @param expectedResponseBinding - * @return - */ - public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint, - Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) { - return new SamlClient(samlEndpoint).login(user, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true); - } - - private final HttpClientContext context = HttpClientContext.create(); - private final URI samlEndpoint; - - public SamlClient(URI samlEndpoint) { - this.samlEndpoint = samlEndpoint; - } - - public HttpClientContext getContext() { - return context; - } - - public URI getSamlEndpoint() { - return samlEndpoint; - } - - /** - * Send request for login form and then login using user param. Check whether client requires consent and handle consent page. - * - * @param user - * @param samlEndpoint - * @param samlRequest - * @param relayState - * @param requestBinding - * @param expectedResponseBinding - * @param consentRequired - * @param consent - * @return - */ - 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); - String loginPageText; - - try (CloseableHttpResponse response = client.execute(post, context)) { - assertThat(response, statusCodeIsHC(Response.Status.OK)); - loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - assertThat(loginPageText, containsString("login")); - } - - HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); - - if (consentRequired) { - // Client requires consent - try (CloseableHttpResponse response = client.execute(loginRequest, context)) { - String consentPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - loginRequest = handleConsentPage(consentPageText, consent); - } - } - - strategy.setRedirectable(false); - 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); - } finally { - if (response != null) { - EntityUtils.consumeQuietly(response.getEntity()); - try { - response.close(); - } catch (IOException ex) { - } - } - } - } - - /** - * Send request for login form and then login using user param for clients which doesn't require consent - * - * @param user - * @param idpInitiatedURI - * @param expectedResponseBinding - * @return - */ - public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) { - return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, false, true); - } - - /** - * Send request for login form and then login using user param. For clients which requires consent - * - * @param user - * @param idpInitiatedURI - * @param expectedResponseBinding - * @param consent - * @return - */ - public static SAMLDocumentHolder idpInitiatedLoginWithRequiredConsent(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean 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 samlEndpoint - * @param expectedResponseBinding - * @param consent - * @return - */ - 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)); - - String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - response.close(); - - assertThat(loginPageText, containsString("login")); - - HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); - - if (consentRequired) { - // Client requires consent - response = client.execute(loginRequest, context); - String consentPageText = EntityUtils.toString(response.getEntity(), "UTF-8"); - loginRequest = handleConsentPage(consentPageText, consent); - } - - strategy.setRedirectable(false); - return client.execute(loginRequest, context); - }); - } - - -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json index e1901298d2..aed0231baf 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json @@ -8,6 +8,7 @@ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], + "passwordPolicy": "hashIterations(1)", "defaultRoles": [ "user" ], "smtpServer": { "from": "auto@keycloak.org", diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml index 486da62ab3..62c011cf01 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly10/pom.xml @@ -24,10 +24,10 @@ org.keycloak.testsuite integration-arquillian-tests-adapters-jboss - 3.2.0.CR1-SNAPSHOT + 3.3.0.CR1-SNAPSHOT - integration-arquillian-tests-adapters-wildfly + integration-arquillian-tests-adapters-wildfly10 Adapter Tests - JBoss - Wildfly 10 diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java index c3e013549a..9fb585e55e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java @@ -91,9 +91,9 @@ public class OIDCBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java index bbbbc479d1..8fca98dc63 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLBrokerUserPropertyTest.java @@ -90,9 +90,9 @@ public class SAMLBrokerUserPropertyTest extends AbstractKeycloakIdentityProvider @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java index 8afc49b692..5e177796df 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java @@ -93,9 +93,9 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java index 8a453a7fe6..a0ee823d9b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java @@ -98,9 +98,9 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractKeycloakI @Override protected void doAssertTokenRetrieval(String pageSource) { try { - SAML2Request saml2Request = new SAML2Request(); - ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + ResponseType responseType = (ResponseType) SAML2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)) + .getSamlObject(); assertNotNull(responseType); assertFalse(responseType.getAssertions().isEmpty());