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/19 17:00:10 UTC

[cxf-fediz] branch 1.4.x-fixes updated: FEDIZ-222 - Added initial support to process a SAML LogoutResponse

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

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


The following commit(s) were added to refs/heads/1.4.x-fixes by this push:
     new 03b04b5  FEDIZ-222 - Added initial support to process a SAML LogoutResponse
03b04b5 is described below

commit 03b04b574cc5115d916afe1c443aec96c87cfce7
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Thu Jul 19 17:46:30 2018 +0100

    FEDIZ-222 - Added initial support to process a SAML LogoutResponse
---
 .../cxf/fediz/core/processor/FedizRequest.java     |   7 ++
 .../fediz/core/processor/RedirectionResponse.java  |   9 ++
 .../fediz/core/processor/SAMLProcessorImpl.java    |  81 +++++++++++++-
 .../samlsso/SAMLProtocolResponseValidator.java     |   4 +-
 .../samlsso/SAML2PResponseComponentBuilder.java    |  28 ++++-
 .../cxf/fediz/core/samlsso/SAMLResponseTest.java   | 117 +++++++++++++++++++++
 6 files changed, 240 insertions(+), 6 deletions(-)

diff --git a/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/FedizRequest.java b/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/FedizRequest.java
index 4b07a57..03bb776 100644
--- a/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/FedizRequest.java
+++ b/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/FedizRequest.java
@@ -38,6 +38,7 @@ public class FedizRequest implements Serializable {
     private Certificate[] certs;
     private transient HttpServletRequest request;
     private RequestState requestState;
+    private boolean signOutRequest;
 
     public Certificate[] getCerts() {
         if (certs != null) {
@@ -88,5 +89,11 @@ public class FedizRequest implements Serializable {
     public void setRequestState(RequestState requestState) {
         this.requestState = requestState;
     }
+    public boolean isSignOutRequest() {
+        return signOutRequest;
+    }
+    public void setSignOutRequest(boolean signOutRequest) {
+        this.signOutRequest = signOutRequest;
+    }
 
 }
diff --git a/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/RedirectionResponse.java b/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/RedirectionResponse.java
index 91ded34..32a90cf 100644
--- a/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/RedirectionResponse.java
+++ b/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/RedirectionResponse.java
@@ -35,6 +35,7 @@ public class RedirectionResponse implements Serializable {
     private String redirectionURL;
     private Map<String, String> headers = new HashMap<>();
     private RequestState requestState;
+    private String state;
 
     public String getRedirectionURL() {
         return redirectionURL;
@@ -60,4 +61,12 @@ public class RedirectionResponse implements Serializable {
         this.requestState = requestState;
     }
 
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
 }
diff --git a/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/SAMLProcessorImpl.java b/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/SAMLProcessorImpl.java
index 638dd8a..1008674 100644
--- a/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/SAMLProcessorImpl.java
+++ b/plugins/core/src/main/java/org/apache/cxf/fediz/core/processor/SAMLProcessorImpl.java
@@ -26,6 +26,7 @@ import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
 import java.security.Signature;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.UUID;
@@ -36,6 +37,7 @@ import javax.servlet.http.HttpServletRequest;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
+import org.apache.cxf.fediz.core.Claim;
 import org.apache.cxf.fediz.core.RequestState;
 import org.apache.cxf.fediz.core.SAMLSSOConstants;
 import org.apache.cxf.fediz.core.TokenValidator;
@@ -64,6 +66,7 @@ import org.opensaml.core.xml.XMLObject;
 import org.opensaml.saml.common.xml.SAMLConstants;
 import org.opensaml.saml.saml2.core.AuthnRequest;
 import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.StatusResponseType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -92,8 +95,17 @@ public class SAMLProcessorImpl extends AbstractFedizProcessor {
             throw new IllegalStateException("Unsupported protocol");
         }
 
-        if (request.getResponseToken() == null || request.getState() == null) {
-            LOG.error("Missing response token or RelayState parameters");
+        if (request.getResponseToken() == null) {
+            LOG.error("Missing response token parameter");
+            throw new ProcessingException(TYPE.INVALID_REQUEST);
+        }
+
+        if (request.isSignOutRequest()) {
+            return processSignOutResponse(request, config);
+        }
+
+        if (request.getState() == null) {
+            LOG.error("Missing RelayState parameter");
             throw new ProcessingException(TYPE.INVALID_REQUEST);
         }
 
@@ -224,12 +236,74 @@ public class SAMLProcessorImpl extends AbstractFedizProcessor {
         return fedResponse;
     }
 
+    private FedizResponse processSignOutResponse(FedizRequest request, FedizContext config) throws ProcessingException {
+        SAMLProtocol protocol = (SAMLProtocol)config.getProtocol();
+
+        InputStream tokenStream = null;
+        try {
+            byte[] deflatedToken = Base64.decode(request.getResponseToken());
+            if (protocol.isDisableDeflateEncoding()) {
+                tokenStream = new ByteArrayInputStream(deflatedToken);
+            } else {
+                tokenStream = CompressionUtils.inflate(deflatedToken);
+            }
+        } catch (IllegalArgumentException | DataFormatException ex) {
+            LOG.warn("Invalid data format", ex);
+            throw new ProcessingException(TYPE.INVALID_REQUEST);
+        } catch (Base64DecodingException e) {
+            throw new ProcessingException(TYPE.INVALID_REQUEST);
+        }
+
+        Document doc = null;
+        Element el = null;
+        try {
+            doc = DOMUtils.readXml(tokenStream);
+            el = doc.getDocumentElement();
+
+        } catch (Exception e) {
+            LOG.warn("Failed to parse token", e);
+            throw new ProcessingException(TYPE.INVALID_REQUEST);
+        }
+
+        LOG.debug("Received response: " + DOM2Writer.nodeToString(el));
+
+        XMLObject responseObject = null;
+        try {
+            responseObject = OpenSAMLUtil.fromDom(el);
+        } catch (WSSecurityException ex) {
+            LOG.debug(ex.getMessage(), ex);
+            throw new ProcessingException(TYPE.INVALID_REQUEST);
+        }
+        if (!(responseObject instanceof org.opensaml.saml.saml2.core.LogoutResponse)) {
+            throw new ProcessingException(TYPE.INVALID_REQUEST);
+        }
+
+        org.opensaml.saml.saml2.core.LogoutResponse logoutResponse = 
+            (org.opensaml.saml.saml2.core.LogoutResponse)responseObject;
+        
+        // Validate the Response
+        validateSamlResponseProtocol(logoutResponse, config);
+        
+        Date issueInstant = logoutResponse.getIssueInstant().toDate();
+        
+        FedizResponse fedResponse = new FedizResponse(
+            null, logoutResponse.getIssuer().getValue(),
+            Collections.<String>emptyList(), Collections.<Claim>emptyList(),
+            null,
+            issueInstant,
+            null,
+            null,
+            logoutResponse.getID());
+
+        return fedResponse;
+    }
+
     /**
      * Validate the received SAML Response as per the protocol
      * @throws ProcessingException
      */
     protected void validateSamlResponseProtocol(
-        org.opensaml.saml.saml2.core.Response samlResponse,
+        StatusResponseType samlResponse,
         FedizContext config
     ) throws ProcessingException {
         try {
@@ -471,6 +545,7 @@ public class SAMLProcessorImpl extends AbstractFedizProcessor {
             RedirectionResponse response = new RedirectionResponse();
             response.addHeader("Cache-Control", "no-cache, no-store");
             response.addHeader("Pragma", "no-cache");
+            response.setState(relayState);
 
             redirectURL = redirectURL + "?" + sb.toString();
             response.setRedirectionURL(redirectURL);
diff --git a/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/SAMLProtocolResponseValidator.java b/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/SAMLProtocolResponseValidator.java
index 9f2c038..fff1621 100644
--- a/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/SAMLProtocolResponseValidator.java
+++ b/plugins/core/src/main/java/org/apache/cxf/fediz/core/samlsso/SAMLProtocolResponseValidator.java
@@ -68,7 +68,7 @@ public class SAMLProtocolResponseValidator {
      * @throws WSSecurityException
      */
     public void validateSamlResponse(
-        org.opensaml.saml.saml2.core.Response samlResponse,
+        org.opensaml.saml.saml2.core.StatusResponseType samlResponse,
         FedizContext config
     ) throws WSSecurityException {
         // Check the Status Code
@@ -120,7 +120,7 @@ public class SAMLProtocolResponseValidator {
      * Validate the Response signature (if it exists)
      */
     private void validateResponseSignature(
-        org.opensaml.saml.saml2.core.Response samlResponse,
+        org.opensaml.saml.saml2.core.StatusResponseType samlResponse,
         FedizContext config
     ) throws WSSecurityException {
         if (!samlResponse.isSigned()) {
diff --git a/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAML2PResponseComponentBuilder.java b/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAML2PResponseComponentBuilder.java
index e280f63..a193cae 100644
--- a/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAML2PResponseComponentBuilder.java
+++ b/plugins/core/src/test/java/org/apache/cxf/fediz/core/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;
@@ -38,6 +39,8 @@ import org.opensaml.saml.saml2.core.StatusMessage;
 public final class SAML2PResponseComponentBuilder {
 
     private static SAMLObjectBuilder<Response> responseBuilder;
+    
+    private static SAMLObjectBuilder<LogoutResponse> logoutResponseBuilder;
 
     private static SAMLObjectBuilder<Issuer> issuerBuilder;
 
@@ -83,6 +86,29 @@ public final class SAML2PResponseComponentBuilder {
 
         return response;
     }
+    
+    public static LogoutResponse createSAMLLogoutResponse(
+        String inResponseTo,
+        String issuer,
+        Status status,
+        String destination
+    ) {
+        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);
+        response.setDestination(destination);
+
+        return response;
+    }
 
     @SuppressWarnings("unchecked")
     public static Issuer createIssuer(
@@ -148,4 +174,4 @@ public final class SAML2PResponseComponentBuilder {
     }
 
 
-}
\ No newline at end of file
+}
diff --git a/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAMLResponseTest.java b/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAMLResponseTest.java
index a0b3681..e09e2e3 100644
--- a/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAMLResponseTest.java
+++ b/plugins/core/src/test/java/org/apache/cxf/fediz/core/samlsso/SAMLResponseTest.java
@@ -25,6 +25,8 @@ import java.math.BigInteger;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
@@ -63,6 +65,7 @@ import org.apache.cxf.fediz.core.processor.FedizResponse;
 import org.apache.cxf.fediz.core.processor.SAMLProcessorImpl;
 import org.apache.wss4j.common.crypto.Crypto;
 import org.apache.wss4j.common.crypto.CryptoFactory;
+import org.apache.wss4j.common.crypto.CryptoType;
 import org.apache.wss4j.common.ext.WSPasswordCallback;
 import org.apache.wss4j.common.ext.WSSecurityException;
 import org.apache.wss4j.common.saml.OpenSAMLUtil;
@@ -82,8 +85,16 @@ import org.joda.time.DateTimeZone;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
+import org.opensaml.saml.common.SAMLObjectContentReference;
+import org.opensaml.saml.common.SignableSAMLObject;
+import org.opensaml.saml.saml2.core.LogoutResponse;
 import org.opensaml.saml.saml2.core.Response;
 import org.opensaml.saml.saml2.core.Status;
+import org.opensaml.security.x509.BasicX509Credential;
+import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
+import org.opensaml.xmlsec.signature.KeyInfo;
+import org.opensaml.xmlsec.signature.Signature;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
 
 import static org.junit.Assert.fail;
 
@@ -1239,6 +1250,32 @@ public class SAMLResponseTest {
             // expected
         }
     }
+    
+    @org.junit.Test
+    public void validateLogoutResponse() throws Exception {
+        // Mock up a LogoutResponse
+        FedizContext config = getFederationConfigurator().getFedizContext("ROOT");
+
+        String requestId = URLEncoder.encode(UUID.randomUUID().toString(), "UTF-8");
+        
+        String status = "urn:oasis:names:tc:SAML:2.0:status:Success";
+        Element logoutResponse = createLogoutResponse(status, TEST_REQUEST_URL, true, requestId);
+
+        HttpServletRequest req = EasyMock.createMock(HttpServletRequest.class);
+        EasyMock.expect(req.getRequestURL()).andReturn(new StringBuffer(TEST_REQUEST_URL));
+        EasyMock.expect(req.getRemoteAddr()).andReturn(TEST_CLIENT_ADDRESS);
+        EasyMock.replay(req);
+
+        FedizRequest wfReq = new FedizRequest();
+        wfReq.setResponseToken(encodeResponse(logoutResponse));
+        String relayState = URLEncoder.encode(UUID.randomUUID().toString(), "UTF-8");
+        wfReq.setState(relayState);
+        wfReq.setRequest(req);
+        wfReq.setSignOutRequest(true);
+
+        FedizProcessor wfProc = new SAMLProcessorImpl();
+        wfProc.processRequest(wfReq, config);
+    }
 
     private String createSamlResponseStr(String requestId) throws Exception {
         // Create SAML Assertion
@@ -1308,6 +1345,86 @@ public class SAMLResponseTest {
         return policyElement;
     }
 
+    private Element createLogoutResponse(String statusValue, String destination, 
+                                         boolean sign, String requestID) throws Exception {
+        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
+        Document doc = docBuilder.newDocument();
+
+        Status status =
+            SAML2PResponseComponentBuilder.createStatus(statusValue, null);
+        LogoutResponse response =
+            SAML2PResponseComponentBuilder.createSAMLLogoutResponse(requestID, TEST_IDP_ISSUER, status, destination);
+
+        // Sign the LogoutResponse
+        if (sign) {
+            signResponse(response, "mystskey");
+        }
+
+        Element policyElement = OpenSAMLUtil.toDom(response, doc);
+        doc.appendChild(policyElement);
+
+        return policyElement;
+    }
+    
+    private void signResponse(SignableSAMLObject signableObject, String alias) throws Exception {
+
+        Signature signature = OpenSAMLUtil.buildSignature();
+        signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
+        cryptoType.setAlias(alias);
+        X509Certificate[] issuerCerts = crypto.getX509Certificates(cryptoType);
+
+        String sigAlgo = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1;
+        String pubKeyAlgo = issuerCerts[0].getPublicKey().getAlgorithm();
+        if (pubKeyAlgo.equalsIgnoreCase("DSA")) {
+            sigAlgo = SignatureConstants.ALGO_ID_SIGNATURE_DSA;
+        } else if (pubKeyAlgo.equalsIgnoreCase("EC")) {
+            sigAlgo = SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA1;
+        }
+        
+        WSPasswordCallback[] cb = {
+            new WSPasswordCallback(alias, WSPasswordCallback.SIGNATURE)
+        };
+        cbPasswordHandler.handle(cb);
+        String password = cb[0].getPassword();
+        
+        PrivateKey privateKey;
+        try {
+            privateKey = crypto.getPrivateKey(alias, password);
+        } catch (Exception ex) {
+            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, ex);
+        }
+        if (privateKey == null) {
+            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "empty",
+                new Object[] {"No private key was found using issuer name: " + alias});
+        }
+
+        signature.setSignatureAlgorithm(sigAlgo);
+
+        BasicX509Credential signingCredential =
+            new BasicX509Credential(issuerCerts[0], privateKey);
+
+        signature.setSigningCredential(signingCredential);
+
+        X509KeyInfoGeneratorFactory kiFactory = new X509KeyInfoGeneratorFactory();
+        kiFactory.setEmitEntityCertificate(true);
+
+        try {
+            KeyInfo keyInfo = kiFactory.newInstance().generate(signingCredential);
+            signature.setKeyInfo(keyInfo);
+        } catch (org.opensaml.security.SecurityException ex) {
+            throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, ex, "empty",
+                new Object[] {"Error generating KeyInfo from signing credential"});
+        }
+
+        signableObject.setSignature(signature);
+        String digestAlg = SignatureConstants.ALGO_ID_DIGEST_SHA1;
+        SAMLObjectContentReference contentRef =
+            (SAMLObjectContentReference)signature.getContentReferences().get(0);
+        contentRef.setDigestAlgorithm(digestAlg);
+        signableObject.releaseDOM();
+        signableObject.releaseChildrenDOM(true);
+    }
 
     /**
      * Returns the first element that matches <code>name</code> and