KEYCLOAK-4189 Preparation for cross-DC SAML testing
This commit is contained in:
parent
bed3040f5d
commit
c7046b6325
38 changed files with 1682 additions and 896 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -78,6 +78,11 @@
|
|||
<artifactId>junit</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-all</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.subethamail</groupId>
|
||||
<artifactId>subethasmtp</artifactId>
|
||||
|
|
|
@ -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<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
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<NameValuePair> 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<NameValuePair> 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> T executeAndTransform(ResultExtractor<T> resultTransformer, List<Step> 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<URI> 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();
|
||||
}
|
||||
}
|
|
@ -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<Step> steps = new LinkedList<>();
|
||||
|
||||
public SamlClient execute(Consumer<CloseableHttpResponse> resultConsumer) {
|
||||
final SamlClient samlClient = new SamlClient();
|
||||
samlClient.executeAndTransform(r -> {
|
||||
resultConsumer.accept(r);
|
||||
return null;
|
||||
}, steps);
|
||||
return samlClient;
|
||||
}
|
||||
|
||||
public <T> T executeAndTransform(ResultExtractor<T> resultTransformer) {
|
||||
return new SamlClient().executeAndTransform(resultTransformer, steps);
|
||||
}
|
||||
|
||||
public List<Step> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public <T extends Step> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.*;
|
|
@ -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<AuthnRequestType, CreateAuthnRequestStepBuilder> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<LogoutRequestType, CreateLogoutRequestStepBuilder> {
|
||||
|
||||
private final URI authServerSamlUrl;
|
||||
private final String issuer;
|
||||
private final Binding requestBinding;
|
||||
|
||||
private Supplier<String> sessionIndex = () -> null;
|
||||
private Supplier<NameIDType> nameId = () -> null;
|
||||
private Supplier<String> 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<String> 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<String> 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<NameIDType> 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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<NameValuePair> parameters = new LinkedList<>();
|
||||
for (Element form : theLoginPage.getElementsByTag("form")) {
|
||||
String method = form.attr("method");
|
||||
String action = form.attr("action");
|
||||
boolean isPost = method != null && "post".equalsIgnoreCase(method);
|
||||
|
||||
for (Element input : form.getElementsByTag("input")) {
|
||||
if (Objects.equals(input.id(), "username")) {
|
||||
parameters.add(new BasicNameValuePair(input.attr("name"), username));
|
||||
} else if (Objects.equals(input.id(), "password")) {
|
||||
parameters.add(new BasicNameValuePair(input.attr("name"), password));
|
||||
} else {
|
||||
parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
|
||||
}
|
||||
}
|
||||
|
||||
if (isPost) {
|
||||
HttpPost res = new HttpPost(action);
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
res.setEntity(formEntity);
|
||||
|
||||
return res;
|
||||
} else {
|
||||
UriBuilder b = UriBuilder.fromPath(action);
|
||||
for (NameValuePair parameter : parameters) {
|
||||
b.queryParam(parameter.getName(), parameter.getValue());
|
||||
}
|
||||
return new HttpGet(b.build());
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid login form: " + loginPage);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<SAML2Object, ModifySamlResponseStepBuilder> {
|
||||
|
||||
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<NameValuePair> params = URLEncodedUtils.parse(locationUri, "UTF-8");
|
||||
for (Iterator<NameValuePair> 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<NameValuePair> 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<NameValuePair> 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<NameValuePair> 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<NameValuePair> 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;
|
||||
}
|
||||
}
|
|
@ -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<NameValuePair> parameters = new LinkedList<>();
|
||||
for (Element form : theLoginPage.getElementsByTag("form")) {
|
||||
String method = form.attr("method");
|
||||
String action = form.attr("action");
|
||||
boolean isPost = method != null && "post".equalsIgnoreCase(method);
|
||||
|
||||
for (Element input : form.getElementsByTag("input")) {
|
||||
if (Objects.equals(input.id(), "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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<T extends SAML2Object, This extends SamlDocumentStepBuilder<T, This>> implements Step {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Saml2ObjectTransformer<T extends SAML2Object> {
|
||||
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<T> 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: <null>", 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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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<String> nameIdMatcher) throws Exception {
|
||||
AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
|
||||
loginRep.setProtocolBinding(requestBinding.getBindingUri());
|
||||
loginRep.setNameIDPolicy(nameIDPolicy);
|
||||
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));
|
||||
|
|
|
@ -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") + "\"")));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<ClientRepresentation> 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<ConditionAbstractType> conditions = conditionsType.getConditions();
|
||||
final List<ConditionAbstractType> conditions = conditionsType.getConditions();
|
||||
|
||||
final Collection<ConditionAbstractType> oneTimeUseConditions = Collections2.filter(conditions, input -> input instanceof OneTimeUseType);
|
||||
final Collection<ConditionAbstractType> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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<NameIDType> nameIdRef = new AtomicReference<>();
|
||||
private final AtomicReference<String> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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("<dsig:Signature")); // KEYCLOAK-4262
|
||||
assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), not(containsString("<samlp:LogoutResponse"))); // KEYCLOAK-4261
|
||||
assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:Response")); // KEYCLOAK-4261
|
||||
assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:Status")); // KEYCLOAK-4181
|
||||
assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:RequestDenied\"")); // KEYCLOAK-4181
|
||||
final String samlDocumentString = IOUtil.documentToString(documentHolder.getSamlDocument());
|
||||
assertThat(samlDocumentString, containsString("<dsig:Signature")); // KEYCLOAK-4262
|
||||
assertThat(samlDocumentString, not(containsString("<samlp:LogoutResponse"))); // KEYCLOAK-4261
|
||||
assertThat(samlDocumentString, containsString("<samlp:Response")); // KEYCLOAK-4261
|
||||
assertThat(samlDocumentString, containsString("<samlp:Status")); // KEYCLOAK-4181
|
||||
assertThat(samlDocumentString, containsString("<samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:RequestDenied\"")); // KEYCLOAK-4181
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,603 +0,0 @@
|
|||
/*
|
||||
* 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.representations.idm.UserRepresentation;
|
||||
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 javax.ws.rs.core.UriBuilder;
|
||||
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.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.keycloak.testsuite.admin.Users.getPasswordOf;
|
||||
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot;
|
||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||
|
||||
/**
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class SamlClient {
|
||||
|
||||
/**
|
||||
* SAML bindings and related HttpClient methods.
|
||||
*/
|
||||
public enum Binding {
|
||||
POST {
|
||||
@Override
|
||||
public SAMLDocumentHolder extractResponse(CloseableHttpResponse response) throws IOException {
|
||||
assertThat(response, statusCodeIsHC(Response.Status.OK));
|
||||
String responsePage = EntityUtils.toString(response.getEntity(), "UTF-8");
|
||||
response.close();
|
||||
return extractSamlResponseFromForm(responsePage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpPost 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<NameValuePair> 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<NameValuePair> 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<NameValuePair> parameters = new LinkedList<>();
|
||||
for (Element form : theLoginPage.getElementsByTag("form")) {
|
||||
String method = form.attr("method");
|
||||
String action = form.attr("action");
|
||||
boolean isPost = method != null && "post".equalsIgnoreCase(method);
|
||||
|
||||
for (Element input : form.getElementsByTag("input")) {
|
||||
if (Objects.equals(input.id(), "username")) {
|
||||
parameters.add(new BasicNameValuePair(input.attr("name"), username));
|
||||
} else if (Objects.equals(input.id(), "password")) {
|
||||
parameters.add(new BasicNameValuePair(input.attr("name"), password));
|
||||
} else {
|
||||
parameters.add(new BasicNameValuePair(input.attr("name"), input.val()));
|
||||
}
|
||||
}
|
||||
|
||||
if (isPost) {
|
||||
HttpPost res = new HttpPost(action);
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
res.setEntity(formEntity);
|
||||
|
||||
return res;
|
||||
} else {
|
||||
UriBuilder b = UriBuilder.fromPath(action);
|
||||
for (NameValuePair parameter : parameters) {
|
||||
b.queryParam(parameter.getName(), parameter.getValue());
|
||||
}
|
||||
return new HttpGet(b.build());
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid login form: " + loginPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<NameValuePair> parameters = new LinkedList<>();
|
||||
for (Element form : theLoginPage.getElementsByTag("form")) {
|
||||
String method = form.attr("method");
|
||||
String action = form.attr("action");
|
||||
boolean isPost = method != null && "post".equalsIgnoreCase(method);
|
||||
|
||||
for (Element input : form.getElementsByTag("input")) {
|
||||
if (Objects.equals(input.id(), "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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
<parent>
|
||||
<groupId>org.keycloak.testsuite</groupId>
|
||||
<artifactId>integration-arquillian-tests-adapters-jboss</artifactId>
|
||||
<version>3.2.0.CR1-SNAPSHOT</version>
|
||||
<version>3.3.0.CR1-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>integration-arquillian-tests-adapters-wildfly</artifactId>
|
||||
<artifactId>integration-arquillian-tests-adapters-wildfly10</artifactId>
|
||||
|
||||
<name>Adapter Tests - JBoss - Wildfly 10</name>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue