Inbal explains how to build a SAML service provider using PySAML2
by Inbal Zeligner

Inbal explains how to build a SAML service provider using PySAML2

Today we’re spotlighting Inbal Zelinger, Software Engineer at Twingate. Inbal joined Twingate last year after starting her career at Nice Ltd. She supports Twingate’s JumpCloud integration, and worked with her fellow #Gaters to build support for SAML SSO. At Twingate, we are obsessed with simplifying security and believe sharing our insights with teammates and leaving room for intellectual debate is the key to accomplishing this goal. In the guide below, Inbal shares useful findings that she and the team uncovered while working on the SAML SSO project.

Introduction

The Security Assertion Markup Language (SAML) is an open standard for exchanging authentication and authorization data between parties. It is primarily used to enable single sign-on (SSO). SSO is pretty essential for businesses these days. In a world where each organization works with a wide range of different applications, the SSO concept simplifies the organization’s account management by allowing them to manage only one identity per user. From the user’s point of view it saves precious time and the need to keep up with many different accounts and passwords.

In this article, I will give a primer on how SAML integration between a service provider (SP) and an identity provider (IdP) works. I will then dive into how to build the integration for a multi-tenant service provider using Django and PySAML2. I will share some inner workings of PySAML2 that their documentation doesn’t cover: how to define a metadata loader and use it to set up a

Saml2Client
.

Terminologies

Before I get into how SAML integration works, let’s go over some common terminologies.

  • SAML 2.0 is an XML based standard that uses security tokens containing assertions to pass information about a subject (usually the user) between an Identity Provider (SAML authority) and a Service Provider (SAML consumer). SAML won’t send passwords over the web for every login; it uses the tokens instead. An important use case that SAML addresses is web-browser single sign-on (SSO).
  • Single Sign-On (SSO) is an authentication system that allows users to federate the authentication between single authority (usually an Identity provider) and several apps. For example, you can log into JumpCloud and then access Twingate without authenticating again. There are a several open standard for SSO such as OpenID Connect and SAML.
  • Identity Provider (IdP) is the entity that creates and maintains identity of users. It also provides authentication service.
  • Service Provider (SP) provides some specific functionalities to end users. For example, Twingate as a service provider provides Zero-trust access to private resources. An SP often relies on an IdP for user authentication.
  • Trust relationship - In SAML SSO, the IdP and the SP communicates via an intermediary, often a browser. As such, the IdP and the SP must set up a trust relationship initially. The SP trusts the IdP to authenticate the user. The IdP trusts the SP and sends it the subject (user) details. A trust relationship between SP and IdP can be achieved by exchanging a SAML metadata document with one another. Another way to achieve the trust relationship is via manual setup, which is common in practice. An SSO app is configured in the IdP. The IdP generates a SAML metadata file that can then be uploaded to the SP.

SAML SSO integration as a Service Provider

User flow

SAML defines 2 possible SSO integration: SP-initiated and IdP-initiated. Here is the user flow for SP-initiated flow.

Logging into an application using SP-initiated SAML authentication

Configuration

In order to configure SAML app there are several configurations that we need to consider:

  • Identity Provider
    • IdP entity ID
    • Single Sign-On Service URL (see [SMALProf] section 4.1.3). This endpoint is where the user is redirected to on the IdP to do the authentication e.g. in JumpCloud,
      https://sso.jumpcloud.com/saml2/saml2
    • IdP X509 public certificate - Used by the SP to verify the signature of the SAML assertions and/or response.
  • Service Provider
    • SP entity ID
    • ACS endpoint - Assertion Consumer Service URL (see [SMALProf] section 4.1.3). This endpoint on the SP is where the IdP redirects the user back to after authentication with the assertions.

There are other settings when configuring a SAML app; however, these can vary across different IdPs and we do not need to store this information on the service provider. Some settings are:

  • Whether the IdP should sign the response.
  • Attributes that the IdP should include in the response (so we can create new user).

Service provider app implementation

In this demo we will build a service provider app (Sample_SP_app) using PySAML2, that will fit to a multi-tenant environment in which each tenant has a different SSO app setting. If you wish to jump directly to the code, you can find it here.

Our service provider is a fictional service, Sample_SP_app.

PySAML2 is a python implementation of SAML 2.0 standard. It contains all necessary pieces for building a SAML service provider. It supports parsing IdP metadata file and can be used as an IdP or SP. However, it does not have a simple way to load multiple IdP and SP metadata. After digging in, we figure out how to extend its implementation.

In order to setup a SAML integration, we need to provide our SP app the IdP metadata and create a trust relationship. PySAML2 describes a few ways to do this in the documentations e.g: local directory, a remote URL, etc’ (see docs). Unfortunately, these options are not good enough for us because we are working in a multi-tenant environment where each tenant has its own SAML integration and its own SSO app on the IdP. Each app has a different IdP entity ID, SSO URL and IdP’s X509 public certificate.

Create custom metadata loader

By looking into the PySAML2 internals, we were able to learn that it has an extensible metadata loading mechanism. Each loader is a subclass of InMemoryMetaData. We need to implement the

load()
method and create an
EntityDescriptor
instance. You might wonder what an
EntityDescriptor
is. As it turns out, the root element of a SAML metadata document is an
<EntityDescriptor>
(as defined in the SAML Metadata specification). Hence, in PySAML2 a metadata is represented by an
EntityDescriptor
instance. The built-in
MetaDataFile
, for example, loads the metadata file content and convert it to an
EntityDescriptor
. In our custom loader, we would build the
EntityDescriptor
from the information we saved in the database (each tenant has its own record). For simplicity of the demo, we assume this information is available in memory.

How a metadata is represented in PySAML2.

Configure the
metadata
representation in the
saml2.config.Config

Here is how we tell PySAML2 to use our custom metadata in the settings dictionary.

from dataclasses import dataclass

from saml2.client import Saml2Client
from saml2.config import Config

@dataclass
class IdPConfig:
    entity_id: str
    single_sign_on_url: str
    x509_cert: str

    def __hash__(self):
        return hash(self.entity_id)


def saml_client():
    saml_settings = {
			#...
"metadata": [
					{
                "class": "sample_sp.views.MetaDataIdP",
                "metadata": [
                    (
                        IdPConfig(
                            entity_id="jumpcloud/twingate/sample-sp",
                            single_sign_on_url="https://sso.jumpcloud.com/saml2/saml2",
                            x509_cert="<change_it>"
                        ),
                    )
                ],
            }
        ],
				#...
    }

    config = Config()
    config.load(saml_settings)

    return Saml2Client(config=config)

Django views for SAML endpoints

The service provider must implement 2 views:

  • Login view - user first calls this endpoint to initiate the SSO flow. The SP return a redirect response to the IdP with a
    AuthNRequest
    payload
  • Assertion consumer service (ACS) view - After the user completes the authentication with the IdP, the IdP will redirect the user to this endpoint using POST binding. In simple terms, POST binding is a mechanism to do the redirection using HTML form and HTTP POST request. This allows the IdP to redirect the user back to the SP with the assertions payload, which is usually too big to fit in a typical HTTP redirect.

The implementation of these 2 views in Django are straightforward. We just need to use the

Saml2Client
created from the previous step:

from django.http import JsonResponse
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from saml2 import BINDING_HTTP_POST, md, xmldsig, BINDING_HTTP_REDIRECT, samlp
from saml2.mdstore import InMemoryMetaData

from apps.service_provider.saml import saml_client, IdPConfig


def login(request):
    client = saml_client()
    request_id, info = client.prepare_for_authenticate()
    redirect_url = dict(info["headers"])["Location"]

    return redirect(redirect_url)


@csrf_exempt
def assertion_consumer_service(request):
    client = saml_client()
    authn_response = client.parse_authn_request_response(
        request.POST["SAMLResponse"], BINDING_HTTP_POST
    )
    session_info = authn_response.session_info()
    session_info["name_id"] = str(session_info["name_id"])

    return JsonResponse(session_info)

At this point, we are ready to create our own SAML app in an IdP and integrate it with our Django server. You can take a look at the README in the demo repo for step-by-step instructions of how to do that with JumpCloud as the IdP.


Featured Articles