From f946b5d2693abb49a0a2800392f98c78d6a79d51 Mon Sep 17 00:00:00 2001 From: "Pawel S. Veselov" Date: Thu, 4 Feb 2021 00:31:58 -0800 Subject: [PATCH 1/2] Added logout support --- .../java/mujina/api/IdpConfiguration.java | 1 + .../main/java/mujina/api/IdpController.java | 6 + .../java/mujina/idp/MetadataController.java | 7 ++ .../java/mujina/idp/SAMLMessageHandler.java | 53 ++++++--- .../main/java/mujina/idp/SsoController.java | 84 ++++++++++++-- .../mujina/idp/MetadataControllerTest.java | 7 +- .../java/mujina/idp/UserControllerTest.java | 108 ++++++++++++++++++ 7 files changed, 235 insertions(+), 31 deletions(-) diff --git a/mujina-idp/src/main/java/mujina/api/IdpConfiguration.java b/mujina-idp/src/main/java/mujina/api/IdpConfiguration.java index ed051ce1..f0a7d89f 100755 --- a/mujina-idp/src/main/java/mujina/api/IdpConfiguration.java +++ b/mujina-idp/src/main/java/mujina/api/IdpConfiguration.java @@ -24,6 +24,7 @@ public class IdpConfiguration extends SharedConfiguration { private Map> attributes = new TreeMap<>(); private List users = new ArrayList<>(); private String acsEndpoint; + private String slsEndpoint; private AuthenticationMethod authenticationMethod; private AuthenticationMethod defaultAuthenticationMethod; private final String idpPrivateKey; diff --git a/mujina-idp/src/main/java/mujina/api/IdpController.java b/mujina-idp/src/main/java/mujina/api/IdpController.java index b1c334b7..71ed4347 100644 --- a/mujina-idp/src/main/java/mujina/api/IdpController.java +++ b/mujina-idp/src/main/java/mujina/api/IdpController.java @@ -82,6 +82,12 @@ public void setAcsEndpoint(@RequestBody String acsEndpoint) { configuration().setAcsEndpoint(acsEndpoint); } + @PutMapping("/slsendpoint") + public void setSlsEndpoint(@RequestBody String slsEndpoint) { + LOG.info("Request to set Single Logout Service Endpoint to {}", slsEndpoint); + configuration().setSlsEndpoint(slsEndpoint); + } + private IdpConfiguration configuration() { return IdpConfiguration.class.cast(super.configuration); } diff --git a/mujina-idp/src/main/java/mujina/idp/MetadataController.java b/mujina-idp/src/main/java/mujina/idp/MetadataController.java index 2c15573a..6408471e 100644 --- a/mujina-idp/src/main/java/mujina/idp/MetadataController.java +++ b/mujina-idp/src/main/java/mujina/idp/MetadataController.java @@ -9,6 +9,7 @@ import org.opensaml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml2.metadata.KeyDescriptor; import org.opensaml.saml2.metadata.NameIDFormat; +import org.opensaml.saml2.metadata.SingleLogoutService; import org.opensaml.saml2.metadata.SingleSignOnService; import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.io.MarshallingException; @@ -84,6 +85,12 @@ public String metadata(@Value("${idp.base_url}") String idpBaseUrl) throws Secur idpssoDescriptor.getSingleSignOnServices().add(singleSignOnService); + SingleLogoutService singleLogoutService = buildSAMLObject(SingleLogoutService.class, SingleLogoutService.DEFAULT_ELEMENT_NAME); + singleLogoutService.setLocation(idpBaseUrl + "/SingleLogoutService"); + singleLogoutService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + + idpssoDescriptor.getSingleLogoutServices().add(singleLogoutService); + X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); keyInfoGeneratorFactory.setEmitEntityCertificate(true); KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); diff --git a/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java b/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java index e4d02301..cf4c6413 100644 --- a/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java +++ b/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java @@ -11,11 +11,12 @@ import org.opensaml.common.binding.encoding.SAMLMessageEncoder; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.Assertion; -import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.LogoutResponse; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.Status; import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.StatusResponseType; import org.opensaml.saml2.metadata.Endpoint; import org.opensaml.saml2.metadata.SingleSignOnService; import org.opensaml.saml2.metadata.provider.MetadataProviderException; @@ -86,10 +87,9 @@ public SAMLMessageContext extractSAMLMessageContext(HttpServletRequest request, SAMLObject inboundSAMLMessage = messageContext.getInboundSAMLMessage(); - AuthnRequest authnRequest = (AuthnRequest) inboundSAMLMessage; //lambda is poor with Exceptions for (ValidatorSuite validatorSuite : validatorSuites) { - validatorSuite.validate(authnRequest); + validatorSuite.validate(inboundSAMLMessage); } return messageContext; } @@ -105,27 +105,23 @@ private SAMLMessageDecoder samlMessageDecoder(boolean postRequest) { } @SuppressWarnings("unchecked") - public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse response) throws MarshallingException, SignatureException, MessageEncodingException { - Status status = buildStatus(StatusCode.SUCCESS_URI); + private void sendResponseCommon(StatusResponseType responseObject, SAMLPrincipal principal, String statusCode, HttpServletResponse response) throws MessageEncodingException { + + Status status = buildStatus(statusCode); String entityId = idpConfiguration.getEntityId(); Credential signingCredential = resolveCredential(entityId); - Response authResponse = buildSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME); Issuer issuer = buildIssuer(entityId); - authResponse.setIssuer(issuer); - authResponse.setID(SAMLBuilder.randomSAMLId()); - authResponse.setIssueInstant(new DateTime()); - authResponse.setInResponseTo(principal.getRequestID()); + responseObject.setIssuer(issuer); + responseObject.setID(SAMLBuilder.randomSAMLId()); + responseObject.setIssueInstant(new DateTime()); + responseObject.setInResponseTo(principal.getRequestID()); - Assertion assertion = buildAssertion(principal, status, entityId); - signAssertion(assertion, signingCredential); - - authResponse.getAssertions().add(assertion); - authResponse.setDestination(principal.getAssertionConsumerServiceURL()); + responseObject.setDestination(principal.getAssertionConsumerServiceURL()); - authResponse.setStatus(status); + responseObject.setStatus(status); Endpoint endpoint = buildSAMLObject(Endpoint.class, SingleSignOnService.DEFAULT_ELEMENT_NAME); endpoint.setLocation(principal.getAssertionConsumerServiceURL()); @@ -136,7 +132,7 @@ public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse respo messageContext.setOutboundMessageTransport(outTransport); messageContext.setPeerEntityEndpoint(endpoint); - messageContext.setOutboundSAMLMessage(authResponse); + messageContext.setOutboundSAMLMessage(responseObject); messageContext.setOutboundSAMLMessageSigningCredential(signingCredential); messageContext.setOutboundMessageIssuer(entityId); @@ -146,6 +142,29 @@ public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse respo } + public void sendLogoutResponse(SAMLPrincipal principal, HttpServletResponse response, String statusCode) throws MessageEncodingException { + + LogoutResponse logoutResponse = buildSAMLObject(LogoutResponse.class, LogoutResponse.DEFAULT_ELEMENT_NAME); + sendResponseCommon(logoutResponse, principal, statusCode, response); + + } + + public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse response) throws MarshallingException, SignatureException, MessageEncodingException { + + String entityId = idpConfiguration.getEntityId(); + Credential signingCredential = resolveCredential(entityId); + + Response authResponse = buildSAMLObject(Response.class, Response.DEFAULT_ELEMENT_NAME); + + Assertion assertion = buildAssertion(principal, buildStatus(StatusCode.SUCCESS_URI), entityId); + signAssertion(assertion, signingCredential); + + authResponse.getAssertions().add(assertion); + + sendResponseCommon(authResponse, principal, StatusCode.SUCCESS_URI, response); + + } + private Credential resolveCredential(String entityId) { try { return keyManager.resolveSingle(new CriteriaSet(new EntityIDCriteria(entityId))); diff --git a/mujina-idp/src/main/java/mujina/idp/SsoController.java b/mujina-idp/src/main/java/mujina/idp/SsoController.java index a8c23839..e60136d2 100644 --- a/mujina-idp/src/main/java/mujina/idp/SsoController.java +++ b/mujina-idp/src/main/java/mujina/idp/SsoController.java @@ -5,7 +5,10 @@ import mujina.saml.SAMLPrincipal; import org.opensaml.common.binding.SAMLMessageContext; import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.LogoutRequest; import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.core.RequestAbstractType; +import org.opensaml.saml2.core.StatusCode; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.ws.message.decoder.MessageDecodingException; import org.opensaml.ws.message.encoder.MessageEncodingException; @@ -13,22 +16,25 @@ import org.opensaml.xml.security.SecurityException; import org.opensaml.xml.signature.SignatureException; import org.opensaml.xml.validation.ValidationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; +import javax.servlet.http.HttpSession; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import static java.util.Collections.singletonList; @@ -37,6 +43,8 @@ @Controller public class SsoController { + protected final Logger LOG = LoggerFactory.getLogger(getClass()); + @Autowired private SAMLMessageHandler samlMessageHandler; @@ -45,36 +53,88 @@ public class SsoController { @GetMapping("/SingleSignOnService") public void singleSignOnServiceGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) - throws IOException, MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException, ServletException { + throws MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException { doSSO(request, response, authentication, false); } @PostMapping("/SingleSignOnService") public void singleSignOnServicePost(HttpServletRequest request, HttpServletResponse response, Authentication authentication) - throws IOException, MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException, ServletException { + throws MarshallingException, SignatureException, MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException { doSSO(request, response, authentication, true); } - @SuppressWarnings("unchecked") - private void doSSO(HttpServletRequest request, HttpServletResponse response, Authentication authentication, boolean postRequest) throws ValidationException, SecurityException, MessageDecodingException, MarshallingException, SignatureException, MessageEncodingException, MetadataProviderException, IOException, ServletException { + @GetMapping("/SingleLogoutService") + public void singleLogoutServiceGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException { + doSLO(request, response, authentication, false); + } + + @PostMapping("/SingleLogoutService") + public void singleLogoutServicePost(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws MessageEncodingException, ValidationException, SecurityException, MessageDecodingException, MetadataProviderException { + doSLO(request, response, authentication, true); + } + + private void doSSO(HttpServletRequest request, HttpServletResponse response, Authentication authentication, boolean postRequest) throws ValidationException, SecurityException, MessageDecodingException, MarshallingException, SignatureException, MessageEncodingException, MetadataProviderException { SAMLMessageContext messageContext = samlMessageHandler.extractSAMLMessageContext(request, response, postRequest); AuthnRequest authnRequest = (AuthnRequest) messageContext.getInboundSAMLMessage(); String assertionConsumerServiceURL = idpConfiguration.getAcsEndpoint() != null ? idpConfiguration.getAcsEndpoint() : authnRequest.getAssertionConsumerServiceURL(); + + samlMessageHandler.sendAuthnResponse(makeSAMLPrincipal(authentication, authnRequest, assertionConsumerServiceURL, messageContext.getRelayState()), response); + + } + + private SAMLPrincipal makeSAMLPrincipal(Authentication authentication, RequestAbstractType request, + String assertionConsumerServiceURL, String relayState) { + List attributes = attributes(authentication); - SAMLPrincipal principal = new SAMLPrincipal( + return new SAMLPrincipal( authentication.getName(), attributes.stream() .filter(attr -> "urn:oasis:names:tc:SAML:1.1:nameid-format".equals(attr.getName())) - .findFirst().map(attr -> attr.getValue()).orElse(NameIDType.UNSPECIFIED), + .findFirst().map(SAMLAttribute::getValue).orElse(NameIDType.UNSPECIFIED), attributes, - authnRequest.getIssuer().getValue(), - authnRequest.getID(), + request.getIssuer().getValue(), + request.getID(), assertionConsumerServiceURL, - messageContext.getRelayState()); + relayState); + + } + + private void doSLO(HttpServletRequest request, HttpServletResponse response, Authentication authentication, boolean postRequest) + throws ValidationException, SecurityException, MessageDecodingException, MessageEncodingException, MetadataProviderException { + + SAMLMessageContext messageContext = samlMessageHandler.extractSAMLMessageContext(request, response, postRequest); + LogoutRequest logoutRequest = (LogoutRequest) messageContext.getInboundSAMLMessage(); + + // There is no SLS endpoint specified in the logout request, so the only + // thing we can use is the SLS from the IDP configuration. + String destination = idpConfiguration.getSlsEndpoint(); + + if (!Objects.equals(authentication.getPrincipal(), logoutRequest.getNameID().getValue())) { + + LOG.warn("User "+authentication.getPrincipal()+" sent logout request for "+logoutRequest.getNameID()); + + samlMessageHandler.sendLogoutResponse(makeSAMLPrincipal(authentication, logoutRequest, + destination, messageContext.getRelayState()), + response, StatusCode.NO_AUTHN_CONTEXT_URI); + return; + } + + LOG.warn("Logging out " + authentication.getPrincipal()); + + HttpSession session = request.getSession(false); + SecurityContextHolder.clearContext(); + if (session != null) { + session.invalidate(); + } + + samlMessageHandler.sendLogoutResponse(makeSAMLPrincipal(authentication, logoutRequest, + destination, messageContext.getRelayState()), + response, StatusCode.SUCCESS_URI); - samlMessageHandler.sendAuthnResponse(principal, response); } @SuppressWarnings("unchecked") diff --git a/mujina-idp/src/test/java/mujina/idp/MetadataControllerTest.java b/mujina-idp/src/test/java/mujina/idp/MetadataControllerTest.java index bbfd3111..dbca7d9d 100644 --- a/mujina-idp/src/test/java/mujina/idp/MetadataControllerTest.java +++ b/mujina-idp/src/test/java/mujina/idp/MetadataControllerTest.java @@ -20,13 +20,16 @@ public void metadata() throws Exception { given() .config(newConfig() .xmlConfig(xmlConfig().declareNamespace("md", "urn:oasis:names:tc:SAML:2.0:metadata"))) - .header("Content-Type", "application/xml") .get("/metadata") .then() + .contentType("application/xml") .statusCode(SC_OK) .body( "EntityDescriptor.IDPSSODescriptor.SingleSignOnService.@Location", - equalTo(idpBaseUrl + "/SingleSignOnService")); + equalTo(idpBaseUrl + "/SingleSignOnService")) + .body( + "EntityDescriptor.IDPSSODescriptor.SingleLogoutService.@Location", + equalTo(idpBaseUrl + "/SingleLogoutService")); } } diff --git a/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java b/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java index 9213415c..140a89d0 100644 --- a/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java +++ b/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java @@ -1,10 +1,20 @@ package mujina.idp; import io.restassured.filter.cookie.CookieFilter; +import io.restassured.path.xml.XmlPath; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; import mujina.AbstractIntegrationTest; +import org.junit.Assert; import org.junit.Test; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.xml.util.Base64; import org.springframework.test.context.TestPropertySource; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_MOVED_TEMPORARILY; import static org.apache.http.HttpStatus.SC_OK; @@ -13,6 +23,18 @@ @TestPropertySource(properties = {"idp.expires:" + (Integer.MAX_VALUE / 2 - 1), "idp.clock_skew: " + (Integer.MAX_VALUE / 2 - 1)}) public class UserControllerTest extends AbstractIntegrationTest { + private String slsEndpoint = "http://localhost:9090/saml/SLS"; + + private List params = Arrays.asList( + new String[]{"SigAlg", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"}, + new String[]{"SAMLRequest", "fVJdS8MwFH3fryh5b5to6WrYCsIQBlPUig++XdJ0C+bL3FT28227oUO25Sm599xz7jlkgWC05xu3dX18lV+9xJjsjbbIp86S9MFyB6iQWzASeRS8uX/c8JuMch9cdMJpMkvOnT+e6zSAKENUzl7gWa+WpBO0m7ddCay4o6KklIqKlUXRkuRdBhyGl2TgusSA2Mu1xQg2DjjKipTOU1a9UcbZLaflB0lWg3VlIU5Uuxg9z3PtBOidw8grWtG8UXar5SGsRoZvJSSpJ8XF6JJPMqE+DhsnPlP0i/y0d4J+GoJYr5Lmeby89KBVp2T41UafyT0Yr2UmnMlbaRzLjYzQQoTM7/wZrw8uGIjX0x4rqk27Ccp7i16KUbklNbRG2eO+h+3q2eH574vUsx8="}, + new String[]{"Signature", "Aj/IPPRSTE17Aa6fJpdoglVFCmjCUA4pw4drtlSkmwwKoYqvXLfjCBmhofAxgqmTkF2m2o188GobNOdccJ2FQu0APJalznp41uLZAUbQsyCfY5K53V5w5A7gDsJfVBM0ajgSYtKai+ZgPqE+qr0vWeF2E5HBqxLx3ui8IGT+GBo="} + ); + + private List formParams = Collections.singletonList( + new String[]{"SAMLRequest", "PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpMb2dvdXRSZXF1ZXN0IHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng2ZTZkYjg2OS1hNmQ0LTM3NTUtMGNiYy00ZTEwODQyNTE1NzciIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTA3LTE4VDAxOjEzOjA2WiIgRGVzdGluYXRpb249Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9TaW5nbGVMb2dvdXRTZXJ2aWNlIj4KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vbW9jay1zcDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDZlNmRiODY5LWE2ZDQtMzc1NS0wY2JjLTRlMTA4NDI1MTU3NyI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+WVIxYVp0ZUhuY0swelhoUTFwd2ZQZkJCVTVNPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5ESDJlS3lQQ2prN3VGOTdqNmcyNGRIbXNUN1hTa1Jvb3lYT2ZnMFAxek1ZbVU2emZyWkU4TXFYdjRmSVQrQ3Irc2YzRjd0TjFFZDBVNXhkWjNYZ0RxWXcrekNJRis2aXQyOC9rcG52c3R4anA2V2Y1RFozYUdock5TWVRxbFJWQVhjOXJDbWtlaVQ4UVZOdWtqbmttNTVvaDlyM0tPL2lMQml1Yk1oeUVHRzFCM21nemR1L3RnU2c0MGxGcjBEa2NEMmtrb3VEZFd5eGUzcTFGUUExZ3g5dW5vTG5GYTFOQ2p2b25rZ2YyTnExdVUyaklsSk1rNnJ5ZHRYMkpQbkY4TmxvS0RsdDJlKzM3emRnODI3YkZlTG05cUU2NGdBekp1UW5FUnFIQ291RVNEZzVJelBRVEFiaWVuQzdqMUJNWG5OYmRxbURmUzNkckswSDlxcWQ2OHc9PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURFekNDQWZ1Z0F3SUJBZ0lKQUtvSy9oZUJqY09ZTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ0F4SGpBY0JnTlZCQW9NRlU5eVoyRnVhWHBoZEdsdmJpd2dRMDQ5VDBsRVF6QWVGdzB4TlRFeE1URXhNREV5TVRWYUZ3MHlOVEV4TVRBeE1ERXlNVFZhTUNBeEhqQWNCZ05WQkFvTUZVOXlaMkZ1YVhwaGRHbHZiaXdnUTA0OVQwbEVRekNDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFOQkd3Si9xcFRRTmlTZ1VnbFNFMlV6RWtVb3crd1M4cjY3ZXR4b0VobHpKWmZnSy9rNVRmRzF3SUNEcWFwSEF4RVZnVU0xMGFCSFJjdE5vY0E1d21sSHR4ZGlkaHpSWnJvcUh3cEt5MkJtc0tYNVoyb0syNVJMcHN5dXNCMUtyb2VtZ0EvQ2pVbkk2cklMMXh4Rm4zS3lPRmgxWkJMVVF0S05RZU1TN0hGR2dTREFwK3NYdVRGdWp6MTJMRkR1Z1gwVDBLQjVhMSswbDh5MFBFYTB5R2Exb2k2c2VPTng4NDlaSHhNMFBSdlV1bldrdVRNK2ZvWjBqWnBGYXBYZTAyeVdNcWhjLzJpWU1pZUUvM0d2T2d1SmNoSnQ2UitjdXQ4VkJiNnViS1VJR0s3cG1vcS9UQjZEVlhwdnNIcXNESlhlY2h4Y2ljdTRwZEtWREhTZWM4NTBDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRks3UnFqb29kU1lWWEdUVkVkTGYza0pmbFAvc01COEdBMVVkSXdRWU1CYUFGSzdScWpvb2RTWVZYR1RWRWRMZjNrSmZsUC9zTUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRE5aa3hsRlhoNEY0NW11Q2JuUWQrV21hWGxHdmI5dGtVeUFJeFZMOEFJdThKMThGNDIwdnBuR3BvVUFFK0h5M2V2Qm1wMm5rckZBZ21yMDU1ZkFqcEhlWkZnRFpCQVBDd1lkM1ROTURlU3lNdGEzS2Erb1M3R1JGRGVQa01FbStrSDQvcklUTktVRjFzT3ZXQlRTb3drOVR1ZEVEeUZxZ0dudGNkdS9sL3pSeHZ4MzN5M0xNRzVVU0QweDRYNElLalJyUk4xQmJjS2dpOGRxMTBDM2pkcU5hbmNUdVBvcVQzV1d6UnZWdEIvcTM0QjdGNzQvNkp6Z0VvT0NFSHVmQk1wNFpGdTU0UDB5RUd0V2ZUd1R6dW9ab2JyQ2hWVkJ0NHcvWFphZ3JSdFVDRE53UnBITmJwanhZdWRicUxxcGkxTVFwVjlvaHQvQnBUSFZKRzJpMHJvPTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPgogICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZCI+YWRtaW48L3NhbWw6TmFtZUlEPgo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+Cg=="} + ); + @Test public void user() throws Exception { doUser("/user.html"); @@ -34,7 +56,10 @@ public void indexNotLoggedIn() throws Exception { private void doUser(String path) throws Exception { CookieFilter cookieFilter = login("admin", "secret", SC_MOVED_TEMPORARILY); + doUser(path, cookieFilter); + } + private void doUser(String path, CookieFilter cookieFilter) { given() .filter(cookieFilter) .get(path) @@ -42,4 +67,87 @@ private void doUser(String path) throws Exception { .statusCode(SC_OK) .body(containsString("admin")); } + + @Test + public void logoutGet() throws Exception { + doLogout(false); + } + + @Test + public void logoutPost() throws Exception { + doLogout(true); + } + + private void doLogout(boolean post) throws Exception { + + // we need to set some ACS endpoint for logout to work + given() + .header("content-type", "application/json") + .body(slsEndpoint) + .put("/api/slsendpoint") + .then() + .statusCode(SC_OK); + + CookieFilter cookieFilter = login("admin", "secret", SC_MOVED_TEMPORARILY); + doUser("/user.html", cookieFilter); + + doSLO(post, cookieFilter, true); + doSLO(post, cookieFilter, false); + + } + + private void doSLO(boolean post, CookieFilter cookieFilter, boolean ok) { + + RequestSpecification requestSpecification = given(); + if (post) { + formParams.forEach(param -> requestSpecification.formParam(param[0], param[1])); + } else { + params.forEach(param -> requestSpecification.param(param[0], param[1])); + } + requestSpecification.filter(cookieFilter); + + String path = "/SingleLogoutService"; + Response response = post ? requestSpecification.post(path) : requestSpecification.get(path); + + int needStatus; + + if (post) { + needStatus = ok ? SC_OK : SC_MOVED_TEMPORARILY; + } else { + needStatus = SC_OK; + } + + response + .then() + .statusCode(needStatus); + + if (ok) { + + if (post) { + + String stringResponse = response.getBody().asString(); + XmlPath xmlPath = new XmlPath(XmlPath.CompatibilityMode.HTML, stringResponse); + String samlResponse = xmlPath.get("html.body.form.div.input.findAll{it.@name == 'SAMLResponse'}[0].@value").toString(); + xmlPath = new XmlPath(new String(Base64.decode(samlResponse))); + + Assert.assertEquals(StatusCode.SUCCESS_URI, xmlPath.get("LogoutResponse.Status.StatusCode.@Value")); + Assert.assertEquals(slsEndpoint, xmlPath.get("LogoutResponse.@Destination")); + + } + + } else { + + if (!post) { + + // make sure we get a login form, not a logout form. + String stringResponse = response.getBody().asString(); + XmlPath xmlPath = new XmlPath(XmlPath.CompatibilityMode.HTML, stringResponse); + Assert.assertEquals("Login page", xmlPath.get("html.head.title.text()").toString()); + + } + + } + + } + } From a94ae6cf1199a5731148766e37a6598b36be82bc Mon Sep 17 00:00:00 2001 From: Pawel Veselov Date: Mon, 22 Feb 2021 22:17:55 +0100 Subject: [PATCH 2/2] Use HTTP Redirect binding for logout --- .../java/mujina/idp/SAMLMessageHandler.java | 11 ++- .../java/mujina/idp/UserControllerTest.java | 92 +++++++++++++------ 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java b/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java index cf4c6413..adc0f7bb 100644 --- a/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java +++ b/mujina-idp/src/main/java/mujina/idp/SAMLMessageHandler.java @@ -10,6 +10,7 @@ import org.opensaml.common.binding.decoding.SAMLMessageDecoder; import org.opensaml.common.binding.encoding.SAMLMessageEncoder; import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.binding.encoding.HTTPRedirectDeflateEncoder; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.LogoutResponse; @@ -55,6 +56,7 @@ public class SAMLMessageHandler { private final KeyManager keyManager; private final Collection decoders; private final SAMLMessageEncoder encoder; + private final SAMLMessageEncoder logoutEncoder; private final SecurityPolicyResolver resolver; private final IdpConfiguration idpConfiguration; @@ -73,6 +75,7 @@ public SAMLMessageHandler(KeyManager keyManager, Collection getValidatorSuite("saml2-core-schema-validator"), getValidatorSuite("saml2-core-spec-validator")); this.proxiedSAMLContextProviderLB = new ProxiedSAMLContextProviderLB(new URI(idpBaseUrl)); + logoutEncoder = new HTTPRedirectDeflateEncoder(); } public SAMLMessageContext extractSAMLMessageContext(HttpServletRequest request, HttpServletResponse response, boolean postRequest) throws ValidationException, SecurityException, MessageDecodingException, MetadataProviderException { @@ -105,7 +108,9 @@ private SAMLMessageDecoder samlMessageDecoder(boolean postRequest) { } @SuppressWarnings("unchecked") - private void sendResponseCommon(StatusResponseType responseObject, SAMLPrincipal principal, String statusCode, HttpServletResponse response) throws MessageEncodingException { + private void sendResponseCommon(SAMLMessageEncoder encoder, StatusResponseType responseObject, + SAMLPrincipal principal, String statusCode, + HttpServletResponse response) throws MessageEncodingException { Status status = buildStatus(statusCode); @@ -145,7 +150,7 @@ private void sendResponseCommon(StatusResponseType responseObject, SAMLPrincipal public void sendLogoutResponse(SAMLPrincipal principal, HttpServletResponse response, String statusCode) throws MessageEncodingException { LogoutResponse logoutResponse = buildSAMLObject(LogoutResponse.class, LogoutResponse.DEFAULT_ELEMENT_NAME); - sendResponseCommon(logoutResponse, principal, statusCode, response); + sendResponseCommon(logoutEncoder, logoutResponse, principal, statusCode, response); } @@ -161,7 +166,7 @@ public void sendAuthnResponse(SAMLPrincipal principal, HttpServletResponse respo authResponse.getAssertions().add(assertion); - sendResponseCommon(authResponse, principal, StatusCode.SUCCESS_URI, response); + sendResponseCommon(encoder, authResponse, principal, StatusCode.SUCCESS_URI, response); } diff --git a/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java b/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java index 140a89d0..fa630759 100644 --- a/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java +++ b/mujina-idp/src/test/java/mujina/idp/UserControllerTest.java @@ -5,15 +5,26 @@ import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; import mujina.AbstractIntegrationTest; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; import org.junit.Assert; import org.junit.Test; import org.opensaml.saml2.core.StatusCode; import org.opensaml.xml.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.TestPropertySource; +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.zip.Inflater; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_MOVED_TEMPORARILY; @@ -23,15 +34,20 @@ @TestPropertySource(properties = {"idp.expires:" + (Integer.MAX_VALUE / 2 - 1), "idp.clock_skew: " + (Integer.MAX_VALUE / 2 - 1)}) public class UserControllerTest extends AbstractIntegrationTest { - private String slsEndpoint = "http://localhost:9090/saml/SLS"; + @LocalServerPort + private int testPort; - private List params = Arrays.asList( + protected final Logger LOG = LoggerFactory.getLogger(getClass()); + + private final String slsEndpoint = "http://localhost:9090/saml/SLS"; + + private final List params = Arrays.asList( new String[]{"SigAlg", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"}, new String[]{"SAMLRequest", "fVJdS8MwFH3fryh5b5to6WrYCsIQBlPUig++XdJ0C+bL3FT28227oUO25Sm599xz7jlkgWC05xu3dX18lV+9xJjsjbbIp86S9MFyB6iQWzASeRS8uX/c8JuMch9cdMJpMkvOnT+e6zSAKENUzl7gWa+WpBO0m7ddCay4o6KklIqKlUXRkuRdBhyGl2TgusSA2Mu1xQg2DjjKipTOU1a9UcbZLaflB0lWg3VlIU5Uuxg9z3PtBOidw8grWtG8UXar5SGsRoZvJSSpJ8XF6JJPMqE+DhsnPlP0i/y0d4J+GoJYr5Lmeby89KBVp2T41UafyT0Yr2UmnMlbaRzLjYzQQoTM7/wZrw8uGIjX0x4rqk27Ccp7i16KUbklNbRG2eO+h+3q2eH574vUsx8="}, new String[]{"Signature", "Aj/IPPRSTE17Aa6fJpdoglVFCmjCUA4pw4drtlSkmwwKoYqvXLfjCBmhofAxgqmTkF2m2o188GobNOdccJ2FQu0APJalznp41uLZAUbQsyCfY5K53V5w5A7gDsJfVBM0ajgSYtKai+ZgPqE+qr0vWeF2E5HBqxLx3ui8IGT+GBo="} ); - private List formParams = Collections.singletonList( + private final List formParams = Collections.singletonList( new String[]{"SAMLRequest", "PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpMb2dvdXRSZXF1ZXN0IHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng2ZTZkYjg2OS1hNmQ0LTM3NTUtMGNiYy00ZTEwODQyNTE1NzciIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTA3LTE4VDAxOjEzOjA2WiIgRGVzdGluYXRpb249Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9TaW5nbGVMb2dvdXRTZXJ2aWNlIj4KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vbW9jay1zcDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDZlNmRiODY5LWE2ZDQtMzc1NS0wY2JjLTRlMTA4NDI1MTU3NyI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+WVIxYVp0ZUhuY0swelhoUTFwd2ZQZkJCVTVNPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5ESDJlS3lQQ2prN3VGOTdqNmcyNGRIbXNUN1hTa1Jvb3lYT2ZnMFAxek1ZbVU2emZyWkU4TXFYdjRmSVQrQ3Irc2YzRjd0TjFFZDBVNXhkWjNYZ0RxWXcrekNJRis2aXQyOC9rcG52c3R4anA2V2Y1RFozYUdock5TWVRxbFJWQVhjOXJDbWtlaVQ4UVZOdWtqbmttNTVvaDlyM0tPL2lMQml1Yk1oeUVHRzFCM21nemR1L3RnU2c0MGxGcjBEa2NEMmtrb3VEZFd5eGUzcTFGUUExZ3g5dW5vTG5GYTFOQ2p2b25rZ2YyTnExdVUyaklsSk1rNnJ5ZHRYMkpQbkY4TmxvS0RsdDJlKzM3emRnODI3YkZlTG05cUU2NGdBekp1UW5FUnFIQ291RVNEZzVJelBRVEFiaWVuQzdqMUJNWG5OYmRxbURmUzNkckswSDlxcWQ2OHc9PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURFekNDQWZ1Z0F3SUJBZ0lKQUtvSy9oZUJqY09ZTUEwR0NTcUdTSWIzRFFFQkJRVUFNQ0F4SGpBY0JnTlZCQW9NRlU5eVoyRnVhWHBoZEdsdmJpd2dRMDQ5VDBsRVF6QWVGdzB4TlRFeE1URXhNREV5TVRWYUZ3MHlOVEV4TVRBeE1ERXlNVFZhTUNBeEhqQWNCZ05WQkFvTUZVOXlaMkZ1YVhwaGRHbHZiaXdnUTA0OVQwbEVRekNDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFOQkd3Si9xcFRRTmlTZ1VnbFNFMlV6RWtVb3crd1M4cjY3ZXR4b0VobHpKWmZnSy9rNVRmRzF3SUNEcWFwSEF4RVZnVU0xMGFCSFJjdE5vY0E1d21sSHR4ZGlkaHpSWnJvcUh3cEt5MkJtc0tYNVoyb0syNVJMcHN5dXNCMUtyb2VtZ0EvQ2pVbkk2cklMMXh4Rm4zS3lPRmgxWkJMVVF0S05RZU1TN0hGR2dTREFwK3NYdVRGdWp6MTJMRkR1Z1gwVDBLQjVhMSswbDh5MFBFYTB5R2Exb2k2c2VPTng4NDlaSHhNMFBSdlV1bldrdVRNK2ZvWjBqWnBGYXBYZTAyeVdNcWhjLzJpWU1pZUUvM0d2T2d1SmNoSnQ2UitjdXQ4VkJiNnViS1VJR0s3cG1vcS9UQjZEVlhwdnNIcXNESlhlY2h4Y2ljdTRwZEtWREhTZWM4NTBDQXdFQUFhTlFNRTR3SFFZRFZSME9CQllFRks3UnFqb29kU1lWWEdUVkVkTGYza0pmbFAvc01COEdBMVVkSXdRWU1CYUFGSzdScWpvb2RTWVZYR1RWRWRMZjNrSmZsUC9zTUF3R0ExVWRFd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVGQlFBRGdnRUJBRE5aa3hsRlhoNEY0NW11Q2JuUWQrV21hWGxHdmI5dGtVeUFJeFZMOEFJdThKMThGNDIwdnBuR3BvVUFFK0h5M2V2Qm1wMm5rckZBZ21yMDU1ZkFqcEhlWkZnRFpCQVBDd1lkM1ROTURlU3lNdGEzS2Erb1M3R1JGRGVQa01FbStrSDQvcklUTktVRjFzT3ZXQlRTb3drOVR1ZEVEeUZxZ0dudGNkdS9sL3pSeHZ4MzN5M0xNRzVVU0QweDRYNElLalJyUk4xQmJjS2dpOGRxMTBDM2pkcU5hbmNUdVBvcVQzV1d6UnZWdEIvcTM0QjdGNzQvNkp6Z0VvT0NFSHVmQk1wNFpGdTU0UDB5RUd0V2ZUd1R6dW9ab2JyQ2hWVkJ0NHcvWFphZ3JSdFVDRE53UnBITmJwanhZdWRicUxxcGkxTVFwVjlvaHQvQnBUSFZKRzJpMHJvPTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPgogICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3NwLmV4YW1wbGUuY29tL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZCI+YWRtaW48L3NhbWw6TmFtZUlEPgo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+Cg=="} ); @@ -46,7 +62,7 @@ public void index() throws Exception { } @Test - public void indexNotLoggedIn() throws Exception { + public void indexNotLoggedIn() { given() .get("/") .then() @@ -88,15 +104,18 @@ private void doLogout(boolean post) throws Exception { .then() .statusCode(SC_OK); + // let's login before we logout. CookieFilter cookieFilter = login("admin", "secret", SC_MOVED_TEMPORARILY); doUser("/user.html", cookieFilter); + // test log out doSLO(post, cookieFilter, true); + // test logout endpoint with logged out user doSLO(post, cookieFilter, false); } - private void doSLO(boolean post, CookieFilter cookieFilter, boolean ok) { + private void doSLO(boolean post, CookieFilter cookieFilter, boolean ok) throws Exception { RequestSpecification requestSpecification = given(); if (post) { @@ -104,50 +123,63 @@ private void doSLO(boolean post, CookieFilter cookieFilter, boolean ok) { } else { params.forEach(param -> requestSpecification.param(param[0], param[1])); } - requestSpecification.filter(cookieFilter); + requestSpecification + .filter(cookieFilter) + // we need to not let HTTP client redirect, otherwise it will + // attempt to access fake SP SLS URL which has no service + .when().redirects().follow(false); String path = "/SingleLogoutService"; Response response = post ? requestSpecification.post(path) : requestSpecification.get(path); - int needStatus; - - if (post) { - needStatus = ok ? SC_OK : SC_MOVED_TEMPORARILY; - } else { - needStatus = SC_OK; - } - response .then() - .statusCode(needStatus); - - if (ok) { + .statusCode(SC_MOVED_TEMPORARILY); - if (post) { + LOG.info("Redirect to:"+response.getHeader("location")); - String stringResponse = response.getBody().asString(); - XmlPath xmlPath = new XmlPath(XmlPath.CompatibilityMode.HTML, stringResponse); - String samlResponse = xmlPath.get("html.body.form.div.input.findAll{it.@name == 'SAMLResponse'}[0].@value").toString(); - xmlPath = new XmlPath(new String(Base64.decode(samlResponse))); + URI redirectURI = new URI(response.getHeader("location")); - Assert.assertEquals(StatusCode.SUCCESS_URI, xmlPath.get("LogoutResponse.Status.StatusCode.@Value")); - Assert.assertEquals(slsEndpoint, xmlPath.get("LogoutResponse.@Destination")); + if (ok) { + // we must receive a redirect request to SLS URL with SAMLResponse query parameter + List params = URLEncodedUtils.parse(redirectURI, StandardCharsets.UTF_8); + Map pMap = new HashMap<>(); + for (NameValuePair nvp : params) { + pMap.put(nvp.getName(), nvp.getValue()); } + String samlResponse = pMap.get("SAMLResponse"); + XmlPath xmlPath = new XmlPath(getDeflatedResponse(samlResponse)); + + Assert.assertEquals(StatusCode.SUCCESS_URI, xmlPath.get("LogoutResponse.Status.StatusCode.@Value")); + Assert.assertEquals(slsEndpoint, xmlPath.get("LogoutResponse.@Destination")); } else { - if (!post) { + // we should be redirected to our own login + Assert.assertEquals("http://localhost:"+testPort+"/login", redirectURI.toString()); - // make sure we get a login form, not a logout form. - String stringResponse = response.getBody().asString(); - XmlPath xmlPath = new XmlPath(XmlPath.CompatibilityMode.HTML, stringResponse); - Assert.assertEquals("Login page", xmlPath.get("html.head.title.text()").toString()); + } - } + } + private String getDeflatedResponse(String input) throws Exception { + + byte[] decoded = Base64.decode(input); + + Inflater inflater = new Inflater(true); + inflater.setInput(decoded); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + while (!inflater.finished()) { + byte [] chunk = new byte[1024]; + int bytes = inflater.inflate(chunk); + buf.write(chunk, 0, bytes); } + return new String(buf.toByteArray(), StandardCharsets.UTF_8); + } }