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 a5a9efe FEDIZ-221 - Sending back LogoutResponse from the IdP a5a9efe is described below commit a5a9efe8211a386bdef507748aa7e2d81b552e9e Author: Colm O hEigeartaigh <cohei...@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 +++++++++++++++++ .../org/apache/cxf/fediz/systests/idp/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 cc1cd61..b0730d3 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 @@ -37,6 +37,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/idp/IdpTest.java b/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/idp/IdpTest.java index 572cddb..41ed6bc 100644 --- a/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/idp/IdpTest.java +++ b/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/idp/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; @@ -1718,7 +1719,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();