Repository: cxf-fediz Updated Branches: refs/heads/master 82ab06413 -> e68df8357
Added initial support for validating SAML redirect signatures Project: http://git-wip-us.apache.org/repos/asf/cxf-fediz/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf-fediz/commit/e68df835 Tree: http://git-wip-us.apache.org/repos/asf/cxf-fediz/tree/e68df835 Diff: http://git-wip-us.apache.org/repos/asf/cxf-fediz/diff/e68df835 Branch: refs/heads/master Commit: e68df8357ff18991a22e577cf480903961c08ba5 Parents: 82ab064 Author: Colm O hEigeartaigh <cohei...@apache.org> Authored: Tue Mar 22 16:45:05 2016 +0000 Committer: Colm O hEigeartaigh <cohei...@apache.org> Committed: Tue Mar 22 16:45:05 2016 +0000 ---------------------------------------------------------------------- .../idp/beans/samlsso/AuthnRequestParser.java | 7 +- .../idp/samlsso/AuthnRequestValidator.java | 54 +++++++-- .../WEB-INF/flows/saml-validate-request.xml | 5 +- .../apache/cxf/fediz/systests/idp/IdpTest.java | 117 +++++++++++++++++++ 4 files changed, 168 insertions(+), 15 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/e68df835/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java ---------------------------------------------------------------------- diff --git a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java index 737425f..410e8c1 100644 --- a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java +++ b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/beans/samlsso/AuthnRequestParser.java @@ -31,7 +31,6 @@ import org.apache.cxf.fediz.service.idp.domain.Idp; import org.apache.cxf.fediz.service.idp.samlsso.AuthnRequestValidator; import org.apache.cxf.fediz.service.idp.util.WebUtils; import org.apache.cxf.rs.security.saml.DeflateEncoderDecoder; -import org.apache.cxf.rs.security.saml.sso.SSOConstants; import org.apache.cxf.staxutils.StaxUtils; import org.apache.wss4j.common.saml.OpenSAMLUtil; import org.apache.wss4j.common.util.DOM2Writer; @@ -49,8 +48,8 @@ public class AuthnRequestParser { private static final Logger LOG = LoggerFactory.getLogger(AuthnRequestParser.class); - public void parseSAMLRequest(RequestContext context, Idp idp) throws ProcessingException { - String samlRequest = context.getFlowScope().getString(SSOConstants.SAML_REQUEST); + public void parseSAMLRequest(RequestContext context, Idp idp, String signature, + String relayState, String samlRequest) throws ProcessingException { LOG.debug("Received SAML Request: {}", samlRequest); AuthnRequest parsedRequest = null; @@ -69,7 +68,7 @@ public class AuthnRequestParser { if (parsedRequest != null) { try { AuthnRequestValidator validator = new AuthnRequestValidator(); - validator.validateAuthnRequest(context, parsedRequest, idp); + validator.validateAuthnRequest(context, parsedRequest, idp, signature, relayState, samlRequest); } catch (Exception ex) { LOG.warn("Error validating request {}", ex.getMessage(), ex); throw new ProcessingException(TYPE.BAD_REQUEST); http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/e68df835/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/AuthnRequestValidator.java ---------------------------------------------------------------------- diff --git a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/AuthnRequestValidator.java b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/AuthnRequestValidator.java index ebb48dc..b20b1f1 100644 --- a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/AuthnRequestValidator.java +++ b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/samlsso/AuthnRequestValidator.java @@ -18,6 +18,10 @@ */ package org.apache.cxf.fediz.service.idp.samlsso; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + import org.w3c.dom.Document; import org.apache.cxf.fediz.core.exception.ProcessingException; @@ -25,6 +29,7 @@ import org.apache.cxf.fediz.core.exception.ProcessingException.TYPE; import org.apache.cxf.fediz.core.util.CertsUtils; import org.apache.cxf.fediz.service.idp.domain.Idp; import org.apache.cxf.fediz.service.idp.util.WebUtils; +import org.apache.cxf.rs.security.saml.sso.SSOConstants; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.saml.SAMLKeyInfo; @@ -55,28 +60,57 @@ public class AuthnRequestValidator { private static final Logger LOG = LoggerFactory.getLogger(AuthnRequestValidator.class); - public void validateAuthnRequest(RequestContext context, AuthnRequest authnRequest, Idp idp) - throws ProcessingException, WSSecurityException { + public void validateAuthnRequest(RequestContext context, AuthnRequest authnRequest, Idp idp, String signature, + String relayState, String samlRequest) + throws Exception { if (authnRequest.isSigned()) { // Check destination - String destination = authnRequest.getDestination(); - LOG.debug("Validating destination: {}", destination); - - String localAddr = WebUtils.getHttpServletRequest(context).getRequestURL().toString(); - if (!localAddr.startsWith(destination)) { - LOG.debug("The destination {} does not match the local address {}", destination, localAddr); - throw new ProcessingException(TYPE.BAD_REQUEST); - } + checkDestination(context, authnRequest); // Check signature Crypto issuerCrypto = CertsUtils.getCryptoFromCertificate(idp.getCertificate()); validateAuthnRequestSignature(authnRequest.getSignature(), issuerCrypto); + } else if (signature != null) { + // Check destination + checkDestination(context, authnRequest); + + // Check signature + X509Certificate validatingCert = CertsUtils.parseX509Certificate(idp.getCertificate()); + + java.security.Signature sig = java.security.Signature.getInstance("SHA1withRSA"); + sig.initVerify(validatingCert); + + // Recreate request to sign + String requestToSign = WebUtils.getHttpServletRequest(context).getRequestURL().toString() + "?"; + requestToSign += SSOConstants.RELAY_STATE + "=" + relayState; + requestToSign += "&" + SSOConstants.SAML_REQUEST + "=" + URLEncoder.encode(samlRequest, "UTF-8"); + requestToSign += "&" + SSOConstants.SIG_ALG + "=" + + URLEncoder.encode(SSOConstants.RSA_SHA1, StandardCharsets.UTF_8.name()); + + sig.update(requestToSign.getBytes(StandardCharsets.UTF_8)); + + if (!sig.verify(signature.getBytes())) { + LOG.debug("Signature validation failed"); + throw new ProcessingException(TYPE.BAD_REQUEST); + } } else { LOG.debug("No signature is present, therefore the request is rejected"); throw new ProcessingException(TYPE.BAD_REQUEST); } } + private void checkDestination(RequestContext context, AuthnRequest authnRequest) throws ProcessingException { + // Check destination + String destination = authnRequest.getDestination(); + LOG.debug("Validating destination: {}", destination); + + String localAddr = WebUtils.getHttpServletRequest(context).getRequestURL().toString(); + if (!localAddr.startsWith(destination)) { + LOG.debug("The destination {} does not match the local address {}", destination, localAddr); + throw new ProcessingException(TYPE.BAD_REQUEST); + } + } + /** * Validate the AuthnRequest signature */ http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/e68df835/services/idp/src/main/webapp/WEB-INF/flows/saml-validate-request.xml ---------------------------------------------------------------------- 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 b97d020..52f7960 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 @@ -27,6 +27,7 @@ <on-entry> <set name="flowScope.RelayState" value="requestParameters.RelayState" /> <set name="flowScope.SAMLRequest" value="requestParameters.SAMLRequest" /> + <set name="flowScope.Signature" value="requestParameters.Signature" /> <set name="flowScope.idpConfig" value="config.getIDP(null)" /> </on-entry> <if test="requestParameters.RelayState == null or requestParameters.RelayState.length() == 0" @@ -36,7 +37,9 @@ </decision-state> <action-state id="parseAndValidateSAMLRequest"> - <evaluate expression="authnRequestParser.parseSAMLRequest(flowRequestContext, flowScope.idpConfig)" /> + <evaluate expression="authnRequestParser.parseSAMLRequest(flowRequestContext, flowScope.idpConfig, + flowScope.Signature, flowScope.RelayState, + flowScope.SAMLRequest)" /> <transition to="signinSAMLRequest"/> <transition on-exception="org.apache.cxf.fediz.core.exception.ProcessingException" to="viewBadRequest" /> </action-state> http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/e68df835/systests/samlsso/src/test/java/org/apache/cxf/fediz/systests/idp/IdpTest.java ---------------------------------------------------------------------- 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 441ca2c..e43e62f 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 @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.UUID; @@ -56,6 +57,7 @@ import org.apache.wss4j.common.crypto.CryptoType; import org.apache.wss4j.common.saml.OpenSAMLUtil; import org.apache.wss4j.common.util.DOM2Writer; import org.apache.wss4j.dom.engine.WSSConfig; +import org.apache.xml.security.utils.Base64; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; @@ -385,6 +387,121 @@ public class IdpTest { webClient.close(); } + @org.junit.Test + @org.junit.Ignore + public void testSeparateSignature() throws Exception { + OpenSAMLUtil.initSamlEngine(); + + // Create SAML AuthnRequest + Document doc = DOMUtils.createDocument(); + doc.appendChild(doc.createElement("root")); + // Create the AuthnRequest + String consumerURL = "https://localhost/acsa"; + AuthnRequest authnRequest = + new DefaultAuthnRequestBuilder().createAuthnRequest( + null, "urn:org:apache:cxf:fediz:fedizhelloworld", consumerURL + ); + authnRequest.setDestination("https://localhost:" + getIdpHttpsPort() + "/fediz-idp/saml"); + + Element authnRequestElement = OpenSAMLUtil.toDom(authnRequest, doc); + String authnRequestEncoded = encodeAuthnRequest(authnRequestElement); + + String urlEncodedRequest = URLEncoder.encode(authnRequestEncoded, "UTF-8"); + + String relayState = UUID.randomUUID().toString(); + + // Sign request + Crypto crypto = CryptoFactory.getInstance("stsKeystoreA.properties"); + + CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS); + cryptoType.setAlias("realma"); + + // Get the private key + PrivateKey privateKey = crypto.getPrivateKey("realma", "realma"); + + java.security.Signature signature = java.security.Signature.getInstance("SHA1withRSA"); + signature.initSign(privateKey); + + String requestToSign = "https://localhost:" + getIdpHttpsPort() + "/fediz-idp/saml?"; + requestToSign += SSOConstants.RELAY_STATE + "=" + relayState; + requestToSign += "&" + SSOConstants.SAML_REQUEST + "=" + urlEncodedRequest; + requestToSign += "&" + SSOConstants.SIG_ALG + "=" + + URLEncoder.encode(SSOConstants.RSA_SHA1, StandardCharsets.UTF_8.name()); + + signature.update(requestToSign.getBytes(StandardCharsets.UTF_8)); + byte[] signBytes = signature.sign(); + + String encodedSignature = Base64.encode(signBytes); + + String url = "https://localhost:" + getIdpHttpsPort() + "/fediz-idp/saml?"; + url += SSOConstants.RELAY_STATE + "=" + relayState; + url += "&" + SSOConstants.SAML_REQUEST + "=" + urlEncodedRequest; + url += "&" + SSOConstants.SIGNATURE + "=" + URLEncoder.encode(encodedSignature, StandardCharsets.UTF_8.name()); + + String user = "alice"; + String password = "ecila"; + + final WebClient webClient = new WebClient(); + webClient.getOptions().setUseInsecureSSL(true); + webClient.getCredentialsProvider().setCredentials( + new AuthScope("localhost", Integer.parseInt(getIdpHttpsPort())), + new UsernamePasswordCredentials(user, password)); + + webClient.getOptions().setJavaScriptEnabled(false); + final HtmlPage idpPage = webClient.getPage(url); + webClient.getOptions().setJavaScriptEnabled(true); + Assert.assertEquals("IDP SignIn Response Form", idpPage.getTitleText()); + + // Parse the form to get the token (SAMLResponse) + DomNodeList<DomElement> results = idpPage.getElementsByTagName("input"); + + String samlResponse = null; + boolean foundRelayState = false; + for (DomElement result : results) { + if ("SAMLResponse".equals(result.getAttributeNS(null, "name"))) { + samlResponse = result.getAttributeNS(null, "value"); + } else if ("RelayState".equals(result.getAttributeNS(null, "name"))) { + foundRelayState = true; + Assert.assertEquals(result.getAttributeNS(null, "value"), relayState); + } + } + + Assert.assertNotNull(samlResponse); + Assert.assertTrue(foundRelayState); + + // Check the "action" + DomNodeList<DomElement> formResults = idpPage.getElementsByTagName("form"); + Assert.assertFalse(formResults.isEmpty()); + + DomElement formResult = formResults.get(0); + String action = formResult.getAttributeNS(null, "action"); + Assert.assertTrue(action.equals(consumerURL)); + + // Decode + verify response + byte[] deflatedToken = Base64Utility.decode(samlResponse); + InputStream inputStream = new ByteArrayInputStream(deflatedToken); + + Document responseDoc = StaxUtils.read(new InputStreamReader(inputStream, "UTF-8")); + + XMLObject responseObject = OpenSAMLUtil.fromDom(responseDoc.getDocumentElement()); + Assert.assertTrue(responseObject instanceof org.opensaml.saml.saml2.core.Response); + + org.opensaml.saml.saml2.core.Response samlResponseObject = + (org.opensaml.saml.saml2.core.Response)responseObject; + Assert.assertTrue(authnRequest.getID().equals(samlResponseObject.getInResponseTo())); + + // Check claims + String parsedResponse = DOM2Writer.nodeToString(responseDoc); + String claim = ClaimTypes.FIRSTNAME.toString(); + Assert.assertTrue(parsedResponse.contains(claim)); + claim = ClaimTypes.LASTNAME.toString(); + Assert.assertTrue(parsedResponse.contains(claim)); + claim = ClaimTypes.EMAILADDRESS.toString(); + Assert.assertTrue(parsedResponse.contains(claim)); + + webClient.close(); + } + private String encodeAuthnRequest(Element authnRequest) throws IOException { String requestMessage = DOM2Writer.nodeToString(authnRequest);