/* * 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.text.MessageFormat; import java.util.Collection; import java.util.List; import javax.naming.ConfigurationException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactoryConfigurationError; import at.gv.egiz.eaaf.core.exceptions.EaafBuilderException; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.modules.pvp2.api.credential.EaafX509Credential; import at.gv.egiz.eaaf.modules.pvp2.api.metadata.IPvpMetadataBuilderConfiguration; import at.gv.egiz.eaaf.modules.pvp2.exception.CredentialsNotAvailableException; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.Saml2Utils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.core.xml.util.XMLObjectSupport; import org.opensaml.saml.common.SignableSAMLObject; 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.xmlsec.keyinfo.KeyInfoGenerator; import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory; import org.opensaml.xmlsec.signature.support.SignatureException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import net.shibboleth.utilities.java.support.xml.SerializeSupport; /** * PVP metadata builder implementation. * * @author tlenz * */ public class PvpMetadataBuilder { private static final String ERROR_ROLE_DESCR = "Can not build {0}"; 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); } else { final String msg = MessageFormat.format(ERROR_ROLE_DESCR, IDPSSODescriptor.DEFAULT_ELEMENT_LOCAL_NAME); throw new EaafBuilderException("internal.pvp.13", new Object[] { msg }, msg); } } // set SP metadata for interfederation if (config.buildSpSsoDescriptor()) { final RoleDescriptor spSsoDesc = generateSpMetadata(config); if (spSsoDesc != null) { entityDescriptor.getRoleDescriptors().add(spSsoDesc); } else { final String msg = MessageFormat.format(ERROR_ROLE_DESCR, SPSSODescriptor.DEFAULT_ELEMENT_LOCAL_NAME); throw new EaafBuilderException("internal.pvp.13", new Object[] { msg }, msg); } } SignableSAMLObject metadataToSign; // 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); metadataToSign = entitiesDescriptor; } else { entityDescriptor.setValidUntil(date.plusHours(config.getMetadataValidUntil())); entityDescriptor.setID(Saml2Utils.getSecureIdentifier()); metadataToSign = entityDescriptor; } // sign metadata final EaafX509Credential metadataSignCred = config.getMetadataSigningCredentials(); final SignableSAMLObject signedMetadata = Saml2Utils.signSamlObject(metadataToSign, metadataSignCred, true); // Serialize metadata final Element document = XMLObjectSupport.marshall(signedMetadata); final String serializedMetadata = SerializeSupport.nodeToString(document); return serializedMetadata; } private RoleDescriptor generateSpMetadata(final IPvpMetadataBuilderConfiguration config) throws 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, 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 if (config.getIdpPossibleAttributes() != null) { 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; } }