/******************************************************************************* *******************************************************************************/ 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 org.apache.commons.httpclient.auth.CredentialsNotAvailableException; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.opensaml.Configuration; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml2.metadata.AttributeConsumingService; import org.opensaml.saml2.metadata.ContactPerson; import org.opensaml.saml2.metadata.EntitiesDescriptor; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml2.metadata.KeyDescriptor; import org.opensaml.saml2.metadata.LocalizedString; import org.opensaml.saml2.metadata.NameIDFormat; import org.opensaml.saml2.metadata.Organization; import org.opensaml.saml2.metadata.RequestedAttribute; import org.opensaml.saml2.metadata.RoleDescriptor; import org.opensaml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml2.metadata.ServiceName; import org.opensaml.saml2.metadata.SingleLogoutService; import org.opensaml.saml2.metadata.SingleSignOnService; import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.io.MarshallingException; import org.opensaml.xml.security.SecurityException; import org.opensaml.xml.security.SecurityHelper; import org.opensaml.xml.security.credential.Credential; import org.opensaml.xml.security.credential.UsageType; import org.opensaml.xml.security.keyinfo.KeyInfoGenerator; import org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory; import org.opensaml.xml.signature.Signature; import org.opensaml.xml.signature.SignatureException; import org.opensaml.xml.signature.Signer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.w3c.dom.Document; 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.opensaml.initialize.EAAFDefaultSAML2Bootstrap; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.AbstractCredentialProvider; import at.gv.egiz.eaaf.modules.pvp2.impl.utils.SAML2Utils; /** * @author tlenz * */ @Service("PVPMetadataBuilder") public class PVPMetadataBuilder { private static final Logger log = LoggerFactory.getLogger(PVPMetadataBuilder.class); X509KeyInfoGeneratorFactory keyInfoFactory = null; /** * */ 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 * @throws ConfigurationException * @throws CredentialsNotAvailableException * @throws TransformerFactoryConfigurationError * @throws MarshallingException * @throws TransformerException * @throws ParserConfigurationException * @throws IOException * @throws SignatureException */ public String buildPVPMetadata(IPVPMetadataBuilderConfiguration config) throws CredentialsNotAvailableException, EAAFException, SecurityException, TransformerFactoryConfigurationError, MarshallingException, TransformerException, ParserConfigurationException, IOException, SignatureException { DateTime date = new DateTime(); EntityDescriptor entityDescriptor = SAML2Utils .createSAMLObject(EntityDescriptor.class); //set entityID entityDescriptor.setEntityID(config.getEntityID()); //set contact and organisation information List contactPersons = config.getContactPersonInformation(); if (contactPersons != null) entityDescriptor.getContactPersons().addAll(contactPersons); Organization organisation = config.getOrgansiationInformation(); if (organisation != null) entityDescriptor.setOrganization(organisation); //set IDP metadata if (config.buildIDPSSODescriptor()) { RoleDescriptor idpSSODesc = generateIDPMetadata(config); if (idpSSODesc != null) entityDescriptor.getRoleDescriptors().add(idpSSODesc); } //set SP metadata for interfederation if (config.buildSPSSODescriptor()) { RoleDescriptor spSSODesc = generateSPMetadata(config); if (spSSODesc != null) entityDescriptor.getRoleDescriptors().add(spSSODesc); } //set metadata signature parameters Credential metadataSignCred = config.getMetadataSigningCredentials(); Signature signature = AbstractCredentialProvider.getIDPSignature(metadataSignCred); SecurityHelper.prepareSignatureParams(signature, metadataSignCred, null, null); //initialize XML document builder DocumentBuilder builder; DocumentBuilderFactory factory = DocumentBuilderFactory .newInstance(); builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); //build entities descriptor if (config.buildEntitiesDescriptorAsRootElement()) { 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 EAAFDefaultSAML2Bootstrap.initializeDefaultPVPConfiguration(); entitiesDescriptor.setSignature(signature); //marshall document Marshaller out = Configuration.getMarshallerFactory() .getMarshaller(entitiesDescriptor); out.marshall(entitiesDescriptor, document); } else { entityDescriptor.setValidUntil(date.plusHours(config.getMetadataValidUntil())); entityDescriptor.setID(SAML2Utils.getSecureIdentifier()); entityDescriptor.setSignature(signature); //marshall document Marshaller out = Configuration.getMarshallerFactory() .getMarshaller(entityDescriptor); out.marshall(entityDescriptor, document); } //sign metadata Signer.signObject(signature); //transform metadata object to XML string Transformer transformer = TransformerFactory.newInstance() .newTransformer(); StringWriter sw = new StringWriter(); StreamResult sr = new StreamResult(sw); DOMSource source = new DOMSource(document); transformer.transform(source, sr); sw.close(); return sw.toString(); } private RoleDescriptor generateSPMetadata(IPVPMetadataBuilderConfiguration config) throws CredentialsNotAvailableException, SecurityException, EAAFException { SPSSODescriptor spSSODescriptor = SAML2Utils.createSAMLObject(SPSSODescriptor.class); spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); spSSODescriptor.setAuthnRequestsSigned(config.wantAuthnRequestSigned()); spSSODescriptor.setWantAssertionsSigned(config.wantAssertionSigned()); KeyInfoGenerator keyInfoGenerator = keyInfoFactory.newInstance(); //Set AuthRequest Signing certificate Credential authcredential = config.getRequestorResponseSigningCredentials(); if (authcredential == null) { log.warn("SP Metadata generation FAILED! --> Builder has NO request signing-credential. "); return null; } else { KeyDescriptor signKeyDescriptor = SAML2Utils .createSAMLObject(KeyDescriptor.class); signKeyDescriptor.setUse(UsageType.SIGNING); signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(authcredential)); spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); } //Set assertion encryption credentials Credential authEncCredential = config.getEncryptionCredentials(); if (authEncCredential != null) { 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.getSPAllowedNameITTypes() == null || config.getSPAllowedNameITTypes().size() == 0) { log.warn("SP Metadata generation FAILED! --> Builder has NO provideable SAML2 nameIDFormats. "); return null; } else { for (String format : config.getSPAllowedNameITTypes()) { NameIDFormat nameIDFormat = SAML2Utils.createSAMLObject(NameIDFormat.class); nameIDFormat.setFormat(format); spSSODescriptor.getNameIDFormats().add(nameIDFormat); } } //add POST-Binding assertion consumer services if (StringUtils.isNotEmpty(config.getSPAssertionConsumerServicePostBindingURL())) { 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())) { 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())) { 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())) { 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())) { SingleLogoutService soapSLOService = SAML2Utils.createSAMLObject(SingleLogoutService.class); soapSLOService.setLocation(config.getSPSLOSOAPBindingURL()); soapSLOService.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI); spSSODescriptor.getSingleLogoutServices().add(soapSLOService); } //add required attributes Collection reqSPAttr = config.getSPRequiredAttributes(); AttributeConsumingService attributeService = SAML2Utils.createSAMLObject(AttributeConsumingService.class); attributeService.setIndex(0); attributeService.setIsDefault(true); ServiceName serviceName = SAML2Utils.createSAMLObject(ServiceName.class); serviceName.setName(new LocalizedString("Default Service", "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(IPVPMetadataBuilderConfiguration config) throws EAAFException, CredentialsNotAvailableException, SecurityException { //check response signing credential 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.getIDPPossibleNameITTypes() == null || config.getIDPPossibleNameITTypes().size() == 0) { log.warn("IDP Metadata generation FAILED! --> Builder has NO provideable SAML2 nameIDFormats. "); return null; } // build SAML2 IDP-SSO descriptor element 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())) { 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())) { 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())) { 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())) { 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 KeyDescriptor signKeyDescriptor = SAML2Utils .createSAMLObject(KeyDescriptor.class); signKeyDescriptor.setUse(UsageType.SIGNING); 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 (String format : config.getIDPPossibleNameITTypes()) { NameIDFormat nameIDFormat = SAML2Utils.createSAMLObject(NameIDFormat.class); nameIDFormat.setFormat(format); idpSSODescriptor.getNameIDFormats().add(nameIDFormat); } return idpSSODescriptor; } }