Repository: cxf Updated Branches: refs/heads/3.0.x-fixes f7c0e6279 -> 06696d589
Add initial support for a SAML SSO Metadata service Conflicts: rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractSSOSpHandler.java Project: http://git-wip-us.apache.org/repos/asf/cxf/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf/commit/06696d58 Tree: http://git-wip-us.apache.org/repos/asf/cxf/tree/06696d58 Diff: http://git-wip-us.apache.org/repos/asf/cxf/diff/06696d58 Branch: refs/heads/3.0.x-fixes Commit: 06696d589fbba7af84994392ffb67def0096c2a3 Parents: f7c0e62 Author: Colm O hEigeartaigh <cohei...@apache.org> Authored: Tue Apr 14 17:16:16 2015 +0100 Committer: Colm O hEigeartaigh <cohei...@apache.org> Committed: Wed Apr 15 16:35:39 2015 +0100 ---------------------------------------------------------------------- .../security/saml/sso/AbstractSSOSpHandler.java | 19 ++ .../saml/sso/AbstractServiceProviderFilter.java | 18 -- .../rs/security/saml/sso/MetadataService.java | 104 ++++++++ .../rs/security/saml/sso/MetadataWriter.java | 263 +++++++++++++++++++ .../cxf/rs/security/saml/sso/SSOConstants.java | 6 + 5 files changed, 392 insertions(+), 18 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf/blob/06696d58/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractSSOSpHandler.java ---------------------------------------------------------------------- diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractSSOSpHandler.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractSSOSpHandler.java index f839620..84c34ee 100644 --- a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractSSOSpHandler.java +++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractSSOSpHandler.java @@ -50,6 +50,7 @@ public class AbstractSSOSpHandler { private String signaturePropertiesFile; private CallbackHandler callbackHandler; private String callbackHandlerClass; + private String signatureUsername; static { OpenSAMLUtil.initSamlEngine(); @@ -243,4 +244,22 @@ public class AbstractSSOSpHandler { } return handler; } + + /** + * Set the username/alias to use to sign any request + * @param signatureUsername the username/alias to use to sign any request + */ + public void setSignatureUsername(String signatureUsername) { + this.signatureUsername = signatureUsername; + LOG.fine("Setting signatureUsername: " + signatureUsername); + } + + /** + * Get the username/alias to use to sign any request + * @return the username/alias to use to sign any request + */ + public String getSignatureUsername() { + return signatureUsername; + } + } http://git-wip-us.apache.org/repos/asf/cxf/blob/06696d58/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractServiceProviderFilter.java ---------------------------------------------------------------------- diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractServiceProviderFilter.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractServiceProviderFilter.java index e96566a..2ab978d 100644 --- a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractServiceProviderFilter.java +++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/AbstractServiceProviderFilter.java @@ -78,7 +78,6 @@ public abstract class AbstractServiceProviderFilter extends AbstractSSOSpHandler private String assertionConsumerServiceAddress; private AuthnRequestBuilder authnRequestBuilder = new DefaultAuthnRequestBuilder(); private boolean signRequest; - private String signatureUsername; private String webAppDomain; private boolean addWebAppContext = true; @@ -117,23 +116,6 @@ public abstract class AbstractServiceProviderFilter extends AbstractSSOSpHandler return idpServiceAddress; } - /** - * Set the username/alias to use to sign any request - * @param signatureUsername the username/alias to use to sign any request - */ - public void setSignatureUsername(String signatureUsername) { - this.signatureUsername = signatureUsername; - LOG.fine("Setting signatureUsername: " + signatureUsername); - } - - /** - * Get the username/alias to use to sign any request - * @return the username/alias to use to sign any request - */ - public String getSignatureUsername() { - return signatureUsername; - } - @PreDestroy public void close() { super.close(); http://git-wip-us.apache.org/repos/asf/cxf/blob/06696d58/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataService.java ---------------------------------------------------------------------- diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataService.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataService.java new file mode 100644 index 0000000..37b5623 --- /dev/null +++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataService.java @@ -0,0 +1,104 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.rs.security.saml.sso; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.security.auth.callback.CallbackHandler; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.w3c.dom.Document; + +import org.apache.cxf.common.i18n.BundleUtils; +import org.apache.cxf.common.logging.LogUtils; +import org.apache.cxf.jaxrs.utils.ExceptionUtils; +import org.apache.wss4j.common.crypto.Crypto; +import org.apache.wss4j.common.crypto.CryptoType; +import org.apache.wss4j.common.ext.WSPasswordCallback; + +@Path("metadata") +public class MetadataService extends AbstractSSOSpHandler { + protected static final Logger LOG = LogUtils.getL7dLogger(MetadataService.class); + protected static final ResourceBundle BUNDLE = BundleUtils.getBundle(MetadataService.class); + + private String serviceAddress; + private String logoutServiceAddress; + + @GET + @Produces("text/xml") + public Document getMetadata() { + try { + MetadataWriter metadataWriter = new MetadataWriter(); + + Crypto crypto = getSignatureCrypto(); + if (crypto == null) { + LOG.fine("No crypto instance of properties file configured for signature"); + throw ExceptionUtils.toInternalServerErrorException(null, null); + } + String signatureUser = getSignatureUsername(); + if (signatureUser == null) { + LOG.fine("No user configured for signature"); + throw ExceptionUtils.toInternalServerErrorException(null, null); + } + CallbackHandler callbackHandler = getCallbackHandler(); + if (callbackHandler == null) { + LOG.fine("No CallbackHandler configured to supply a password for signature"); + throw ExceptionUtils.toInternalServerErrorException(null, null); + } + + CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS); + cryptoType.setAlias(signatureUser); + X509Certificate[] issuerCerts = crypto.getX509Certificates(cryptoType); + if (issuerCerts == null) { + throw new Exception( + "No issuer certs were found to sign the request using name: " + signatureUser + ); + } + + // Get the password + WSPasswordCallback[] cb = {new WSPasswordCallback(signatureUser, WSPasswordCallback.SIGNATURE)}; + callbackHandler.handle(cb); + String password = cb[0].getPassword(); + + // Get the private key + PrivateKey privateKey = crypto.getPrivateKey(signatureUser, password); + + return metadataWriter.getMetaData(serviceAddress, logoutServiceAddress, privateKey, issuerCerts[0], + true); + } catch (Exception ex) { + LOG.log(Level.FINE, ex.getMessage(), ex); + throw ExceptionUtils.toInternalServerErrorException(ex, null); + } + } + + + protected void reportError(String code) { + org.apache.cxf.common.i18n.Message errorMsg = + new org.apache.cxf.common.i18n.Message(code, BUNDLE); + LOG.warning(errorMsg.toString()); + } + + +} http://git-wip-us.apache.org/repos/asf/cxf/blob/06696d58/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataWriter.java ---------------------------------------------------------------------- diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataWriter.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataWriter.java new file mode 100644 index 0000000..1a9c8be --- /dev/null +++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/MetadataWriter.java @@ -0,0 +1,263 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cxf.rs.security.saml.sso; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.security.Key; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.X509Data; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.w3c.dom.Document; + +import org.apache.xml.security.stax.impl.util.IDGenerator; +import org.apache.xml.security.utils.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MetadataWriter { + + private static final Logger LOG = LoggerFactory.getLogger(MetadataWriter.class); + + private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + private static final DocumentBuilderFactory DOC_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + private static final XMLSignatureFactory XML_SIGNATURE_FACTORY = XMLSignatureFactory.getInstance("DOM"); + + static { + DOC_BUILDER_FACTORY.setNamespaceAware(true); + } + + //CHECKSTYLE:OFF + public Document getMetaData( + String serviceURL, + String logoutURL, + Key signingKey, + X509Certificate signingCert, + boolean wantRequestsSigned + ) throws Exception { + + ByteArrayOutputStream bout = new ByteArrayOutputStream(4096); + Writer streamWriter = new OutputStreamWriter(bout, "UTF-8"); + XMLStreamWriter writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(streamWriter); + + writer.writeStartDocument("UTF-8", "1.0"); + + String referenceID = IDGenerator.generateID("_"); + writer.writeStartElement("md", "EntityDescriptor", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("ID", referenceID); + + writer.writeAttribute("entityID", serviceURL); + + writer.writeNamespace("md", SSOConstants.SAML2_METADATA_NS); + writer.writeNamespace("wsa", SSOConstants.WS_ADDRESSING_NS); + writer.writeNamespace("xsi", SSOConstants.SCHEMA_INSTANCE_NS); + + writeSAMLMetadata(writer, serviceURL, logoutURL, signingCert, wantRequestsSigned); + + writer.writeEndElement(); // EntityDescriptor + + writer.writeEndDocument(); + + streamWriter.flush(); + bout.flush(); + + if (LOG.isDebugEnabled()) { + String out = new String(bout.toByteArray()); + LOG.debug("***************** unsigned ****************"); + LOG.debug(out); + LOG.debug("***************** unsigned ****************"); + } + + InputStream is = new ByteArrayInputStream(bout.toByteArray()); + + if (signingKey != null) { + return signMetaInfo(signingCert, signingKey, is, referenceID); + } + return DOC_BUILDER_FACTORY.newDocumentBuilder().parse(is); + } + + private void writeSAMLMetadata( + XMLStreamWriter writer, + String serviceURL, + String logoutURL, + X509Certificate signingCert, + boolean wantRequestsSigned + ) throws XMLStreamException, MalformedURLException, CertificateEncodingException { + + writer.writeStartElement("md", "SPSSODescriptor", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("AuthnRequestsSigned", Boolean.toString(wantRequestsSigned)); + writer.writeAttribute("WantAssertionsSigned", "true"); + writer.writeAttribute("protocolSupportEnumeration", "urn:oasis:names:tc:SAML:2.0:protocol"); + + if (logoutURL != null) { + writer.writeStartElement("md", "SingleLogoutService", SSOConstants.SAML2_METADATA_NS); + + writer.writeAttribute("Location", logoutURL); + + writer.writeAttribute("Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + writer.writeEndElement(); // SingleLogoutService + } + + writer.writeStartElement("md", "AssertionConsumerService", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("Location", serviceURL); + writer.writeAttribute("index", "0"); + writer.writeAttribute("isDefault", "true"); + writer.writeAttribute("Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + writer.writeEndElement(); // AssertionConsumerService + + /* + if (protocol.getClaimTypesRequested() != null && !protocol.getClaimTypesRequested().isEmpty()) { + writer.writeStartElement("md", "AttributeConsumingService", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("index", "0"); + + writer.writeStartElement("md", "ServiceName", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("xml:lang", "en"); + writer.writeCharacters(config.getName()); + writer.writeEndElement(); // ServiceName + + for (Claim claim : protocol.getClaimTypesRequested()) { + writer.writeStartElement("md", "RequestedAttribute", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("isRequired", Boolean.toString(claim.isOptional())); + writer.writeAttribute("Name", claim.getType()); + writer.writeAttribute("NameFormat", + "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"); + writer.writeEndElement(); // RequestedAttribute + } + + writer.writeEndElement(); // AttributeConsumingService + } + */ + + if (signingCert != null) { + writer.writeStartElement("md", "KeyDescriptor", SSOConstants.SAML2_METADATA_NS); + writer.writeAttribute("use", "signing"); + + writer.writeStartElement("ds", "KeyInfo", "http://www.w3.org/2000/09/xmldsig#"); + writer.writeNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); + writer.writeStartElement("ds", "X509Data", "http://www.w3.org/2000/09/xmldsig#"); + writer.writeStartElement("ds", "X509Certificate", "http://www.w3.org/2000/09/xmldsig#"); + + // Write the Base-64 encoded certificate + byte data[] = signingCert.getEncoded(); + String encodedCertificate = Base64.encode(data); + writer.writeCharacters(encodedCertificate); + + writer.writeEndElement(); // X509Certificate + writer.writeEndElement(); // X509Data + writer.writeEndElement(); // KeyInfo + writer.writeEndElement(); // KeyDescriptor + } + + writer.writeEndElement(); // SPSSODescriptor + } + + private static Document signMetaInfo(X509Certificate signingCert, Key signingKey, + InputStream metaInfo, String referenceID + ) throws Exception { + String signatureMethod = null; + if ("SHA1withDSA".equals(signingCert.getSigAlgName())) { + signatureMethod = SignatureMethod.DSA_SHA1; + } else if ("SHA1withRSA".equals(signingCert.getSigAlgName())) { + signatureMethod = SignatureMethod.RSA_SHA1; + } else if ("SHA256withRSA".equals(signingCert.getSigAlgName())) { + signatureMethod = SignatureMethod.RSA_SHA1; + } else { + LOG.error("Unsupported signature method: " + signingCert.getSigAlgName()); + throw new RuntimeException("Unsupported signature method: " + signingCert.getSigAlgName()); + } + + List<Transform> transformList = new ArrayList<Transform>(); + transformList.add(XML_SIGNATURE_FACTORY.newTransform(Transform.ENVELOPED, (TransformParameterSpec)null)); + transformList.add(XML_SIGNATURE_FACTORY.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, + (C14NMethodParameterSpec)null)); + + // Create a Reference to the enveloped document (in this case, + // you are signing the whole document, so a URI of "" signifies + // that, and also specify the SHA1 digest algorithm and + // the ENVELOPED Transform. + Reference ref = + XML_SIGNATURE_FACTORY.newReference("#" + referenceID, + XML_SIGNATURE_FACTORY.newDigestMethod(DigestMethod.SHA1, null), + transformList, + null, null); + + // Create the SignedInfo. + SignedInfo si = + XML_SIGNATURE_FACTORY.newSignedInfo( + XML_SIGNATURE_FACTORY.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, + (C14NMethodParameterSpec)null), + XML_SIGNATURE_FACTORY.newSignatureMethod(signatureMethod, null), + Collections.singletonList(ref)); + + // Create the KeyInfo containing the X509Data. + KeyInfoFactory kif = XML_SIGNATURE_FACTORY.getKeyInfoFactory(); + List<Object> x509Content = new ArrayList<Object>(); + x509Content.add(signingCert.getSubjectX500Principal().getName()); + x509Content.add(signingCert); + X509Data xd = kif.newX509Data(x509Content); + KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd)); + + // Instantiate the document to be signed. + Document doc = DOC_BUILDER_FACTORY.newDocumentBuilder().parse(metaInfo); + + // Create a DOMSignContext and specify the RSA PrivateKey and + // location of the resulting XMLSignature's parent element. + //DOMSignContext dsc = new DOMSignContext(keyEntry.getPrivateKey(), doc.getDocumentElement()); + DOMSignContext dsc = new DOMSignContext(signingKey, doc.getDocumentElement()); + dsc.setIdAttributeNS(doc.getDocumentElement(), null, "ID"); + dsc.setNextSibling(doc.getDocumentElement().getFirstChild()); + + // Create the XMLSignature, but don't sign it yet. + XMLSignature signature = XML_SIGNATURE_FACTORY.newXMLSignature(si, ki); + + // Marshal, generate, and sign the enveloped signature. + signature.sign(dsc); + + // Output the resulting document. + return doc; + } + +} http://git-wip-us.apache.org/repos/asf/cxf/blob/06696d58/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SSOConstants.java ---------------------------------------------------------------------- diff --git a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SSOConstants.java b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SSOConstants.java index fde2ddd..871194b 100644 --- a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SSOConstants.java +++ b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SSOConstants.java @@ -35,6 +35,12 @@ public final class SSOConstants { public static final String SECURITY_CONTEXT_TOKEN = "org.apache.cxf.websso.context"; public static final String RACS_IS_COLLOCATED = "org.apache.cxf.racs.is.collocated"; + public static final String SAML2_METADATA_NS = "urn:oasis:names:tc:SAML:2.0:metadata"; + + public static final String WS_ADDRESSING_NS = "http://www.w3.org/2005/08/addressing"; + + public static final String SCHEMA_INSTANCE_NS = "http://www.w3.org/2001/XMLSchema-instance"; + private SSOConstants() { } }