KEYCLOAK-4262 Test for rejected consent

This commit is contained in:
mhajas 2017-02-13 09:22:42 +01:00
parent b31c38a158
commit 119435ac76
8 changed files with 303 additions and 116 deletions

View file

@ -118,12 +118,8 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "8080", System.getProperty("auth.server.http.port")); modifyDocElementAttribute(doc, "SingleLogoutService", "redirectBindingUrl", "8080", System.getProperty("auth.server.http.port"));
} }
try {
archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath); archive.add(new StringAsset(IOUtil.documentToString(doc)), adapterConfigPath);
} catch (TransformerException e) {
log.error("Can't transform document to String");
throw new RuntimeException(e);
}
// For running SAML tests it is necessary to have few dependencies on app-server side. // For running SAML tests it is necessary to have few dependencies on app-server side.
// Few of them are not in adapter zip so we need to add them manually here // Few of them are not in adapter zip so we need to add them manually here
@ -179,20 +175,14 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
String dependency = testClass.getAnnotation(UseServletFilter.class).filterDependency(); String dependency = testClass.getAnnotation(UseServletFilter.class).filterDependency();
((WebArchive) archive).addAsLibraries(KeycloakDependenciesResolver.resolveDependencies((dependency + ":" + System.getProperty("project.version")))); ((WebArchive) archive).addAsLibraries(KeycloakDependenciesResolver.resolveDependencies((dependency + ":" + System.getProperty("project.version"))));
try {
Document jbossXmlDoc = loadXML(archive.get(JBOSS_DEPLOYMENT_XML_PATH).getAsset().openStream()); Document jbossXmlDoc = loadXML(archive.get(JBOSS_DEPLOYMENT_XML_PATH).getAsset().openStream());
removeNodeByAttributeValue(jbossXmlDoc, "dependencies", "module", "name", "org.keycloak.keycloak-saml-core"); removeNodeByAttributeValue(jbossXmlDoc, "dependencies", "module", "name", "org.keycloak.keycloak-saml-core");
removeNodeByAttributeValue(jbossXmlDoc, "dependencies", "module", "name", "org.keycloak.keycloak-adapter-spi"); removeNodeByAttributeValue(jbossXmlDoc, "dependencies", "module", "name", "org.keycloak.keycloak-adapter-spi");
archive.add(new StringAsset((documentToString(jbossXmlDoc))), JBOSS_DEPLOYMENT_XML_PATH); archive.add(new StringAsset((documentToString(jbossXmlDoc))), JBOSS_DEPLOYMENT_XML_PATH);
} catch (TransformerException e) {
log.error("Can't transform document to String");
throw new RuntimeException(e);
}
} }
protected void modifyWebXml(Archive<?> archive, TestClass testClass) { protected void modifyWebXml(Archive<?> archive, TestClass testClass) {
try {
Document webXmlDoc = loadXML( Document webXmlDoc = loadXML(
archive.get(WEBXML_PATH).getAsset().openStream()); archive.get(WEBXML_PATH).getAsset().openStream());
if (isTomcatAppServer(testClass.getJavaClass())) { if (isTomcatAppServer(testClass.getJavaClass())) {
@ -247,10 +237,6 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH); archive.add(new StringAsset((documentToString(webXmlDoc))), WEBXML_PATH);
} catch (TransformerException e) {
log.error("Can't transform document to String");
throw new RuntimeException(e);
}
} }
} }

View file

@ -29,6 +29,7 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer; import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
@ -89,13 +90,18 @@ public class IOUtil {
} }
} }
public static String documentToString(Document newDoc) throws TransformerException { public static String documentToString(Document newDoc) {
try {
DOMSource domSource = new DOMSource(newDoc); DOMSource domSource = new DOMSource(newDoc);
Transformer transformer = TransformerFactory.newInstance().newTransformer(); Transformer transformer = TransformerFactory.newInstance().newTransformer();
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
StreamResult sr = new StreamResult(sw); StreamResult sr = new StreamResult(sw);
transformer.transform(domSource, sr); transformer.transform(domSource, sr);
return sw.toString(); return sw.toString();
} catch (TransformerException e) {
log.error("Can't transform document to String");
throw new RuntimeException(e);
}
} }
public static void modifyDocElementAttribute(Document doc, String tagName, String attributeName, String regex, String replacement) { public static void modifyDocElementAttribute(Document doc, String tagName, String attributeName, String regex, String replacement) {

View file

@ -0,0 +1,45 @@
package org.keycloak.testsuite.saml;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.AbstractAuthTest;
import org.keycloak.testsuite.adapter.page.SAMLServlet;
import org.keycloak.testsuite.util.SamlClient;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriBuilderException;
import java.net.URI;
import java.util.List;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
/**
* @author mhajas
*/
public class AbstractSamlTest extends AbstractAuthTest {
protected static final String REALM_NAME = "demo";
protected static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
protected static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
protected static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC = "http://localhost:8080/sales-post-enc/";
protected static final String SAML_CLIENT_ID_SALES_POST_ENC = "http://localhost:8081/sales-post-enc/";
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
}
protected AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, String realmName) {
return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
}
protected URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
return RealmsResource
.protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
.build(realm, SamlProtocol.LOGIN_PROTOCOL);
}
}

View file

@ -60,27 +60,7 @@ import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
* *
* @author hmlnarik * @author hmlnarik
*/ */
public class AuthnRequestNameIdFormatTest extends AbstractAuthTest { public class AuthnRequestNameIdFormatTest extends AbstractSamlTest {
private static final String REALM_NAME = "demo";
private static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
private static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
}
public AuthnRequestType createLoginRequestDocument(String issuer, String assertionConsumerURL, String realmName) {
return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
}
private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
return RealmsResource
.protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
.build(realm, SamlProtocol.LOGIN_PROTOCOL);
}
private void testLoginWithNameIdPolicy(Binding requestBinding, Binding responseBinding, NameIDPolicyType nameIDPolicy, Matcher<String> nameIdMatcher) throws Exception { 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); AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);

View file

@ -53,12 +53,7 @@ import static org.keycloak.testsuite.util.IOUtil.loadRealm;
* @author hmlnarik * @author hmlnarik
*/ */
@Ignore @Ignore
public class ConcurrentAuthnRequestTest extends AbstractAuthTest { public class ConcurrentAuthnRequestTest extends AbstractSamlTest {
private static final String REALM_NAME = "demo";
private static final String SAML_ASSERTION_CONSUMER_URL_SALES_POST = "http://localhost:8080/sales-post/";
private static final String SAML_CLIENT_ID_SALES_POST = "http://localhost:8081/sales-post/";
public static final int ITERATIONS = 10000; public static final int ITERATIONS = 10000;
public static final int CONCURRENT_THREADS = 5; public static final int CONCURRENT_THREADS = 5;
@ -123,12 +118,6 @@ public class ConcurrentAuthnRequestTest extends AbstractAuthTest {
return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName)); return SamlClient.createLoginRequestDocument(issuer, assertionConsumerURL, getAuthServerSamlEndpoint(realmName));
} }
private URI getAuthServerSamlEndpoint(String realm) throws IllegalArgumentException, UriBuilderException {
return RealmsResource
.protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
.build(realm, SamlProtocol.LOGIN_PROTOCOL);
}
private void testLogin(Binding requestBinding) throws Exception { private void testLogin(Binding requestBinding) throws Exception {
AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME); AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
Document samlRequest = SAML2Request.convert(loginRep); Document samlRequest = SAML2Request.convert(loginRep);

View file

@ -0,0 +1,55 @@
package org.keycloak.testsuite.saml;
import org.junit.Test;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
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.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 java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
import static org.keycloak.testsuite.util.SamlClient.idpInitiatedLoginWithRequiredConsent;
/**
* @author mhajas
*/
public class SamlConsentTest extends AbstractSamlTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(loadRealm("/adapter-test/keycloak-saml/testsaml.json"));
}
@Test
public void signingOfRejectedConsentAssertionTest() throws ParsingException, ConfigurationException, ProcessingException {
ClientRepresentation client = adminClient.realm(REALM_NAME)
.clients()
.findByClientId(SAML_CLIENT_ID_SALES_POST_ENC)
.get(0);
adminClient.realm(REALM_NAME)
.clients()
.get(client.getId())
.update(ClientBuilder.edit(client)
.consentRequired(true)
.attribute("saml.encrypt", "false") //remove after RHSSO-797
.attribute("saml_idp_initiated_sso_url_name", "sales-post-enc")
.attribute("saml_assertion_consumer_url_post", SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC + "saml")
.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);
assertThat(IOUtil.documentToString(documentHolder.getSamlDocument()), containsString("<dsig:Signature"));
}
}

View file

@ -106,7 +106,10 @@ public class ClientBuilder {
} }
public ClientBuilder attribute(String name, String value) { public ClientBuilder attribute(String name, String value) {
Map<String, String> attributes = new HashMap<String, String>(); Map<String, String> attributes = rep.getAttributes();
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(name, value); attributes.put(name, value);
rep.setAttributes(attributes); rep.setAttributes(attributes);
return this; return this;

View file

@ -58,6 +58,7 @@ import org.w3c.dom.Document;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.admin.Users.getPasswordOf; import static org.keycloak.testsuite.admin.Users.getPasswordOf;
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot;
import static org.keycloak.testsuite.util.Matchers.*; import static org.keycloak.testsuite.util.Matchers.*;
/** /**
@ -288,6 +289,58 @@ public class SamlClient {
throw new IllegalArgumentException("Invalid login form: " + loginPage); 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 &lt;AuthnRequest&gt; description for more details. * Creates a SAML login request document with the given parameters. See SAML &lt;AuthnRequest&gt; description for more details.
* @param issuer * @param issuer
@ -307,7 +360,7 @@ public class SamlClient {
} }
/** /**
* Send request for login form and then login using user param * Send request for login form and then login using user param. This method is designed for clients without required consent
* @param user * @param user
* @param samlEndpoint * @param samlEndpoint
* @param samlRequest * @param samlRequest
@ -318,6 +371,38 @@ public class SamlClient {
*/ */
public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint, public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) { Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
return login(user, samlEndpoint, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true);
}
/**
* Send request for login form and then login using user param. This method is designed for clients which requires consent
* @param user
* @param samlEndpoint
* @param samlRequest
* @param relayState
* @param requestBinding
* @param expectedResponseBinding
* @return
*/
public static SAMLDocumentHolder loginWithRequiredConsent(UserRepresentation user, URI samlEndpoint,
Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consent) {
return login(user, samlEndpoint, samlRequest, relayState, requestBinding, expectedResponseBinding, true, consent);
}
/**
* 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 static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
CloseableHttpResponse response = null; CloseableHttpResponse response = null;
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect(); SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) { try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
@ -334,6 +419,13 @@ public class SamlClient {
HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); 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); strategy.setRedirectable(false);
response = client.execute(loginRequest, context); response = client.execute(loginRequest, context);
@ -349,13 +441,37 @@ public class SamlClient {
} }
/** /**
* Send request for login form and then login using user param * Send request for login form and then login using user param for clients which doesn't require consent
* @param user * @param user
* @param idpInitiatedURI * @param idpInitiatedURI
* @param expectedResponseBinding * @param expectedResponseBinding
* @return * @return
*/ */
public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) { public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) {
return idpInitiatedLogin(user, idpInitiatedURI, 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 idpInitiatedLogin(user, idpInitiatedURI, 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 idpInitiatedURI
* @param expectedResponseBinding
* @param consent
* @return
*/
public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
CloseableHttpResponse response = null; CloseableHttpResponse response = null;
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect(); SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) { try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
@ -373,6 +489,13 @@ public class SamlClient {
HttpUriRequest loginRequest = handleLoginPage(user, loginPageText); 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); strategy.setRedirectable(false);
response = client.execute(loginRequest, context); response = client.execute(loginRequest, context);