Source code for onelogin.saml2.metadata
# -*- coding: utf-8 -*-
""" OneLogin_Saml2_Metadata class
MIT License
Metadata class of Python Toolkit.
"""
from time import gmtime, strftime, time
from datetime import datetime
from defusedxml.minidom import parseString
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils
[docs]class OneLogin_Saml2_Metadata(object):
"""
A class that contains methods related to the metadata of the SP
"""
TIME_VALID = 172800 # 2 days
TIME_CACHED = 604800 # 1 week
[docs] @staticmethod
def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None):
"""
Builds the metadata of the SP
:param sp: The SP data
:type sp: string
:param authnsign: authnRequestsSigned attribute
:type authnsign: string
:param wsign: wantAssertionsSigned attribute
:type wsign: string
:param valid_until: Metadata's expiry date
:type valid_until: string|DateTime|Timestamp
:param cache_duration: Duration of the cache in seconds
:type cache_duration: int|string
:param contacts: Contacts info
:type contacts: dict
:param organization: Organization info
:type organization: dict
"""
if valid_until is None:
valid_until = int(time()) + OneLogin_Saml2_Metadata.TIME_VALID
if not isinstance(valid_until, basestring):
if isinstance(valid_until, datetime):
valid_until_time = valid_until.timetuple()
else:
valid_until_time = gmtime(valid_until)
valid_until_str = strftime(r'%Y-%m-%dT%H:%M:%SZ', valid_until_time)
else:
valid_until_str = valid_until
if cache_duration is None:
cache_duration = OneLogin_Saml2_Metadata.TIME_CACHED
if not isinstance(cache_duration, basestring):
cache_duration_str = 'PT%sS' % cache_duration # 'P'eriod of 'T'ime x 'S'econds
else:
cache_duration_str = cache_duration
if contacts is None:
contacts = {}
if organization is None:
organization = {}
str_attribute_consuming_service = ''
if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
attr_cs_desc_str = ''
if "serviceDescription" in sp['attributeConsumingService']:
attr_cs_desc_str = """ <md:ServiceDescription xml:lang="en">%s</md:ServiceDescription>
""" % sp['attributeConsumingService']['serviceDescription']
requested_attribute_data = []
for req_attribs in sp['attributeConsumingService']['requestedAttributes']:
req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = ''
req_attr_aux_str = ' />'
if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']:
req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat']
if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
req_attr_friendlyname_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false'
if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
if isinstance(req_attribs['attributeValue'], basestring):
req_attribs['attributeValue'] = [req_attribs['attributeValue']]
req_attr_aux_str = ">"
for attrValue in req_attribs['attributeValue']:
req_attr_aux_str += """
<saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">%(attributeValue)s</saml:AttributeValue>""" % \
{
'attributeValue': attrValue
}
req_attr_aux_str += """
</md:RequestedAttribute>"""
requested_attribute = """ <md:RequestedAttribute Name="%(req_attr_name)s"%(req_attr_nameformat_str)s%(req_attr_friendlyname_str)s%(req_attr_isrequired_str)s%(req_attr_aux_str)s""" % \
{
'req_attr_name': req_attribs['name'],
'req_attr_nameformat_str': req_attr_nameformat_str,
'req_attr_friendlyname_str': req_attr_friendlyname_str,
'req_attr_isrequired_str': req_attr_isrequired_str,
'req_attr_aux_str': req_attr_aux_str
}
requested_attribute_data.append(requested_attribute)
str_attribute_consuming_service = """ <md:AttributeConsumingService index="1">
<md:ServiceName xml:lang="en">%(service_name)s</md:ServiceName>
%(attr_cs_desc)s%(requested_attribute_str)s
</md:AttributeConsumingService>
""" % \
{
'service_name': sp['attributeConsumingService']['serviceName'],
'attr_cs_desc': attr_cs_desc_str,
'requested_attribute_str': '\n'.join(requested_attribute_data)
}
sls = ''
if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']:
sls = """ <md:SingleLogoutService Binding="%(binding)s"
Location="%(location)s" />\n""" % \
{
'binding': sp['singleLogoutService']['binding'],
'location': sp['singleLogoutService']['url'],
}
str_authnsign = 'true' if authnsign else 'false'
str_wsign = 'true' if wsign else 'false'
str_organization = ''
if len(organization) > 0:
organization_names = []
organization_displaynames = []
organization_urls = []
for (lang, info) in organization.items():
organization_names.append(""" <md:OrganizationName xml:lang="%s">%s</md:OrganizationName>""" % (lang, info['name']))
organization_displaynames.append(""" <md:OrganizationDisplayName xml:lang="%s">%s</md:OrganizationDisplayName>""" % (lang, info['displayname']))
organization_urls.append(""" <md:OrganizationURL xml:lang="%s">%s</md:OrganizationURL>""" % (lang, info['url']))
org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls)
str_organization = """ <md:Organization>
%(org)s
</md:Organization>\n""" % {'org': org_data}
str_contacts = ''
if len(contacts) > 0:
contacts_info = []
for (ctype, info) in contacts.items():
contact = """ <md:ContactPerson contactType="%(type)s">
<md:GivenName>%(name)s</md:GivenName>
<md:EmailAddress>%(email)s</md:EmailAddress>
</md:ContactPerson>""" % \
{
'type': ctype,
'name': info['givenName'],
'email': info['emailAddress'],
}
contacts_info.append(contact)
str_contacts = '\n'.join(contacts_info) + '\n'
metadata = u"""<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
%(valid)s
%(cache)s
entityID="%(entity_id)s">
<md:SPSSODescriptor AuthnRequestsSigned="%(authnsign)s" WantAssertionsSigned="%(wsign)s" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
%(sls)s <md:NameIDFormat>%(name_id_format)s</md:NameIDFormat>
<md:AssertionConsumerService Binding="%(binding)s"
Location="%(location)s"
index="1" />
%(attribute_consuming_service)s </md:SPSSODescriptor>
%(organization)s%(contacts)s</md:EntityDescriptor>""" % \
{
'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '',
'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '',
'entity_id': sp['entityId'],
'authnsign': str_authnsign,
'wsign': str_wsign,
'name_id_format': sp['NameIDFormat'],
'binding': sp['assertionConsumerService']['binding'],
'location': sp['assertionConsumerService']['url'],
'sls': sls,
'organization': str_organization,
'contacts': str_contacts,
'attribute_consuming_service': str_attribute_consuming_service
}
return metadata
[docs] @staticmethod
def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA256, digest_algorithm=OneLogin_Saml2_Constants.SHA256):
"""
Signs the metadata with the key/cert provided
:param metadata: SAML Metadata XML
:type metadata: string
:param key: x509 key
:type key: string
:param cert: x509 cert
:type cert: string
:param sign_algorithm: Signature algorithm method
:type sign_algorithm: string
:param digest_algorithm: Digest algorithm method
:type digest_algorithm: string
:returns: Signed Metadata
:rtype: string
"""
return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm)
[docs] @staticmethod
def add_x509_key_descriptors(metadata, cert=None, add_encryption=True):
"""
Adds the x509 descriptors (sign/encryption) to the metadata
The same cert will be used for sign/encrypt
:param metadata: SAML Metadata XML
:type metadata: string
:param cert: x509 cert
:type cert: string
:param add_encryption: Determines if the KeyDescriptor[use="encryption"] should be added.
:type add_encryption: boolean
:returns: Metadata with KeyDescriptors
:rtype: string
"""
if cert is None or cert == '':
return metadata
try:
xml = parseString(metadata.encode('utf-8'), forbid_dtd=True, forbid_entities=True, forbid_external=True)
except Exception as e:
raise Exception('Error parsing metadata. ' + e.message)
formated_cert = OneLogin_Saml2_Utils.format_cert(cert, False)
x509_certificate = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Certificate')
content = xml.createTextNode(formated_cert)
x509_certificate.appendChild(content)
key_data = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:X509Data')
key_data.appendChild(x509_certificate)
key_info = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'ds:KeyInfo')
key_info.appendChild(key_data)
key_descriptor = xml.createElementNS(OneLogin_Saml2_Constants.NS_DS, 'md:KeyDescriptor')
entity_descriptor = xml.getElementsByTagName('md:EntityDescriptor')[0]
sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0]
sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
if add_encryption:
sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
signing = xml.getElementsByTagName('md:KeyDescriptor')[0]
signing.setAttribute('use', 'signing')
signing.appendChild(key_info)
signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
if add_encryption:
encryption = xml.getElementsByTagName('md:KeyDescriptor')[1]
encryption.setAttribute('use', 'encryption')
encryption.appendChild(key_info.cloneNode(True))
encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
return xml.toxml()