SAML Authentication in Python — Deep Dive

Setting up python3-saml

The python3-saml library requires two configuration files and optionally an advanced settings file. Installation pulls in xmlsec, which needs system-level XML security libraries (libxmlsec1-dev on Debian/Ubuntu).

# Install dependencies
# pip install python3-saml

from onelogin.saml2.auth import OneLogin_Saml2_Auth

def prepare_flask_request(request):
    """Adapt Flask request for python3-saml."""
    return {
        "https": "on" if request.scheme == "https" else "off",
        "http_host": request.host,
        "script_name": request.path,
        "get_data": request.args.copy(),
        "post_data": request.form.copy(),
    }

SP settings (settings.json) define your application’s entity ID, ACS endpoint, and optional signing/encryption certificates:

{
  "sp": {
    "entityId": "https://myapp.example.com/saml/metadata",
    "assertionConsumerService": {
      "url": "https://myapp.example.com/saml/acs",
      "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    },
    "singleLogoutService": {
      "url": "https://myapp.example.com/saml/sls",
      "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    },
    "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
    "x509cert": "",
    "privateKey": ""
  },
  "idp": {
    "entityId": "https://idp.example.com/saml/metadata",
    "singleSignOnService": {
      "url": "https://idp.example.com/saml/sso",
      "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
    },
    "x509cert": "MIIDpDCCAoy..."
  }
}

Flask integration: login and ACS endpoints

from flask import Flask, request, redirect, session, make_response

app = Flask(__name__)
app.secret_key = "change-me-in-production"

@app.route("/saml/login")
def saml_login():
    req = prepare_flask_request(request)
    auth = OneLogin_Saml2_Auth(req, custom_base_path="/path/to/saml/")
    return redirect(auth.login())

@app.route("/saml/acs", methods=["POST"])
def saml_acs():
    req = prepare_flask_request(request)
    auth = OneLogin_Saml2_Auth(req, custom_base_path="/path/to/saml/")
    auth.process_response()
    errors = auth.get_errors()

    if errors:
        return f"SAML Error: {', '.join(errors)}", 400

    if not auth.is_authenticated():
        return "Authentication failed", 401

    # Extract user attributes
    session["saml_name_id"] = auth.get_nameid()
    session["saml_session_index"] = auth.get_session_index()
    attributes = auth.get_attributes()
    session["user_email"] = attributes.get("email", [None])[0]
    session["user_groups"] = attributes.get("groups", [])

    relay_state = request.form.get("RelayState", "/")
    return redirect(relay_state)

Signature validation internals

SAML security rests on XML digital signatures. The IdP signs the assertion (or the entire response) with its private key. Your SP validates against the IdP’s public certificate.

Three things must hold for a valid response:

  1. Signature is mathematically valid — the xmlsec library checks this against the IdP’s certificate
  2. Assertion conditions are metNotBefore and NotOnOrAfter timestamps fall within acceptable clock skew (typically ±2 minutes)
  3. Destination matches — the response’s Destination attribute matches your ACS URL exactly
# Advanced settings enforce strict validation
# advanced_settings.json
{
  "security": {
    "wantAssertionsSigned": true,
    "wantMessagesSigned": false,
    "wantNameIdEncrypted": false,
    "wantAssertionsEncrypted": false,
    "signMetadata": true,
    "requestedAuthnContext": false,
    "rejectUnsolicitedResponsesWithInResponseTo": true
  }
}

Preventing XML signature wrapping attacks

The most dangerous SAML vulnerability is XML Signature Wrapping (XSW). An attacker takes a legitimately signed assertion and wraps it inside a crafted XML structure. The signature validates against the original assertion, but the SP extracts attributes from a different, attacker-controlled element.

Defenses in python3-saml:

  • Strict schema validation — reject responses that don’t match the SAML 2.0 schema
  • ID reference checking — ensure the signature’s Reference URI points to the actual assertion being processed
  • wantAssertionsSigned: true — reject unsigned assertions even if the outer response is signed
  • Keep python3-saml updated — the library has patched multiple XSW variants

Handling clock skew

IdP and SP servers rarely have perfectly synchronized clocks. SAML assertions include NotBefore and NotOnOrAfter conditions. If your server’s clock is even slightly off, valid assertions get rejected.

# python3-saml allows configuring clock tolerance
# In advanced_settings.json:
{
  "security": {
    "rejectedTimes": 120  # seconds of allowed clock skew
  }
}

Use NTP on all servers. A 2-minute tolerance handles most drift, but larger values increase replay attack windows.

Single Logout (SLO)

SAML supports coordinated logout across all SPs. When a user logs out of one app, the IdP sends LogoutRequest messages to every SP that received assertions during the session.

@app.route("/saml/logout")
def saml_logout():
    req = prepare_flask_request(request)
    auth = OneLogin_Saml2_Auth(req, custom_base_path="/path/to/saml/")
    return redirect(auth.logout(
        name_id=session.get("saml_name_id"),
        session_index=session.get("saml_session_index"),
    ))

@app.route("/saml/sls", methods=["GET", "POST"])
def saml_sls():
    req = prepare_flask_request(request)
    auth = OneLogin_Saml2_Auth(req, custom_base_path="/path/to/saml/")
    auth.process_slo()
    errors = auth.get_errors()
    if not errors:
        session.clear()
    return redirect("/")

SLO is notoriously fragile in practice. If one SP is down, the logout chain breaks. Many deployments skip SLO and rely on short session timeouts instead.

Django integration with djangosaml2

For Django apps, djangosaml2 provides middleware and views that integrate with Django’s authentication system:

# settings.py
INSTALLED_APPS = [
    # ...
    "djangosaml2",
]

AUTHENTICATION_BACKENDS = [
    "djangosaml2.backends.Saml2Backend",
    "django.contrib.auth.backends.ModelBackend",
]

SAML_SESSION_COOKIE_NAME = "saml_session"
SAML_CREATE_UNKNOWN_USER = True

import saml2
SAML_CONFIG = {
    "entityid": "https://myapp.example.com/saml/metadata/",
    "service": {
        "sp": {
            "name": "My Django App",
            "endpoints": {
                "assertion_consumer_service": [
                    ("https://myapp.example.com/saml/acs/", saml2.BINDING_HTTP_POST),
                ],
            },
            "required_attributes": ["email"],
            "optional_attributes": ["givenName", "sn"],
        },
    },
}

Certificate rotation without downtime

IdP certificates expire. When they do, assertion validation breaks unless you’ve prepared. The approach:

  1. Before the old cert expires, add the new certificate to your SP configuration alongside the old one
  2. python3-saml supports multiple IdP certificates — validations succeed if either cert matches
  3. Once the IdP switches to the new cert, remove the old one from your config
  4. Publish updated SP metadata so the IdP picks up any SP certificate changes

Production checklist

  • Always use HTTPS — SAML responses travel through the browser via POST; TLS prevents interception
  • Validate InResponseTo — match it against the AuthnRequest ID you stored in the session to prevent unsolicited response injection
  • Set wantAssertionsSigned: true — never accept unsigned assertions
  • Enforce audience restriction — the assertion’s AudienceRestriction must contain your SP entity ID
  • Implement replay detection — cache assertion IDs and reject duplicates within the validity window
  • Log assertion details — capture NameID, session index, and timestamps for audit trails without logging the full XML (which may contain sensitive attributes)

The one thing to remember: SAML integration in Python means correctly validating XML signatures, enforcing time conditions, and protecting against XML wrapping attacks — the library handles the crypto, but you own the configuration and security hardening.

pythonsecurityauthenticationenterprise

See Also