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:
- Signature is mathematically valid — the
xmlseclibrary checks this against the IdP’s certificate - Assertion conditions are met —
NotBeforeandNotOnOrAftertimestamps fall within acceptable clock skew (typically ±2 minutes) - Destination matches — the response’s
Destinationattribute 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-samlupdated — 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:
- Before the old cert expires, add the new certificate to your SP configuration alongside the old one
python3-samlsupports multiple IdP certificates — validations succeed if either cert matches- Once the IdP switches to the new cert, remove the old one from your config
- 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
AudienceRestrictionmust 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.
See Also
- Python Api Key Management Why apps use special passwords called API keys, and how to keep them safe — explained with a library card analogy
- Python Attribute Based Access Control How apps make fine-grained permission decisions based on who you are, what you're accessing, and the circumstances — explained with an airport analogy
- Python Audit Logging Learn Audit Logging with a clear mental model so your Python code is easier to trust and maintain.
- Python Bandit Security Scanning Why Bandit Security Scanning helps Python teams catch painful mistakes early without slowing daily development.
- Python Clickjacking Prevention How invisible website layers trick you into clicking the wrong thing, and how Python apps stop it