wiki:SAML2.0

ndg_saml

NDG SAML is a Python implementation of SAML 2.0 developed for the  NERC DataGrid and  ESG (Earth System Grid) security architecture. Both use a federated model for access control and SAML 2.0 was selected to provide the interfaces for attribute and authorisation decision queries. This implementation is based on the Java  OpenSAML code.

The code uses ElementTree for serialisation to and parsing from XML but an API makes it easily extendable to use other Python XML parsers if desired.

Releases

0.5.1

Minor fixes to 0.5 release:

  • Fix for date time parsing where no seconds fraction is present
  • fixed error message for InResponseTo ID check for Subject Query.

0.5

Adds support for SOAP binding to query/request profile with WSGI middleware for server side applications and client side package.

0.4

Initial release to PyPI includes SAML core for attribute and authorisation decision queries.

Installation

The code is available on PyPI at  http://pypi.python.org/pypi/ndg-saml/.

$ sudo easy_install ndg_saml

Example Code

These samples are taken direct from the unit tests.

Attribute Query

This example creates an attribute query:

from ndg.saml.saml2.core import (AttributeQuery, SAMLVersion, Issuer, Subject,
                                 NameID, Attribute, XSStringAttributeValue)
from uuid import uuid4
from datetime import datetime

attributeQuery = AttributeQuery()
attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
attributeQuery.id = str(uuid4())
attributeQuery.issueInstant = datetime.utcnow()

attributeQuery.issuer = Issuer()
attributeQuery.issuer.format = Issuer.X509_SUBJECT
attributeQuery.issuer.value = '/O=NDG/OU=BADC/CN=PolicyInformationPoint'
                
attributeQuery.subject = Subject()  
attributeQuery.subject.nameID = NameID()
attributeQuery.subject.nameID.format = NameID.X509_SUBJECT
attributeQuery.subject.nameID.value = '/O=NDG/OU=BADC/CN=PhilipKershaw'

# special case handling for 'LastName' attribute
emailAddressAttribute = Attribute()
emailAddressAttribute.name = "urn:esg:email:address"
emailAddressAttribute.nameFormat = "%s#%s" % (
                                XSStringAttributeValue.TYPE_NAME.namespaceURI,
                                XSStringAttributeValue.TYPE_NAME.localPart)

emailAddress = XSStringAttributeValue()
emailAddress.value = 'pjk@somewhere.ac.uk'
emailAddressAttribute.attributeValues.append(emailAddress)

attributeQuery.attributes.append(emailAddressAttribute)

# Convert to ElementTree representation
from ndg.saml.xml.etree import AttributeQueryElementTree, prettyPrint

elem = AttributeQueryElementTree.toXML(attributeQuery)

# Serialise as string
xmlOut = prettyPrint(elem)
print(xmlOut)

Produces:

<samlp:AttributeQuery xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0" IssueInstant="2010-06-01T13:19:50.690263Z" ID="1c15e748-0f74-41f1-848c-1fbdfeef2a06">
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName">/O=NDG/OU=BADC/CN=PolicyInformationPoint</saml:Issuer>
    <saml:Subject xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
        <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName">/O=NDG/OU=BADC/CN=PhilipKershaw</saml:NameID>
    </saml:Subject>
    <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="urn:esg:email:address" NameFormat="http://www.w3.org/2001/XMLSchema#string">
        <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">pjk@somewhere.ac.uk</saml:AttributeValue>
    </saml:Attribute>
</samlp:AttributeQuery>

Query/Request? Profile with SOAP Binding

This example shows how a simple Attribute Service can be created with WSGI together with a client call.

The server is configured using a Paste ini file. A pipeline arranges a custom filter AttributeAuthorityFilter, in this case upstream of the SAMLSoapAttributeInterfaceFilter available with this package. TestApp is a simple WSGI application to terminate the chain.

[pipeline:main]
pipeline = AttributeAuthorityFilter SAMLSoapAttributeInterfaceFilter TestApp

SAMLSoapAttributeInterfaceFilter uses the ndg.saml.saml2.binding.soap.server.wsgi.queryinterface:SOAPQueryInterfaceMiddleware class. The settings define the path for where the service will be mounted and the serialisation/parsing classes for handling the SAML XML content. the ElementTree? based ones have been used.

[filter:SAMLSoapAttributeInterfaceFilter]
paste.filter_app_factory = ndg.saml.saml2.binding.soap.server.wsgi.queryinterface:SOAPQueryInterfaceMiddleware.filter_app_factory
prefix = saml.
saml.mountPath = /attributeauthority
saml.queryInterfaceKeyName = attributeQueryInterface
saml.deserialise = ndg.saml.xml.etree:AttributeQueryElementTree.fromXML
saml.serialise = ndg.saml.xml.etree:ResponseElementTree.toXML

The saml.queryInterfaceKeyName setting above is important. This sets the keyword in environ that this class will use to get a callback function. This callback handles the actual query. It has the pattern:

attributeQuery(query, response)

where query is the parsed SAML query and response is the SAML response object which needs to be populated. For covenience, the caller set a number of values in this object such as for example the issuer instant, ID and the inResponseTo values. As the callback is read from environ, any custom middleware upstream of this filter can set it. This is what the AttributeAuthorityFilter does in this example. Looking at the details in the ini file, it sets an environ key named attributeQueryInterface which assigns to a callback function it creates:

[filter:AttributeAuthorityFilter]
# This filter is a container for a binding to a SOAP based interface to the
# Attribute Authority
paste.filter_app_factory = ndg.saml.test.binding.soap.test_attributeservice:TestAttributeServiceMiddleware
queryInterfaceKeyName = attributeQueryInterface

Looking at the ndg.saml.test.binding.soap.test_attributeservice:TestAttributeServiceMiddleware implementation, a factory method creates the callback (shortened for illustration):

    def attributeQueryFactory(self):
        """Makes the attribute query method"""
        
        def attributeQuery(query, response):
            """Attribute Query interface called by the next middleware in the 
            stack the SAML SOAP Query interface middleware instance
            (ndg.saml.saml2.binding.soap.server.wsgi.queryinterface.SOAPQueryInterfaceMiddleware)
            """
            <...> 
                
            assertion = Assertion()
            
            assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
            assertion.id = str(uuid4())
            assertion.issueInstant = response.issueInstant
            
            assertion.conditions = Conditions()
            assertion.conditions.notBefore = assertion.issueInstant
            assertion.conditions.notOnOrAfter = \
                assertion.conditions.notBefore + timedelta(seconds=60*60*8)
            
            assertion.subject = Subject()  
            assertion.subject.nameID = NameID()
            assertion.subject.nameID.format = query.subject.nameID.format
            assertion.subject.nameID.value = query.subject.nameID.value
            <...>
            
            response.assertions.append(assertion)
            response.status.statusCode.value = StatusCode.SUCCESS_URI        

The middleware's __call__ method invokes this method to set the callback in environ:

    def __call__(self, environ, start_response):
        environ[self.queryInterfaceKeyName] = self.attributeQueryFactory()
        return self._app(environ, start_response)

The client request is created using the SOAP binding and sent with SSL. ndg.saml.saml2.binding.soap.client.attributequery.AttributeQuerySslSOAPBinding is a wrapper class to perform this task taking SAML attribute query inputs and SSL settings. In this example, the subject is queried for an e-mail address. The connection is mutually authenticated with the client passing a certificate in the SSL handshake:

from ndg.soap.utils.etree import prettyPrint

from ndg.saml.saml2.core import Attribute, StatusCode
from ndg.saml.xml.etree import ResponseElementTree
from ndg.saml.saml2.binding.soap.client.attributequery import \
    AttributeQuerySslSOAPBinding

attributeQuery = AttributeQuerySslSOAPBinding()

attributeQuery.subjectID = "https://openid.localhost/philip.kershaw"
attributeQuery.subjectIdFormat = "urn:ndg:saml:openid"
attributeQuery.clockSkewTolerance = 2.
attributeQuery.issuerName = '/O=Site A/CN=Authorisation Service'

attribute = Attribute()
attribute.name = 'urn:ndg:saml:emailaddress'
attribute.friendlyName = 'emailAddress'
attribute.nameFormat = 'http://www.w3.org/2001/XMLSchema'

attributeQuery.queryAttributes.append(attribute)

attributeQuery.sslCACertDir = "./trustroots"
attributeQuery.sslCertFilePath = "./client.crt"
attributeQuery.sslPriKeyFilePath = "./client.key"

response = attributeQuery.send(uri='https://localhost:5443/attributeauthority')

# Convert back to ElementTree instance read for string output
samlResponseElem = ResponseElementTree.toXML(response)

print("Pretty print SAML Response ...")
print(prettyPrint(samlResponseElem))

The response is generated:

<samlp:Response ID="c61f8c17-8d2d-4b6a-ad7f-3acd6b4c3adc" InResponseTo="6fb92e0e-4570-4c03-8df9-6ee76ecfb03f" IssueInstant="2010-10-01T13:24:20.446046Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status><saml:Assertion ID="bcd28e09-1eb9-47f6-95c7-5b4133783aec" IssueInstant="2010-10-01T13:24:20.446046Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Subject><saml:NameID Format="urn:ndg:saml:openid">https://openid.localhost/philip.kershaw</saml:NameID></saml:Subject><saml:Conditions NotBefore="2010-10-01T13:24:20.446046Z" NotOnOrAfter="2010-10-01T21:24:20.446046Z" /><saml:AttributeStatement><saml:Attribute FriendlyName="emailAddress" Name="urn:ndg:saml:emailaddress" NameFormat="http://www.w3.org/2001/XMLSchema"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">pkershaw@somewhere.ac.uk</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>
Pretty print SAML Response ...
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" IssueInstant="2010-10-01T13:24:20.446046Z" InResponseTo="6fb92e0e-4570-4c03-8df9-6ee76ecfb03f" Version="2.0" ID="c61f8c17-8d2d-4b6a-ad7f-3acd6b4c3adc">
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk</saml:Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></samlp:StatusCode>
    </samlp:Status>
    <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" IssueInstant="2010-10-01T13:24:20.446046Z" ID="bcd28e09-1eb9-47f6-95c7-5b4133783aec">
        <saml:Subject>
            <saml:NameID Format="urn:ndg:saml:openid">https://openid.localhost/philip.kershaw</saml:NameID>
        </saml:Subject>
        <saml:Conditions NotOnOrAfter="2010-10-01T21:24:20.446046Z" NotBefore="2010-10-01T13:24:20.446046Z"></saml:Conditions>
        <saml:AttributeStatement>
            <saml:Attribute FriendlyName="emailAddress" Name="urn:ndg:saml:emailaddress" NameFormat="http://www.w3.org/2001/XMLSchema">
                <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">pkershaw@somewhere.ac.uk</saml:AttributeValue>
            </saml:Attribute>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>

Repository

 http://proj.badc.rl.ac.uk/svn/ndg-security/trunk/ndg_saml

Unit Tests

See the ndg.saml.test.test_saml module and the ndg.saml.test.binding.soap package.


Philip Kershaw 20/10/10