/* * Copyright 2017 Graz University of Technology EAAF-Core Components has been developed in a * cooperation between EGIZ, A-SIT Plus, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work except in * compliance with the Licence. You may obtain a copy of the Licence at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * Unless required by applicable law or agreed to in writing, software distributed under the Licence * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the Licence for the specific language governing permissions and limitations under * the Licence. * * This product combines work with different licenses. See the "NOTICE" text file for details on the * various modules and licenses. The "NOTICE" text file is part of the distribution. Any derivative * works that you distribute must include a readable copy of the "NOTICE" text file. */ package at.gv.egiz.eaaf.modules.pvp2.impl.builder; import java.io.IOException; import java.io.StringWriter; import java.util.Collection; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IPvpMetadataBuilderConfiguration; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.AbstractCredentialProvider; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.Saml2Utils; import org.apache.commons.httpclient.auth.CredentialsNotAvailableException; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.Marshaller; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.AttributeConsumingService; import org.opensaml.saml.saml2.metadata.ContactPerson; import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml.saml2.metadata.KeyDescriptor; import org.opensaml.saml.saml2.metadata.NameIDFormat; import org.opensaml.saml.saml2.metadata.Organization; import org.opensaml.saml.saml2.metadata.RequestedAttribute; import org.opensaml.saml.saml2.metadata.RoleDescriptor; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml.saml2.metadata.ServiceName; import org.opensaml.saml.saml2.metadata.SingleLogoutService; import org.opensaml.saml.saml2.metadata.SingleSignOnService; import org.opensaml.security.SecurityException; import org.opensaml.security.credential.Credential; import org.opensaml.security.credential.UsageType; import org.opensaml.xml.security.SecurityHelper; import org.opensaml.xmlsec.keyinfo.KeyInfoGenerator; import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; import org.opensaml.xmlsec.signature.Signature; import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.Signer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.w3c.dom.Document; /** * PVP metadata builder implementation. * * @author tlenz * */ @Service("PVPMetadataBuilder") public class PvpMetadataBuilder { private static final Logger log = LoggerFactory.getLogger(PvpMetadataBuilder.class); X509KeyInfoGeneratorFactory keyInfoFactory = null; /** * PVP metadata builder. * */ public PvpMetadataBuilder() { keyInfoFactory = new X509KeyInfoGeneratorFactory(); keyInfoFactory.setEmitEntityIDAsKeyName(true); keyInfoFactory.setEmitEntityCertificate(true); } /** * Build PVP 2.1 conform SAML2 metadata. * * @param config PVPMetadataBuilder configuration* * @return PVP metadata as XML String * @throws SecurityException In case of an error * @throws ConfigurationException In case of an error * @throws CredentialsNotAvailableException In case of an error * @throws TransformerFactoryConfigurationError In case of an error * @throws MarshallingException In case of an error * @throws TransformerException In case of an error * @throws ParserConfigurationException In case of an error * @throws IOException In case of an error * @throws SignatureException In case of an error */ public String buildPvpMetadata(final IPvpMetadataBuilderConfiguration config) throws CredentialsNotAvailableException, EaafException, SecurityException, TransformerFactoryConfigurationError, MarshallingException, TransformerException, ParserConfigurationException, IOException, SignatureException { final DateTime date = new DateTime(); final EntityDescriptor entityDescriptor = Saml2Utils.createSamlObject(EntityDescriptor.class); // set entityID entityDescriptor.setEntityID(config.getEntityID()); // set contact and organisation information final List contactPersons = config.getContactPersonInformation(); if (contactPersons != null) { entityDescriptor.getContactPersons().addAll(contactPersons); } final Organization organisation = config.getOrgansiationInformation(); if (organisation != null) { entityDescriptor.setOrganization(organisation); } // set IDP metadata if (config.buildIdpSsoDescriptor()) { final RoleDescriptor idpSsoDesc = generateIdpMetadata(config); if (idpSsoDesc != null) { entityDescriptor.getRoleDescriptors().add(idpSsoDesc); } } // set SP metadata for interfederation if (config.buildSpSsoDescriptor()) { final RoleDescriptor spSsoDesc = generateSpMetadata(config); if (spSsoDesc != null) { entityDescriptor.getRoleDescriptors().add(spSsoDesc); } } // set metadata signature parameters final Credential metadataSignCred = config.getMetadataSigningCredentials(); final Signature signature = AbstractCredentialProvider.getIdpSignature(metadataSignCred); SecurityHelper.prepareSignatureParams(signature, metadataSignCred, null, null); // initialize XML document builder DocumentBuilder builder; final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); builder = factory.newDocumentBuilder(); final Document document = builder.newDocument(); // build entities descriptor if (config.buildEntitiesDescriptorAsRootElement()) { final EntitiesDescriptor entitiesDescriptor = Saml2Utils.createSamlObject(EntitiesDescriptor.class); entitiesDescriptor.setName(config.getEntityFriendlyName()); entitiesDescriptor.setID(Saml2Utils.getSecureIdentifier()); entitiesDescriptor.setValidUntil(date.plusHours(config.getMetadataValidUntil())); entitiesDescriptor.getEntityDescriptors().add(entityDescriptor); // load default PVP security configurations entitiesDescriptor.setSignature(signature); // marshall document final Marshaller out = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(entitiesDescriptor); out.marshall(entitiesDescriptor, document); } else { entityDescriptor.setValidUntil(date.plusHours(config.getMetadataValidUntil())); entityDescriptor.setID(Saml2Utils.getSecureIdentifier()); entityDescriptor.setSignature(signature); // marshall document final Marshaller out = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(entityDescriptor); out.marshall(entityDescriptor, document); } // sign metadata Signer.signObject(signature); // transform metadata object to XML string final Transformer transformer = TransformerFactory.newInstance().newTransformer(); final StringWriter sw = new StringWriter(); final StreamResult sr = new StreamResult(sw); final DOMSource source = new DOMSource(document); transformer.transform(source, sr); sw.close(); return sw.toString(); } private RoleDescriptor generateSpMetadata(final IPvpMetadataBuilderConfiguration config) throws CredentialsNotAvailableException, SecurityException, EaafException { final SPSSODescriptor spSsoDescriptor = Saml2Utils.createSamlObject(SPSSODescriptor.class); spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); spSsoDescriptor.setAuthnRequestsSigned(config.wantAuthnRequestSigned()); spSsoDescriptor.setWantAssertionsSigned(config.wantAssertionSigned()); final KeyInfoGenerator keyInfoGenerator = keyInfoFactory.newInstance(); // Set AuthRequest Signing certificate final Credential authcredential = config.getRequestorResponseSigningCredentials(); if (authcredential == null) { log.warn("SP Metadata generation FAILED! --> Builder has NO request signing-credential. "); return null; } else { final KeyDescriptor signKeyDescriptor = Saml2Utils.createSamlObject(KeyDescriptor.class); signKeyDescriptor.setUse(UsageType.SIGNING); signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(authcredential)); spSsoDescriptor.getKeyDescriptors().add(signKeyDescriptor); } // Set assertion encryption credentials final Credential authEncCredential = config.getEncryptionCredentials(); if (authEncCredential != null) { final KeyDescriptor encryKeyDescriptor = Saml2Utils.createSamlObject(KeyDescriptor.class); encryKeyDescriptor.setUse(UsageType.ENCRYPTION); encryKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(authEncCredential)); spSsoDescriptor.getKeyDescriptors().add(encryKeyDescriptor); } else { log.warn("No Assertion Encryption-Key defined. This setting is not recommended!"); } // check nameID formates if (config.getSpAllowedNameIdTypes() == null || config.getSpAllowedNameIdTypes().size() == 0) { log.warn( "SP Metadata generation FAILED! --> Builder has NO provideable SAML2 nameIDFormats. "); return null; } else { for (final String format : config.getSpAllowedNameIdTypes()) { final NameIDFormat nameIdFormat = Saml2Utils.createSamlObject(NameIDFormat.class); nameIdFormat.setFormat(format); spSsoDescriptor.getNameIDFormats().add(nameIdFormat); } } // add POST-Binding assertion consumer services if (StringUtils.isNotEmpty(config.getSpAssertionConsumerServicePostBindingUrl())) { final AssertionConsumerService postassertionConsumerService = Saml2Utils.createSamlObject(AssertionConsumerService.class); postassertionConsumerService.setIndex(0); postassertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); postassertionConsumerService .setLocation(config.getSpAssertionConsumerServicePostBindingUrl()); postassertionConsumerService.setIsDefault(true); spSsoDescriptor.getAssertionConsumerServices().add(postassertionConsumerService); } // add POST-Binding assertion consumer services if (StringUtils.isNotEmpty(config.getSpAssertionConsumerServiceRedirectBindingUrl())) { final AssertionConsumerService redirectassertionConsumerService = Saml2Utils.createSamlObject(AssertionConsumerService.class); redirectassertionConsumerService.setIndex(1); redirectassertionConsumerService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); redirectassertionConsumerService .setLocation(config.getSpAssertionConsumerServiceRedirectBindingUrl()); spSsoDescriptor.getAssertionConsumerServices().add(redirectassertionConsumerService); } // validate WebSSO endpoints if (spSsoDescriptor.getAssertionConsumerServices().size() == 0) { log.warn( "SP Metadata generation FAILED! --> NO SAML2 AssertionConsumerService endpoint found. "); return null; } // add POST-Binding SLO descriptor if (StringUtils.isNotEmpty(config.getSpSloPostBindingUrl())) { final SingleLogoutService postSloService = Saml2Utils.createSamlObject(SingleLogoutService.class); postSloService.setLocation(config.getSpSloPostBindingUrl()); postSloService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); spSsoDescriptor.getSingleLogoutServices().add(postSloService); } // add POST-Binding SLO descriptor if (StringUtils.isNotEmpty(config.getSpSloRedirectBindingUrl())) { final SingleLogoutService redirectSloService = Saml2Utils.createSamlObject(SingleLogoutService.class); redirectSloService.setLocation(config.getSpSloRedirectBindingUrl()); redirectSloService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); spSsoDescriptor.getSingleLogoutServices().add(redirectSloService); } // add POST-Binding SLO descriptor if (StringUtils.isNotEmpty(config.getSpSloSoapBindingUrl())) { final SingleLogoutService soapSloService = Saml2Utils.createSamlObject(SingleLogoutService.class); soapSloService.setLocation(config.getSpSloSoapBindingUrl()); soapSloService.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI); spSsoDescriptor.getSingleLogoutServices().add(soapSloService); } // add required attributes final Collection reqSpAttr = config.getSpRequiredAttributes(); final AttributeConsumingService attributeService = Saml2Utils.createSamlObject(AttributeConsumingService.class); attributeService.setIndex(0); attributeService.setIsDefault(true); final ServiceName serviceName = Saml2Utils.createSamlObject(ServiceName.class); serviceName.setValue("Default Service"); serviceName.setXMLLang("en"); attributeService.getNames().add(serviceName); if (reqSpAttr != null && reqSpAttr.size() > 0) { log.debug("Add " + reqSpAttr.size() + " attributes to SP metadata"); attributeService.getRequestAttributes().addAll(reqSpAttr); } else { log.debug("SP metadata contains NO requested attributes."); } spSsoDescriptor.getAttributeConsumingServices().add(attributeService); return spSsoDescriptor; } private IDPSSODescriptor generateIdpMetadata(final IPvpMetadataBuilderConfiguration config) throws EaafException, CredentialsNotAvailableException, SecurityException { // check response signing credential final Credential responseSignCred = config.getRequestorResponseSigningCredentials(); if (responseSignCred == null) { log.warn("IDP Metadata generation FAILED! --> Builder has NO Response signing credential. "); return null; } // check nameID formates if (config.getIdpPossibleNameIdTypes() == null || config.getIdpPossibleNameIdTypes().size() == 0) { log.warn( "IDP Metadata generation FAILED! --> Builder has NO provideable SAML2 nameIDFormats. "); return null; } // build SAML2 IDP-SSO descriptor element final IDPSSODescriptor idpSsoDescriptor = Saml2Utils.createSamlObject(IDPSSODescriptor.class); idpSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); // set ass default value, because PVP 2.x specification defines this feature as // MUST idpSsoDescriptor.setWantAuthnRequestsSigned(config.wantAuthnRequestSigned()); // add WebSSO descriptor for POST-Binding if (StringUtils.isNotEmpty(config.getIdpWebSsoPostBindingUrl())) { final SingleSignOnService postSingleSignOnService = Saml2Utils.createSamlObject(SingleSignOnService.class); postSingleSignOnService.setLocation(config.getIdpWebSsoPostBindingUrl()); postSingleSignOnService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); idpSsoDescriptor.getSingleSignOnServices().add(postSingleSignOnService); } // add WebSSO descriptor for Redirect-Binding if (StringUtils.isNotEmpty(config.getIdpWebSsoRedirectBindingUrl())) { final SingleSignOnService postSingleSignOnService = Saml2Utils.createSamlObject(SingleSignOnService.class); postSingleSignOnService.setLocation(config.getIdpWebSsoRedirectBindingUrl()); postSingleSignOnService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); idpSsoDescriptor.getSingleSignOnServices().add(postSingleSignOnService); } // add Single LogOut POST-Binding endpoing if (StringUtils.isNotEmpty(config.getIdpSloPostBindingUrl())) { final SingleLogoutService postSloService = Saml2Utils.createSamlObject(SingleLogoutService.class); postSloService.setLocation(config.getIdpSloPostBindingUrl()); postSloService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); idpSsoDescriptor.getSingleLogoutServices().add(postSloService); } // add Single LogOut Redirect-Binding endpoing if (StringUtils.isNotEmpty(config.getIdpSloRedirectBindingUrl())) { final SingleLogoutService redirectSloService = Saml2Utils.createSamlObject(SingleLogoutService.class); redirectSloService.setLocation(config.getIdpSloRedirectBindingUrl()); redirectSloService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); idpSsoDescriptor.getSingleLogoutServices().add(redirectSloService); } // validate WebSSO endpoints if (idpSsoDescriptor.getSingleSignOnServices().size() == 0) { log.warn("IDP Metadata generation FAILED! --> NO SAML2 SingleSignOnService endpoint found. "); return null; } // set assertion signing key final KeyDescriptor signKeyDescriptor = Saml2Utils.createSamlObject(KeyDescriptor.class); signKeyDescriptor.setUse(UsageType.SIGNING); final KeyInfoGenerator keyInfoGenerator = keyInfoFactory.newInstance(); signKeyDescriptor .setKeyInfo(keyInfoGenerator.generate(config.getRequestorResponseSigningCredentials())); idpSsoDescriptor.getKeyDescriptors().add(signKeyDescriptor); // set IDP attribute set idpSsoDescriptor.getAttributes().addAll(config.getIdpPossibleAttributes()); // set providable nameID formats for (final String format : config.getIdpPossibleNameIdTypes()) { final NameIDFormat nameIdFormat = Saml2Utils.createSamlObject(NameIDFormat.class); nameIdFormat.setFormat(format); idpSsoDescriptor.getNameIDFormats().add(nameIdFormat); } return idpSsoDescriptor; } }