package com.onelogin.saml2.settings;
import java.net.URL;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.xpath.XPathExpressionException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.PrivateKey;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import com.onelogin.saml2.model.Contact;
import com.onelogin.saml2.model.Organization;
import com.onelogin.saml2.model.AttributeConsumingService;
import com.onelogin.saml2.model.RequestedAttribute;
import com.onelogin.saml2.util.Util;
/**
* Metadata class of OneLogin's Java Toolkit.
*
* A class that contains methods related to the metadata of the SP
*/
public class Metadata {
/**
* Private property to construct a logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(Metadata.class);
// Constants
private static final int N_DAYS_VALID_UNTIL = 2;
private static final int SECONDS_CACHED = 604800; // 1 week
/**
* AttributeConsumingService
*/
private AttributeConsumingService attributeConsumingService = null;
/**
* Generated metadata in string format
*/
private final String metadataString;
/**
* validUntilTime of the metadata. How long the metadata is valid
*/
private final Calendar validUntilTime;
/**
* cacheDuration of the metadata. Duration of the cache in seconds
*/
private final Integer cacheDuration;
/**
* Constructs the Metadata object.
*
* @param settings
* Saml2Settings object. Setting data
* @param validUntilTime
* Metadata's valid time
* @param cacheDuration
* Duration of the cache in seconds
* @param attributeConsumingService
* AttributeConsumingService of service provider
*
* @throws CertificateEncodingException
*/
public Metadata(Saml2Settings settings, Calendar validUntilTime, Integer cacheDuration, AttributeConsumingService attributeConsumingService) throws CertificateEncodingException {
if (validUntilTime == null) {
this.validUntilTime = Calendar.getInstance();
this.validUntilTime.add(Calendar.DAY_OF_YEAR, N_DAYS_VALID_UNTIL);
} else {
this.validUntilTime = validUntilTime;
}
this.attributeConsumingService = attributeConsumingService;
if (cacheDuration == null) {
this.cacheDuration = SECONDS_CACHED;
} else {
this.cacheDuration = cacheDuration;
}
StrSubstitutor substitutor = generateSubstitutor(settings);
String unsignedMetadataString = substitutor.replace(getMetadataTemplate());
LOGGER.debug("metadata --> " + unsignedMetadataString);
metadataString = unsignedMetadataString;
}
/**
* Constructs the Metadata object.
*
* @param settings
* Saml2Settings object. Setting data
* @param validUntilTime
* Metadata's valid time
* @param cacheDuration
* Duration of the cache in seconds
*
* @throws CertificateEncodingException
*/
public Metadata(Saml2Settings settings, Calendar validUntilTime, Integer cacheDuration) throws CertificateEncodingException {
this(settings, validUntilTime, cacheDuration, null);
}
/**
* Constructs the Metadata object.
*
* @param settings
* Saml2Settings object. Setting data
*
* @throws CertificateEncodingException
*/
public Metadata(Saml2Settings settings) throws CertificateEncodingException {
this(settings, null, null);
}
/**
* Substitutes metadata variables within a string by values.
*
* @param settings
* Saml2Settings object. Setting data
*
* @return the StrSubstitutor object of the metadata
*/
private StrSubstitutor generateSubstitutor(Saml2Settings settings) throws CertificateEncodingException {
Map<String, String> valueMap = new HashMap<String, String>();
valueMap.put("id", Util.generateUniqueID());
valueMap.put("validUntilTime", Util.formatDateTime(validUntilTime.getTimeInMillis()));
valueMap.put("cacheDuration", String.valueOf(cacheDuration));
valueMap.put("spEntityId", settings.getSpEntityId());
valueMap.put("strAuthnsign", String.valueOf(settings.getAuthnRequestsSigned()));
valueMap.put("strWsign", String.valueOf(settings.getWantAssertionsSigned()));
valueMap.put("spNameIDFormat", settings.getSpNameIDFormat());
valueMap.put("spAssertionConsumerServiceBinding", settings.getSpAssertionConsumerServiceBinding());
valueMap.put("spAssertionConsumerServiceUrl", settings.getSpAssertionConsumerServiceUrl().toString());
valueMap.put("sls", toSLSXml(settings.getSpSingleLogoutServiceUrl(), settings.getSpSingleLogoutServiceBinding()));
valueMap.put("strAttributeConsumingService", getAttributeConsumingServiceXml());
valueMap.put("strKeyDescriptor", toX509KeyDescriptorsXML(settings.getSPcert()));
valueMap.put("strContacts", toContactsXml(settings.getContacts()));
valueMap.put("strOrganization", toOrganizationXml(settings.getOrganization(), "en"));
return new StrSubstitutor(valueMap);
}
/**
* @return the metadata's template
*/
private static StringBuilder getMetadataTemplate() {
StringBuilder template = new StringBuilder();
template.append("<?xml version=\"1.0\"?>");
template.append("<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\"");
template.append(" validUntil=\"${validUntilTime}\"");
template.append(" cacheDuration=\"PT${cacheDuration}S\"");
template.append(" entityID=\"${spEntityId}\"");
template.append(" ID=\"${id}\">");
template.append("<md:SPSSODescriptor AuthnRequestsSigned=\"${strAuthnsign}\" WantAssertionsSigned=\"${strWsign}\" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">");
template.append("${strKeyDescriptor}");
template.append("${sls}<md:NameIDFormat>${spNameIDFormat}</md:NameIDFormat>");
template.append("<md:AssertionConsumerService Binding=\"${spAssertionConsumerServiceBinding}\"");
template.append(" Location=\"${spAssertionConsumerServiceUrl}\"");
template.append(" index=\"1\"/>");
template.append("${strAttributeConsumingService}");
template.append("</md:SPSSODescriptor>${strOrganization}${strContacts}");
template.append("</md:EntityDescriptor>");
return template;
}
/**
* Generates the AttributeConsumingService section of the metadata's template
*
*
* @return the AttributeConsumingService section of the metadata's template
*/
private String getAttributeConsumingServiceXml() {
StringBuilder attributeConsumingServiceXML = new StringBuilder();
if (attributeConsumingService != null) {
String serviceName = attributeConsumingService.getServiceName();
String serviceDescription = attributeConsumingService.getServiceDescription();
List<RequestedAttribute> requestedAttributes = attributeConsumingService.getRequestedAttributes();
attributeConsumingServiceXML.append("<md:AttributeConsumingService index=\"1\">");
if (serviceName != null && !serviceName.isEmpty()) {
attributeConsumingServiceXML.append("<md:ServiceName xml:lang=\"en\">" + serviceName + "</md:ServiceName>");
}
if (serviceDescription != null && !serviceDescription.isEmpty()) {
attributeConsumingServiceXML.append("<md:ServiceDescription xml:lang=\"en\">" + serviceDescription + "</md:ServiceDescription>");
}
if (requestedAttributes != null && !requestedAttributes.isEmpty()) {
for (RequestedAttribute requestedAttribute : requestedAttributes) {
String name = requestedAttribute.getName();
String friendlyName = requestedAttribute.getFriendlyName();
String nameFormat = requestedAttribute.getNameFormat();
Boolean isRequired = requestedAttribute.isRequired();
List<String> attrValues = requestedAttribute.getAttributeValues() ;
String contentStr = "<md:RequestedAttribute";
if (name != null && !name.isEmpty()) {
contentStr += " Name=\"" + name + "\"";
}
if (nameFormat != null && !nameFormat.isEmpty()) {
contentStr += " NameFormat=\"" + nameFormat + "\"";
}
if (friendlyName != null && !friendlyName.isEmpty()) {
contentStr += " FriendlyName=\"" + friendlyName + "\"";
}
if (isRequired != null) {
contentStr += " isRequired=\"" + isRequired.toString() + "\"";
}
if (attrValues != null && !attrValues.isEmpty()) {
contentStr += ">";
for (String attrValue : attrValues) {
contentStr += "<saml:AttributeValue xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + attrValue + "</saml:AttributeValue>";
}
attributeConsumingServiceXML.append(contentStr + "</md:RequestedAttribute>");
} else {
attributeConsumingServiceXML.append(contentStr + " />");
}
}
}
attributeConsumingServiceXML.append("</md:AttributeConsumingService>");
}
return attributeConsumingServiceXML.toString();
}
/**
* Generates the contact section of the metadata's template
*
* @param contacts
* List of contact objects
*
* @return the contact section of the metadata's template
*/
private String toContactsXml(List<Contact> contacts) {
StringBuilder contactsXml = new StringBuilder();
for (Contact contact : contacts) {
contactsXml.append("<md:ContactPerson contactType=\"" + contact.getContactType() + "\">");
contactsXml.append("<md:GivenName>" + contact.getGivenName() + "</md:GivenName>");
contactsXml.append("<md:EmailAddress>" + contact.getEmailAddress() + "</md:EmailAddress>");
contactsXml.append("</md:ContactPerson>");
}
return contactsXml.toString();
}
/**
* Generates the organization section of the metadata's template
*
* @param organization
* organization object
* @param lang
* language
*
* @return the organization section of the metadata's template
*/
private String toOrganizationXml(Organization organization, String lang) {
String orgXml = "";
if (lang == null) {
lang = "en";
}
if (organization != null) {
orgXml = "<md:Organization><md:OrganizationName xml:lang=\"" + lang + "\">" + organization.getOrgName()
+ "</md:OrganizationName><md:OrganizationDisplayName xml:lang=\"" + lang + "\">"
+ organization.getOrgDisplayName() + "</md:OrganizationDisplayName><md:OrganizationURL xml:lang=\""
+ lang + "\">" + organization.getOrgUrl() + "</md:OrganizationURL></md:Organization>";
}
return orgXml;
}
/**
* Generates the KeyDescriptor section of the metadata's template
*
* @param cert
* the public cert that will be used by the SP to sign and encrypt
*
* @return the KeyDescriptor section of the metadata's template
*/
private String toX509KeyDescriptorsXML(X509Certificate cert) throws CertificateEncodingException {
StringBuilder keyDescriptorXml = new StringBuilder();
if (cert != null) {
Base64 encoder = new Base64(64);
byte[] encodedCert = cert.getEncoded();
String certString = new String(encoder.encode(encodedCert));
keyDescriptorXml.append("<md:KeyDescriptor use=\"signing\">");
keyDescriptorXml.append("<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">");
keyDescriptorXml.append("<ds:X509Data>");
keyDescriptorXml.append("<ds:X509Certificate>"+certString+"</ds:X509Certificate>");
keyDescriptorXml.append("</ds:X509Data>");
keyDescriptorXml.append("</ds:KeyInfo>");
keyDescriptorXml.append("</md:KeyDescriptor>");
keyDescriptorXml.append("<md:KeyDescriptor use=\"encryption\">");
keyDescriptorXml.append("<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">");
keyDescriptorXml.append("<ds:X509Data>");
keyDescriptorXml.append("<ds:X509Certificate>"+certString+"</ds:X509Certificate>");
keyDescriptorXml.append("</ds:X509Data>");
keyDescriptorXml.append("</ds:KeyInfo>");
keyDescriptorXml.append("</md:KeyDescriptor>");
}
return keyDescriptorXml.toString();
}
/**
* @return the md:SingleLogoutService section of the metadata's template
*/
private String toSLSXml(URL spSingleLogoutServiceUrl, String spSingleLogoutServiceBinding) {
StringBuilder slsXml = new StringBuilder();
if (spSingleLogoutServiceUrl != null) {
slsXml.append("<md:SingleLogoutService Binding=\""+spSingleLogoutServiceBinding+"\"");
slsXml.append(" Location=\""+spSingleLogoutServiceUrl.toString()+"\"/>");
}
return slsXml.toString();
}
/**
* @return the metadata
*/
public final String getMetadataString() {
return metadataString;
}
/**
* Signs the metadata with the key/cert provided
*
* @param metadata
* SAML Metadata XML
* @param key
* Private Key
* @param cert
* x509 Public certificate
* @param signAlgorithm
* Signature Algorithm
*
* @return string Signed Metadata
* @throws XMLSecurityException
* @throws XPathExpressionException
*/
public static String signMetadata(String metadata, PrivateKey key, X509Certificate cert, String signAlgorithm) throws XPathExpressionException, XMLSecurityException
{
Document metadataDoc = Util.loadXML(metadata);
String signedMetadata = Util.addSign(metadataDoc, key, cert, signAlgorithm);
LOGGER.debug("Signed metadata --> " + signedMetadata);
return signedMetadata;
}
}