Merge pull request #4067 from hmlnarik/KEYCLOAK-4779
KEYCLOAK-4779 Fix NPE
This commit is contained in:
commit
d081f967ea
10 changed files with 599 additions and 71 deletions
|
@ -267,9 +267,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
|
||||
if (logoutPostUrl == null || logoutPostUrl.trim().isEmpty()) {
|
||||
// if we don't have a redirect uri either, return true and default to the admin url + POST binding
|
||||
if (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty())
|
||||
return true;
|
||||
return false;
|
||||
return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
|
||||
}
|
||||
|
||||
if (samlClient.forcePostBinding()) {
|
||||
|
@ -282,11 +280,8 @@ public class SamlProtocol implements LoginProtocol {
|
|||
if (SAML_POST_BINDING.equals(bindingType))
|
||||
return true;
|
||||
|
||||
if (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty())
|
||||
return true; // we don't have a redirect binding url, so use post binding
|
||||
|
||||
return false; // redirect binding
|
||||
|
||||
// true if we don't have a redirect binding url, so use post binding, false for redirect binding
|
||||
return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
|
||||
}
|
||||
|
||||
protected String getNameIdFormat(SamlClient samlClient, ClientSessionModel clientSession) {
|
||||
|
@ -529,15 +524,20 @@ public class SamlProtocol implements LoginProtocol {
|
|||
if (!(client instanceof ClientModel))
|
||||
return null;
|
||||
try {
|
||||
if (isLogoutPostBindingForClient(clientSession)) {
|
||||
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
|
||||
boolean postBinding = isLogoutPostBindingForClient(clientSession);
|
||||
String bindingUri = getLogoutServiceUrl(uriInfo, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING);
|
||||
if (bindingUri == null) {
|
||||
logger.warnf("Failed to logout client %s, skipping this client. Please configure the logout service url in the admin console for your client applications.", client.getClientId());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (postBinding) {
|
||||
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
|
||||
// This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
|
||||
JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient);
|
||||
return binding.postBinding(logoutBuilder.buildDocument()).request(bindingUri);
|
||||
} else {
|
||||
logger.debug("frontchannel redirect binding");
|
||||
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
|
||||
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(bindingUri, clientSession, client);
|
||||
if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
|
||||
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
|
||||
|
@ -620,7 +620,7 @@ public class SamlProtocol implements LoginProtocol {
|
|||
SamlClient samlClient = new SamlClient(client);
|
||||
String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
|
||||
if (logoutUrl == null) {
|
||||
logger.warnv("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: {1}", client.getClientId());
|
||||
logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s", client.getClientId());
|
||||
return;
|
||||
}
|
||||
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client);
|
||||
|
|
|
@ -26,6 +26,9 @@ public abstract class AbstractSamlTest extends AbstractAuthTest {
|
|||
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_POST2 = "http://localhost:8080/sales-post2/";
|
||||
protected static final String SAML_CLIENT_ID_SALES_POST2 = "http://localhost:8081/sales-post2/";
|
||||
|
||||
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/";
|
||||
protected static final String SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY = "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t";
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.saml;
|
||||
|
||||
import org.keycloak.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.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.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.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.w3c.dom.Document;
|
||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.keycloak.testsuite.util.Matchers.*;
|
||||
import static org.keycloak.testsuite.util.SamlClient.Binding.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class LogoutTest extends AbstractSamlTest {
|
||||
|
||||
private ClientRepresentation salesRep;
|
||||
private ClientRepresentation sales2Rep;
|
||||
|
||||
private SamlClient samlClient;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
|
||||
sales2Rep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST2).get(0);
|
||||
|
||||
adminClient.realm(REALM_NAME)
|
||||
.clients().get(salesRep.getId())
|
||||
.update(ClientBuilder.edit(salesRep)
|
||||
.frontchannelLogout(true)
|
||||
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
|
||||
.build());
|
||||
|
||||
samlClient = new SamlClient(getAuthServerSamlEndpoint(REALM_NAME));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isImportAfterEachMethod() {
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
return new SAML2LogoutRequestBuilder()
|
||||
.destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
|
||||
.issuer(SAML_CLIENT_ID_SALES_POST)
|
||||
.sessionIndex(firstAssertionStatement.getSessionIndex())
|
||||
.userPrincipal(nameId.getValue(), nameId.getFormat().toString())
|
||||
.buildDocument();
|
||||
}
|
||||
|
||||
@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 {
|
||||
// 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
|
||||
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.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;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFrontchannelLogoutInSameBrowser() throws ParsingException, ConfigurationException, ProcessingException {
|
||||
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();
|
||||
|
||||
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 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 {
|
||||
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();
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFrontchannelLogoutWithRedirectUrlDifferentBrowser() 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)
|
||||
.attribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
|
||||
.build());
|
||||
|
||||
Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutWithPostBindingUnsetRedirectBindingSet() throws ParsingException, ConfigurationException, ProcessingException {
|
||||
// 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")
|
||||
.build());
|
||||
|
||||
Document logoutDoc = prepareLogoutFromSalesAfterLoggingIntoTwoApps();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// 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
|
||||
resp = samlClient.getSamlResponse(POST, (client, context, strategy) -> {
|
||||
strategy.setRedirectable(false);
|
||||
HttpUriRequest post = POST.createSamlUnsignedResponse(getAuthServerSamlEndpoint(REALM_NAME), null, logoutRespDoc);
|
||||
return client.execute(post, context);
|
||||
});
|
||||
|
||||
// Expect final successful logout response from auth server signalling final successful logout
|
||||
assertThat(resp.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
}
|
||||
|
||||
}
|
|
@ -91,6 +91,11 @@ public class ClientBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ClientBuilder frontchannelLogout(Boolean frontchannelLogout) {
|
||||
rep.setFrontchannelLogout(frontchannelLogout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientBuilder secret(String secret) {
|
||||
rep.setSecret(secret);
|
||||
return this;
|
||||
|
@ -115,6 +120,15 @@ public class ClientBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ClientBuilder removeAttribute(String name) {
|
||||
Map<String, String> attributes = rep.getAttributes();
|
||||
if (attributes != null) {
|
||||
attributes.remove(name);
|
||||
rep.setAttributes(attributes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientBuilder authenticatorType(String providerId) {
|
||||
rep.setClientAuthenticatorType(providerId);
|
||||
return this;
|
||||
|
|
|
@ -16,12 +16,20 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
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.testsuite.util.matchers.SamlResponseTypeMatcher;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.testsuite.util.matchers.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.hamcrest.Matcher;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
* Additional hamcrest matchers for use in {@link org.junit.Assert#assertThat}.
|
||||
|
@ -109,4 +117,40 @@ public class Matchers {
|
|||
public static <T> Matcher<Response> header(Matcher<Map<String, T>> matcher) {
|
||||
return new ResponseHeaderMatcher(matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches when the SAML status code of a {@link ResponseType} instance is equal to the given code.
|
||||
* @param expectedStatusCode
|
||||
* @return
|
||||
*/
|
||||
public static <T> Matcher<SAML2Object> isSamlResponse(JBossSAMLURIConstants expectedStatus) {
|
||||
return allOf(
|
||||
instanceOf(ResponseType.class),
|
||||
new SamlResponseTypeMatcher(is(URI.create(expectedStatus.get())))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches when the destination of a SAML {@link LogoutRequestType} instance is equal to the given destination.
|
||||
* @param expectedStatusCode
|
||||
* @return
|
||||
*/
|
||||
public static <T> Matcher<SAML2Object> isSamlLogoutRequest(String destination) {
|
||||
return allOf(
|
||||
instanceOf(LogoutRequestType.class),
|
||||
new SamlLogoutRequestTypeMatcher(URI.create(destination))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches when the SAML status of a {@link StatusResponseType} instance is equal to the given code.
|
||||
* @param expectedStatusCode
|
||||
* @return
|
||||
*/
|
||||
public static <T> Matcher<SAML2Object> isSamlStatusResponse(JBossSAMLURIConstants expectedStatus) {
|
||||
return allOf(
|
||||
instanceOf(StatusResponseType.class),
|
||||
new SamlStatusResponseTypeMatcher(is(URI.create(expectedStatus.get())))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,11 +58,11 @@ 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.IOUtil.documentToString;
|
||||
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
|
||||
|
||||
/**
|
||||
|
@ -221,9 +221,11 @@ public class SamlClient {
|
|||
public static SAMLDocumentHolder extractSamlResponseFromForm(String responsePage) {
|
||||
org.jsoup.nodes.Document theResponsePage = Jsoup.parse(responsePage);
|
||||
Elements samlResponses = theResponsePage.select("input[name=SAMLResponse]");
|
||||
assertThat("Checking uniqueness of SAMLResponse input field in the page", samlResponses, hasSize(1));
|
||||
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.first();
|
||||
Element respElement = samlResponses.isEmpty() ? samlRequests.first() : samlResponses.first();
|
||||
|
||||
return SAMLRequestParser.parseResponsePostBinding(respElement.val());
|
||||
}
|
||||
|
@ -237,15 +239,15 @@ public class SamlClient {
|
|||
public static SAMLDocumentHolder extractSamlResponseFromRedirect(String responseUri) {
|
||||
List<NameValuePair> params = URLEncodedUtils.parse(URI.create(responseUri), "UTF-8");
|
||||
|
||||
String samlResponse = null;
|
||||
String samlDoc = null;
|
||||
for (NameValuePair param : params) {
|
||||
if ("SAMLResponse".equals(param.getName())) {
|
||||
assertThat(samlResponse, nullValue());
|
||||
samlResponse = param.getValue();
|
||||
if ("SAMLResponse".equals(param.getName()) || "SAMLRequest".equals(param.getName())) {
|
||||
assertThat("Only one SAMLRequest/SAMLResponse check", samlDoc, nullValue());
|
||||
samlDoc = param.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return SAMLRequestParser.parseResponseRedirectBinding(samlResponse);
|
||||
return SAMLRequestParser.parseResponseRedirectBinding(samlDoc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -386,23 +388,14 @@ public class SamlClient {
|
|||
*/
|
||||
public static SAMLDocumentHolder login(UserRepresentation user, URI samlEndpoint,
|
||||
Document samlRequest, String relayState, Binding requestBinding, Binding expectedResponseBinding) {
|
||||
return login(user, samlEndpoint, samlRequest, relayState, requestBinding, expectedResponseBinding, false, true);
|
||||
return new SamlClient(samlEndpoint).login(user, 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);
|
||||
private final HttpClientContext context = HttpClientContext.create();
|
||||
private final URI samlEndpoint;
|
||||
|
||||
public SamlClient(URI samlEndpoint) {
|
||||
this.samlEndpoint = samlEndpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -418,15 +411,11 @@ public class SamlClient {
|
|||
* @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;
|
||||
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
|
||||
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
|
||||
HttpClientContext context = HttpClientContext.create();
|
||||
|
||||
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);
|
||||
response = client.execute(post, context);
|
||||
CloseableHttpResponse response = client.execute(post, context);
|
||||
|
||||
assertThat(response, statusCodeIsHC(Response.Status.OK));
|
||||
String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
|
||||
|
@ -444,8 +433,90 @@ public class SamlClient {
|
|||
}
|
||||
|
||||
strategy.setRedirectable(false);
|
||||
response = client.execute(loginRequest, context);
|
||||
|
||||
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);
|
||||
|
@ -469,7 +540,7 @@ public class SamlClient {
|
|||
* @return
|
||||
*/
|
||||
public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding) {
|
||||
return idpInitiatedLogin(user, idpInitiatedURI, expectedResponseBinding, false, true);
|
||||
return new SamlClient(idpInitiatedURI).idpInitiatedLogin(user, expectedResponseBinding, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -482,29 +553,24 @@ public class SamlClient {
|
|||
* @return
|
||||
*/
|
||||
public static SAMLDocumentHolder idpInitiatedLoginWithRequiredConsent(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consent) {
|
||||
return idpInitiatedLogin(user, idpInitiatedURI, expectedResponseBinding, true, 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 idpInitiatedURI
|
||||
* @param samlEndpoint
|
||||
* @param expectedResponseBinding
|
||||
* @param consent
|
||||
* @return
|
||||
*/
|
||||
public static SAMLDocumentHolder idpInitiatedLogin(UserRepresentation user, URI idpInitiatedURI, Binding expectedResponseBinding, boolean consentRequired, boolean consent) {
|
||||
CloseableHttpResponse response = null;
|
||||
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
|
||||
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
|
||||
|
||||
HttpGet get = new HttpGet(idpInitiatedURI);
|
||||
response = client.execute(get);
|
||||
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));
|
||||
|
||||
HttpClientContext context = HttpClientContext.create();
|
||||
|
||||
String loginPageText = EntityUtils.toString(response.getEntity(), "UTF-8");
|
||||
response.close();
|
||||
|
||||
|
@ -520,20 +586,8 @@ public class SamlClient {
|
|||
}
|
||||
|
||||
strategy.setRedirectable(false);
|
||||
response = client.execute(loginRequest, context);
|
||||
|
||||
return expectedResponseBinding.extractResponse(response);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
} finally {
|
||||
if (response != null) {
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
try {
|
||||
response.close();
|
||||
} catch (IOException ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return client.execute(loginRequest, context);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.util.matchers;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
|
@ -39,6 +41,17 @@ public class HttpResponseStatusCodeMatcher extends BaseMatcher<HttpResponse> {
|
|||
return (item instanceof HttpResponse) && this.matcher.matches(((HttpResponse) item).getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeMismatch(Object item, Description description) {
|
||||
Description d = description.appendText("was ").appendValue(item)
|
||||
.appendText(" with entity ");
|
||||
try {
|
||||
d.appendText(EntityUtils.toString(((HttpResponse) item).getEntity()));
|
||||
} catch (IOException e) {
|
||||
d.appendText("<Cannot decode entity: " + e.getMessage() + ">");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("response status code matches ").appendDescriptionOf(this.matcher);
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.keycloak.testsuite.util.matchers;
|
||||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
||||
import java.net.URI;
|
||||
import org.hamcrest.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class SamlLogoutRequestTypeMatcher extends BaseMatcher<SAML2Object> {
|
||||
|
||||
private final Matcher<URI> destinationMatcher;
|
||||
|
||||
public SamlLogoutRequestTypeMatcher(URI destination) {
|
||||
this.destinationMatcher = is(destination);
|
||||
}
|
||||
|
||||
public SamlLogoutRequestTypeMatcher(Matcher<URI> destinationMatcher) {
|
||||
this.destinationMatcher = destinationMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Object item) {
|
||||
return destinationMatcher.matches(((LogoutRequestType) item).getDestination());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeMismatch(Object item, Description description) {
|
||||
description.appendText("was ").appendValue(((LogoutRequestType) item).getDestination());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("SAML logout request destination matches ").appendDescriptionOf(this.destinationMatcher);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package org.keycloak.testsuite.util.matchers;
|
||||
|
||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||
import org.keycloak.dom.saml.v2.protocol.ResponseType;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import java.net.URI;
|
||||
import org.hamcrest.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class SamlResponseTypeMatcher extends BaseMatcher<SAML2Object> {
|
||||
|
||||
private final Matcher<URI> statusMatcher;
|
||||
|
||||
public SamlResponseTypeMatcher(JBossSAMLURIConstants expectedStatus) {
|
||||
this.statusMatcher = is(URI.create(expectedStatus.get()));
|
||||
}
|
||||
|
||||
public SamlResponseTypeMatcher(Matcher<URI> statusMatcher) {
|
||||
this.statusMatcher = statusMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Object item) {
|
||||
return statusMatcher.matches(((ResponseType) item).getStatus().getStatusCode().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeMismatch(Object item, Description description) {
|
||||
description.appendText("was ").appendValue(((ResponseType) item).getStatus().getStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("SAML response status code matches ").appendDescriptionOf(this.statusMatcher);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
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.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class SamlStatusResponseTypeMatcher extends BaseMatcher<SAML2Object> {
|
||||
|
||||
private final Matcher<URI> statusMatcher;
|
||||
|
||||
public SamlStatusResponseTypeMatcher(URI statusMatcher) {
|
||||
this.statusMatcher = is(statusMatcher);
|
||||
}
|
||||
|
||||
public SamlStatusResponseTypeMatcher(Matcher<URI> statusMatcher) {
|
||||
this.statusMatcher = statusMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Object item) {
|
||||
return statusMatcher.matches(((StatusResponseType) item).getStatus().getStatusCode().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeMismatch(Object item, Description description) {
|
||||
description.appendText("was ").appendValue(((StatusResponseType) item).getStatus().getStatusCode().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("SAML status response status matches ").appendDescriptionOf(this.statusMatcher);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue