You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2018/07/12 15:59:05 UTC

[cxf-fediz] branch master updated: FEDIZ-221 - Sending back LogoutResponse from the IdP

This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cxf-fediz.git


The following commit(s) were added to refs/heads/master by this push:
     new 579893c  FEDIZ-221 - Sending back LogoutResponse from the IdP
579893c is described below

commit 579893ccfdac7fc6d5d9520706f046bb95b8f31a
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Thu Jul 12 16:58:48 2018 +0100

    FEDIZ-221 - Sending back LogoutResponse from the IdP
---
 .../idp/beans/samlsso/AuthnRequestParser.java      | 59 ++++++++++++++--------
 .../idp/beans/samlsso/SamlResponseCreator.java     | 29 +++++++++++
 .../samlsso/SAML2PResponseComponentBuilder.java    | 24 +++++++++
 .../webapp/WEB-INF/flows/saml-validate-request.xml | 53 +++++++++++--------
 .../WEB-INF/views/samlsignoutresponseform.jsp      | 45 +++++++++++++++++
 .../apache/cxf/fediz/systests/samlsso/IdpTest.java | 16 +++++-
 6 files changed, 183 insertions(+), 43 deletions(-)

diff --git a/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java b/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java
index 852926b..40e53d8 100644
--- a/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java
+++ b/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java
@@ -38,6 +38,7 @@ import org.apache.cxf.fediz.core.util.CertsUtils;
 import org.apache.cxf.fediz.service.idp.IdpConstants;
 import org.apache.cxf.fediz.service.idp.domain.Application;
 import org.apache.cxf.fediz.service.idp.domain.Idp;
+import org.apache.cxf.fediz.service.idp.samlsso.SAMLAbstractRequest;
 import org.apache.cxf.fediz.service.idp.samlsso.SAMLAuthnRequest;
 import org.apache.cxf.fediz.service.idp.samlsso.SAMLLogoutRequest;
 import org.apache.cxf.fediz.service.idp.util.WebUtils;
@@ -113,7 +114,7 @@ public class AuthnRequestParser {
                 LOG.warn("Error parsing request: {}", ex.getMessage());
                 throw new ProcessingException(TYPE.BAD_REQUEST);
             }
-            
+
             // Store various attributes from the AuthnRequest/LogoutRequest
             if (parsedRequest instanceof AuthnRequest) {
                 SAMLAuthnRequest authnRequest = new SAMLAuthnRequest((AuthnRequest)parsedRequest);
@@ -122,7 +123,7 @@ public class AuthnRequestParser {
                 SAMLLogoutRequest logoutRequest = new SAMLLogoutRequest((LogoutRequest)parsedRequest);
                 WebUtils.putAttributeInFlowScope(context, IdpConstants.SAML_LOGOUT_REQUEST, logoutRequest);
             }
-            
+
             validateRequest(parsedRequest);
 
             // Check the signature
@@ -132,7 +133,7 @@ public class AuthnRequestParser {
                     checkDestination(context, parsedRequest);
 
                     // Check signature
-                    X509Certificate validatingCert = 
+                    X509Certificate validatingCert =
                         getValidatingCertificate(idp, parsedRequest.getIssuer().getValue());
                     Crypto issuerCrypto = new CertificateStore(new X509Certificate[] {validatingCert});
                     validateRequestSignature(parsedRequest.getSignature(), issuerCrypto);
@@ -159,16 +160,20 @@ public class AuthnRequestParser {
     }
 
     public String retrieveRealm(RequestContext context) {
-        SAMLAuthnRequest authnRequest =
-            (SAMLAuthnRequest)WebUtils.getAttributeFromFlowScope(context, IdpConstants.SAML_AUTHN_REQUEST);
+        SAMLAbstractRequest request =
+            (SAMLAbstractRequest)WebUtils.getAttributeFromFlowScope(context, IdpConstants.SAML_AUTHN_REQUEST);
+        if (request == null) {
+            request = (SAMLAbstractRequest)WebUtils.getAttributeFromFlowScope(context,
+                                                                              IdpConstants.SAML_LOGOUT_REQUEST);
+        }
 
-        if (authnRequest != null) {
-            String issuer = authnRequest.getIssuer();
-            LOG.debug("Parsed SAML AuthnRequest Issuer: {}", issuer);
+        if (request != null) {
+            String issuer = request.getIssuer();
+            LOG.debug("Parsed SAML Request Issuer: {}", issuer);
             return issuer;
         }
 
-        LOG.debug("No AuthnRequest available to be parsed");
+        LOG.debug("No AuthnRequest or LogoutRequest available to be parsed");
         return null;
     }
 
@@ -190,37 +195,47 @@ public class AuthnRequestParser {
         if (serviceConfig != null) {
             String racs = serviceConfig.getPassiveRequestorEndpoint();
             LOG.debug("Attempting to use the configured passive requestor endpoint instead: {}", racs);
-            return racs;
+            if (racs != null) {
+                return racs;
+            }
         }
 
         return null;
     }
 
     public String retrieveRequestId(RequestContext context) {
-        SAMLAuthnRequest authnRequest =
-            (SAMLAuthnRequest)WebUtils.getAttributeFromFlowScope(context, IdpConstants.SAML_AUTHN_REQUEST);
+        SAMLAbstractRequest request =
+            (SAMLAbstractRequest)WebUtils.getAttributeFromFlowScope(context, IdpConstants.SAML_AUTHN_REQUEST);
+        if (request == null) {
+            request = (SAMLAbstractRequest)WebUtils.getAttributeFromFlowScope(context,
+                                                                              IdpConstants.SAML_LOGOUT_REQUEST);
+        }
 
-        if (authnRequest != null && authnRequest.getRequestId() != null) {
-            String id = authnRequest.getRequestId();
-            LOG.debug("Parsed SAML AuthnRequest Id: {}", id);
+        if (request != null && request.getRequestId() != null) {
+            String id = request.getRequestId();
+            LOG.debug("Parsed SAML Request Id: {}", id);
             return id;
         }
 
-        LOG.debug("No AuthnRequest available to be parsed");
+        LOG.debug("No AuthnRequest/LogoutRequest available to be parsed");
         return null;
     }
 
     public String retrieveRequestIssuer(RequestContext context) {
-        SAMLAuthnRequest authnRequest =
-            (SAMLAuthnRequest)WebUtils.getAttributeFromFlowScope(context, IdpConstants.SAML_AUTHN_REQUEST);
+        SAMLAbstractRequest request =
+            (SAMLAbstractRequest)WebUtils.getAttributeFromFlowScope(context, IdpConstants.SAML_AUTHN_REQUEST);
+        if (request == null) {
+            request = (SAMLAbstractRequest)WebUtils.getAttributeFromFlowScope(context,
+                                                                              IdpConstants.SAML_LOGOUT_REQUEST);
+        }
 
-        if (authnRequest != null && authnRequest.getIssuer() != null) {
-            String issuer = authnRequest.getIssuer();
-            LOG.debug("Parsed SAML AuthnRequest Issuer: {}", issuer);
+        if (request != null && request.getIssuer() != null) {
+            String issuer = request.getIssuer();
+            LOG.debug("Parsed SAML Request Issuer: {}", issuer);
             return issuer;
         }
 
-        LOG.debug("No AuthnRequest available to be parsed");
+        LOG.debug("No AuthnRequest/LogoutRequest available to be parsed");
         return null;
     }
 
diff --git a/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/SamlResponseCreator.java b/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/SamlResponseCreator.java
index 6824202..9ca6b4b 100644
--- a/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/SamlResponseCreator.java
+++ b/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/SamlResponseCreator.java
@@ -50,6 +50,7 @@ import org.apache.wss4j.common.util.DOM2Writer;
 import org.apache.wss4j.dom.WSConstants;
 import org.joda.time.DateTime;
 import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.LogoutResponse;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.Response;
 import org.opensaml.saml.saml2.core.Status;
@@ -96,6 +97,17 @@ public class SamlResponseCreator {
         }
     }
 
+    public String createSAMLLogoutResponse(RequestContext context, Idp idp, String requestId)
+                                         throws ProcessingException {
+        try {
+            Element response = createLogoutResponse(idp, requestId);
+            return encodeResponse(response);
+        } catch (Exception ex) {
+            LOG.warn("Error marshalling SAML Token: {}", ex.getMessage());
+            throw new ProcessingException(TYPE.BAD_REQUEST);
+        }
+    }
+
     private Assertion createSAML2Assertion(RequestContext context, Idp idp, SamlAssertionWrapper receivedToken,
                                            String requestID, String requestIssuer,
                                            String remoteAddr, String racs) throws Exception {
@@ -167,6 +179,23 @@ public class SamlResponseCreator {
         return policyElement;
     }
 
+    protected Element createLogoutResponse(Idp idp, String requestID) throws Exception {
+        Document doc = DOMUtils.newDocument();
+
+        Status status =
+            SAML2PResponseComponentBuilder.createStatus(
+                "urn:oasis:names:tc:SAML:2.0:status:Success", null
+            );
+        String issuer = useRealmForIssuer ? idp.getRealm() : idp.getIdpUrl().toString();
+        LogoutResponse response =
+            SAML2PResponseComponentBuilder.createSAMLLogoutResponse(requestID, issuer, status);
+
+        Element policyElement = OpenSAMLUtil.toDom(response, doc);
+        doc.appendChild(policyElement);
+
+        return policyElement;
+    }
+
     protected String encodeResponse(Element response) throws IOException {
         String responseMessage = DOM2Writer.nodeToString(response);
         LOG.debug("Created Response: {}", responseMessage);
diff --git a/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/SAML2PResponseComponentBuilder.java b/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/SAML2PResponseComponentBuilder.java
index 998df5b..7e48340 100644
--- a/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/SAML2PResponseComponentBuilder.java
+++ b/services/idp-core/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/SAML2PResponseComponentBuilder.java
@@ -27,6 +27,7 @@ import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.saml.common.SAMLObjectBuilder;
 import org.opensaml.saml.common.SAMLVersion;
 import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.LogoutResponse;
 import org.opensaml.saml.saml2.core.Response;
 import org.opensaml.saml.saml2.core.Status;
 import org.opensaml.saml.saml2.core.StatusCode;
@@ -39,6 +40,8 @@ public final class SAML2PResponseComponentBuilder {
 
     private static SAMLObjectBuilder<Response> responseBuilder;
 
+    private static SAMLObjectBuilder<LogoutResponse> logoutResponseBuilder;
+
     private static SAMLObjectBuilder<Issuer> issuerBuilder;
 
     private static SAMLObjectBuilder<Status> statusBuilder;
@@ -76,6 +79,27 @@ public final class SAML2PResponseComponentBuilder {
         return response;
     }
 
+    public static LogoutResponse createSAMLLogoutResponse(
+        String inResponseTo,
+        String issuer,
+        Status status
+    ) {
+        if (logoutResponseBuilder == null) {
+            logoutResponseBuilder = (SAMLObjectBuilder<LogoutResponse>)
+                builderFactory.getBuilder(LogoutResponse.DEFAULT_ELEMENT_NAME);
+        }
+        LogoutResponse response = logoutResponseBuilder.buildObject();
+
+        response.setID(UUID.randomUUID().toString());
+        response.setIssueInstant(new DateTime());
+        response.setInResponseTo(inResponseTo);
+        response.setIssuer(createIssuer(issuer));
+        response.setStatus(status);
+        response.setVersion(SAMLVersion.VERSION_20);
+
+        return response;
+    }
+
     @SuppressWarnings("unchecked")
     public static Issuer createIssuer(
         String issuerValue
diff --git a/services/idp/src/main/webapp/WEB-INF/flows/saml-validate-request.xml b/services/idp/src/main/webapp/WEB-INF/flows/saml-validate-request.xml
index 616786b..3122fcf 100644
--- a/services/idp/src/main/webapp/WEB-INF/flows/saml-validate-request.xml
+++ b/services/idp/src/main/webapp/WEB-INF/flows/saml-validate-request.xml
@@ -89,22 +89,21 @@
         <evaluate expression="authnRequestParser.parseSAMLRequest(flowRequestContext, flowScope.idpConfig,
                                                               flowScope.SAMLRequest, flowScope.SigAlg,
                                                               flowScope.Signature, flowScope.RelayState)" />
-        <transition to="determineRequestType"/>
+        <transition to="retrieveConsumerURL"/>
         <transition on-exception="org.apache.cxf.fediz.core.exception.ProcessingException" to="viewBadRequest" />
     </action-state>
     
-    <decision-state id="determineRequestType">
-        <if test="flowScope.saml_authn_request == null"
-            then="selectSignOutProcess" else="retrieveConsumerURL" />
-    </decision-state>
-    
     <action-state id="retrieveConsumerURL">
         <evaluate expression="authnRequestParser.retrieveConsumerURL(flowRequestContext)" 
                   result="flowScope.consumerURL"/>
-        <transition to="retrieveRealm"/>
+        <transition to="determineRequestType"/>
         <transition on-exception="org.apache.cxf.fediz.core.exception.ProcessingException" to="viewBadRequest" />
     </action-state>
     
+    <decision-state id="determineRequestType">
+        <if test="flowScope.saml_authn_request == null" then="selectSignOutProcess" else="retrieveRealm" />
+    </decision-state>
+    
     <action-state id="retrieveRealm">
         <evaluate expression="authnRequestParser.retrieveRealm(flowRequestContext)" 
                   result="flowScope.realm"/>
@@ -219,33 +218,47 @@
     <decision-state id="selectSignOutProcess">
         <if test="flowScope.idpConfig.rpSingleSignOutConfirmation == true
             or flowScope.idpConfig.rpSingleSignOutCleanupConfirmation == true"
-            then="viewSignoutConfirmation" else="invalidateSessionAction" />
+            then="viewSignoutConfirmation" else="produceSAMLLogoutResponse" />
     </decision-state>
     
-    <!-- normal exit point for logout -->
     <view-state id="viewSignoutConfirmation" view="signoutconfirmationresponse">
-        <transition on="submit" to="invalidateSessionAction"/>
+        <transition on="submit" to="produceSAMLLogoutResponse"/>
         <transition on="cancel" to="redirect" />
     </view-state>
     
-    <!-- normal exit point for logout -->
-    <decision-state id="invalidateSessionAction">
+    <action-state id="produceSAMLLogoutResponse">
+        <on-entry>
+            <evaluate expression="authnRequestParser.retrieveRequestId(flowRequestContext)" 
+                      result="flowScope.requestId"/>
+            <evaluate expression="authnRequestParser.retrieveRequestIssuer(flowRequestContext)" 
+                      result="flowScope.requestIssuer"/>
+        </on-entry>
+        <evaluate expression="samlResponseCreator.createSAMLLogoutResponse(flowRequestContext, flowScope.idpConfig, flowScope.requestId)"
+                  result="flowScope.logoutResponse"/>                                               
+        <transition to="invalidateSessionAction" />
+    </action-state>
+    
+    <action-state id="invalidateSessionAction">
         <on-entry>
             <!-- store the realmConfigMap in the request map before we invalidate the session below.
             Its needed in the signoutresponse.jsp page -->
             <set name="externalContext.requestMap.realmConfigMap" 
                 value="externalContext.sessionMap.realmConfigMap"/>
-            <set name="externalContext.requestMap.wreply" value="flowScope.wreply"/>
             <evaluate expression="homeRealmReminder.removeCookie(flowRequestContext)" />
-            <evaluate expression="logoutAction.submit(flowRequestContext)" />
         </on-entry>
-        <if test="flowScope.idpConfig.isAutomaticRedirectToRpAfterLogout()"
-            then="redirectToRPLogoutPage" else="showLogoutResponsePage" />
-    </decision-state>
-    
-    <end-state id="showLogoutResponsePage" view="signoutresponse" />
+        <evaluate expression="logoutAction.submit(flowRequestContext)" />
+        <transition to="signOutFormResponseView" />
+    </action-state>
     
-    <end-state id="redirectToRPLogoutPage" view="externalRedirect:#{flowScope.wreply}" />
+    <!-- normal exit point for logout -->
+    <!-- browser redirection (self-submitted form 'samlsignoutresponseform.jsp') -->
+    <end-state id="signOutFormResponseView" view="samlsignoutresponseform">
+        <on-entry>
+            <evaluate expression="flowScope.consumerURL" result="requestScope.samlAction" />
+            <evaluate expression="flowScope.RelayState" result="requestScope.relayState" />
+            <evaluate expression="flowScope.logoutResponse" result="requestScope.samlResponse" />
+        </on-entry>
+    </end-state>
 
     <!-- abnormal exit point -->
     <decision-state id="viewBadRequest">
diff --git a/services/idp/src/main/webapp/WEB-INF/views/samlsignoutresponseform.jsp b/services/idp/src/main/webapp/WEB-INF/views/samlsignoutresponseform.jsp
new file mode 100644
index 0000000..ca3eaef
--- /dev/null
+++ b/services/idp/src/main/webapp/WEB-INF/views/samlsignoutresponseform.jsp
@@ -0,0 +1,45 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+
+<html>
+<head>
+<title>IDP SignOut Response Form</title>
+</head>
+<body onload='documentLoaded()'>
+    <form:form method="POST" id="samlsignoutresponseform" name="samlsignoutresponseform" action="${samlAction}" htmlEscape="true">
+        <input type="hidden" name="SAMLResponse" value="${samlResponse}" /><br />
+        <input type="hidden" name="RelayState" value="${relayState}" /><br />
+          <noscript>
+        <p>Script is disabled. Click Submit to continue.</p>
+        <input type="submit" name="_eventId_submit" value="Submit" /><br />
+         </noscript>
+    </form:form>
+    <script language="javascript">
+        /**
+         * Prepares the form for submission by appending any URI
+         * fragment (hash) to the form action in order to propagate it
+         * through the re-direct
+         * @param form The login form object.
+         * @returns the form.
+         */
+        function propagateUriFragment(form) {
+            // Extract the fragment from the browser's current location.
+            var hash = decodeURIComponent(self.document.location.hash);
+
+            // The fragment value may not contain a leading # symbol
+            if (hash && hash.indexOf("#") === -1) {
+                hash = "#" + hash;
+            }
+
+            // Append the fragment to the current action so that it persists to the redirected URL.
+            form.action = form.action + hash;
+            return form;
+        }
+        function documentLoaded() {
+            propagateUriFragment(document.forms[0]);
+            window.setTimeout('document.forms[0].submit()',0);
+        }
+    </script>
+</body>
+</html>
diff --git a/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/samlsso/IdpTest.java b/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/samlsso/IdpTest.java
index a2236c1..d442dab 100644
--- a/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/samlsso/IdpTest.java
+++ b/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/samlsso/IdpTest.java
@@ -86,6 +86,7 @@ import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
 import org.opensaml.saml.saml2.core.AuthnRequest;
 import org.opensaml.saml.saml2.core.Issuer;
 import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.LogoutResponse;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.NameIDPolicy;
 import org.opensaml.saml.saml2.core.RequestedAuthnContext;
@@ -1713,7 +1714,20 @@ public class IdpTest {
 
         HtmlForm form = idpPage.getFormByName("signoutconfirmationresponseform");
         HtmlSubmitInput button = form.getInputByName("_eventId_submit");
-        button.click();
+        HtmlPage signoutPage = button.click();
+
+        // Check Response
+        HtmlForm responseForm = signoutPage.getFormByName("samlsignoutresponseform");
+        String responseValue = responseForm.getInputByName("SAMLResponse").getAttributeNS(null, "value");
+        Assert.assertNotNull(responseValue);
+
+        byte[] deflatedToken = Base64Utility.decode(responseValue);
+        InputStream tokenStream = new ByteArrayInputStream(deflatedToken);
+        Document responseDoc = StaxUtils.read(new InputStreamReader(tokenStream, StandardCharsets.UTF_8));
+
+        LogoutResponse logoutResponse = (LogoutResponse)OpenSAMLUtil.fromDom(responseDoc.getDocumentElement());
+        Assert.assertNotNull(logoutResponse);
+        // TODO further checks
 
         webClient.close();