Skip to content

Instantly share code, notes, and snippets.

@Micrified
Last active December 1, 2024 23:34
Show Gist options
  • Save Micrified/f0aed0a0e68bf29d6e8d9f85fbc3f79d to your computer and use it in GitHub Desktop.
Save Micrified/f0aed0a0e68bf29d6e8d9f85fbc3f79d to your computer and use it in GitHub Desktop.
SAML2

SAML2 Implementation

Below you will find information about the SAML2 implementation. A short description of what is involved is described here. Put simply:

  • Identity Provider: An identity provider, or idp, is ESA's authoritative service for performing authentication on behalf of other services (it enables the SSO function).
  • Service Provider: A service provider, or sp, is any service that wants to be involved in the federated identity network. It performs a service for users, like offering a portal to manage Activites on.

In SAML2, both identity provider and service-provider need to configure themselves to know about one another

  • idp.xml: An XML file that describes the identity provider service. It's usually available at a public URL for services to read.
  • sp.xml: In this case, sp.xml refers to our service provider. This document must be given to the idp so that they can authorize your service to use their identity provider. It can be given by email, but is often also hosted somewhere on the service provider itself (typically on a URL that doubles as the EntityID (unique identifier) of the service). This is so that when (and if) encryption keys expire, an identity provider can just pull the latest embedded certificates from the publically available sp.xml path.

Finally, a few more useful keywords:

  • EntityID: This is just a unique string. It could be "banana". It just has to be unique. Normally, the website URL is used, and oftentimes the path to the sp.xml metadata file is used so that it can double in purpose as a url for fetching the latest sp.xml if needed.
  • Assertion Customer URL: Also called "Assertion customer service" or acs, this is just an endpoint that handles the parsing of replies from the idp to the sp. In other words, it's just a handle on your server for listening in and processing SAML2 protocol messages. It's handled by the library so you just need to route incoming traffic to it.

Request Form Fields

Business Justification

This request is necessary to enable the ESA Activities Portal developer server to behave as a service provider within the ESA's Azure Enterprise Appication Federation Service. It shall allow staff to internally evaluate and perform individual actions within the service.

Mandatory Documents

Below you'll find our service-provider (sp.xml) configuration, which must be given to IT (attach it as an XML file).

<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
                     validUntil="2024-12-03T23:31:37Z"
                     cacheDuration="PT604800S"
                     entityID="http://django-activities-demo.go.esa.int:8000/esa-sso/metadata/">
    <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
                                Location="http://django-activities-demo.go.esa.int:8000/logout/" />
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                                     Location="http://django-activities-demo.go.esa.int:8000/esa-sso/acs/"
                                     index="1" />
        
    </md:SPSSODescriptor>
</md:EntityDescriptor>

You can generate this XML anytime using an online tool like samltool. Here is what you need to know about the fields. Feel free to adjust some of the metadata fields, but note that the SAML2 specific authentication fields and certificate details should be left as-is:

Field Format Example
EntityID <domain-name>/esa-sso/metadata/ http://django-activities-demo.go.esa.int:8000/esa-sso/metadata/
ACS <domain-name>/esa-sso/acs/ http://django-activities-demo.go.esa.int:8000/esa-sso/acs/
Logout <domain-name>/logout/ http://django-activities-demo.go.esa.int:8000/logout/
NameID Leave default Leave default
SP X.509 Cert None None
AuthnRequestsSigned None None
WantAssertionsSigned None None
Organization Name string ESA
Organization DisplayName string Activities Portal
Organization URL string None
Technical Contact Name None None
Support Contact Name None None
Technical Contact Email None None
Support Contact Email None None
Private key to sign metadata None None
X.509 cert to sign metadata None None

Application Identifier

Termed here the EntityID: http://django-activities-demo.go.esa.int:8000/esa-sso/acs/

Assertion Custom URL (SAML) / Callback URL (Open ID)

Do not forget the trailing slash: http://django-activities-demo.go.esa.int:8000/esa-sso/acs/

Logout URL

Do not forget the trailing slash: http://django-activities-demo.go.esa.int:8000/logout/

Secure hash algorithm

SHA-256

Staff Application Coordinator

NA

Environment

Development

Claims Details

email, first_name, last_name,

Preferred authentication profile:

None


In order for any identity provider to work with a service provider, they need to exchange configuration files:

  • sp.xml: Provided to the identity provider. Tells it about the service
  • idp.xml: Provided to the service provider. Tells it about the identity provider.

--

This SAML2 implementation is done using django-saml2-auth, which requires the host have xmlsec1 library installed. Use:

apt-get install xmlsec1

for DEB-based distributions, or find an equivalent for your platform.


Adding SAML2 to Activities

Add the library to the list of installed apps:

# activities/activities/settings.py
INSTALLED_APPS = [
    ...
    'django_saml2_auth',
]

Provide the SAML2 configuration.

# activities/activities/settings.py
SAML2_AUTH = {
    # URL to the ESA identity provider
    'METADATA_AUTO_CONF_URL': 'http://localhost:8088/idp.xml',
    # Unique identifier for this service (need not be a valid url).
    'ENTITY_ID': 'http://localhost:8000/esa-sso/metadata/', 
    # Automatic mapping from identity provider assertion to local user model fields
    'ATTRIBUTES_MAP': {
        'username': 'uid',
    },
    # Security settings.
    'AUTHN_REQUESTS_SIGNED': False,
    'WANT_ASSERTIONS_SIGNED': False,
    'WANT_RESPONSE_SIGNED': False,
    'TOKEN_REQUIRED': False,
    # Debugger settings
    'DEBUG': True,
}

Add your single-sign-on SSO library path hooks next:

# activities/activities/urls.py
from django.urls import path, include
from django_saml2_auth.views import signin

urlpatterns = [
    ...,
    # Built-in paths by the library 
    path("esa-sso/", include("django_saml2_auth.urls")),
    # The local URL you send the user to that will redirect them to the identity-provider login
    path("esa/login", signin, name="esa-sso-login"),
    # The URL where you will host your metadata file, to allow automatic fetching by IDP
    # (happens to be the same as the Entity-ID for convenience)
    path("esa-sso/metadata", MetadataView)
]

Add the link to the SSO signin on the login page:

<h1>Login Providers</h1>
    <div class="login-container">
        <div class="login-option">
           <a href="{% url 'esa-sso-login' %}">ESA</a>.  # <-- New
            <p>For staff</p>
        </div>
        <div class="login-option">
            <a href="/osip/login">OSIP</a>
            <p>For external parties</p>
        </div>
</div> 

Testing SAML2 login

Create a test identity provider. In order for any identity provider to work with a service provider, they need to exchange configuration files:

  • sp.xml: Provided to the identity provider. Tells it about the service
  • idp.xml: Provided to the service provider. Tells it about the identity provider.

In our case, our identity provider will just be a simple server that serves a generated idp.xml on a web address. This address is exactly the same that should be configured in the SAML2 METADATA_AUTO_CONF_URL field.

# Make directory for our identity provider service.
cd activities/website/
mkdir idp && cd idp
# Fetch an example public/private key for the idp. Suitable for testing
mkdir pki
curl -o pki/mycert.pem https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/pki/mycert.pem
curl -o pki/mykey.pem https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/pki/mykey.pem
# Fetch configuration files to generate our sp.xml metadata file
curl -o service_conf.py https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/sp-wsgi/service_conf.py.example
curl -o sp_conf.py https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/sp-wsgi/sp_conf.py.example
# Use stream editor to replace some fields. Feel free to also open "sp_conf.py" and add adjustments manually
sed -e 's|../idp2/idp.xml|idp.xml|' \
    -e 's|8087|8000|' \                                         # <-- Port of our service provider
    -e 's|acs/post|esa-sso/acs/|' \                             # <-- Path to 'acs' in our django_saml2_auth.urls path. 
    -e 's|slo/redirect|logout/|' \                              # <-- Where we want logout to go. 
    -e 's|slo/post|logout/|' \                         # <-- Where we want logout to go. 
    -e 's|%ssp.xml" % (BASE, ""),|esa-sso/metadata/" % BASE,|' \.   # <-- Entity ID of our service provider
    sp_conf.py > sp_conf.tmp && mv sp_conf.tmp sp_conf.py

# Generate the service provide metadata
curl -o idp.xml https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/idp.xml
make_metadata sp_conf.py > sp.xml

# Download a configuration script for setting up the IDP server:
curl -o idp_conf.py https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/idp_conf.py.example
curl -o idp.py https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/idp.py
curl -o idp_user.py https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/idp_user.py

# Adjust the IDP server settings to read our service-provider XML file and to not require HTTPS
sed -e 's|../sp-wsgi/sp.xml|sp.xml|' \
    -e 's|HTTPS = True|HTTPS = False|' \
    idp_conf.py > idp_conf.tmp && mv idp_conf.tmp idp_conf.py
    
# Add some templates for the identity provider to serve as a login form
mkdir htdocs
curl -o htdocs/root.mako https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/templates/root.mako
curl -o htdocs/login.mako https://raw.githubusercontent.com/IdentityPython/pysaml2/master/example/idp2/htdocs/login.mako

# Add the dependencies to use them
python3 -m pip install cherrypy mako legacy-cgi

# Generate the idp.xml
make_metadata idp_conf.py > idp.xml

You may run the server with: python idp.py idp_conf

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment