SAML-Toolkits/python3-saml
TL;DR
- Implement SAML in Python for Enterprise B2B SaaS applications.
- Resolve complex xmlsec1 C-library dependencies across different operating systems.
- Configure python3-saml for production-grade authentication flows.
- Avoid vendor lock-in with managed SSO alternatives.
- Optimize build processes for Docker and modern Python environments.
If you're building B2B SaaS, you eventually hit a wall. It usually happens when a potential Enterprise customer leans in and says, "We love the product, but we can't sign the contract unless it integrates with our Okta instance."
That wall is SAML (Security Assertion Markup Language).
While most of us prefer OIDC for its JSON-friendly modernity, SAML remains the "COBOL of Auth." It is ancient, verbose, and absolutely non-negotiable for the Fortune 500.
You generally have two choices here. You can pay a premium for a managed wrapper like Auth0 or WorkOS to make the headache go away. Or, you can roll up your sleeves and build it yourself using the industry standard: python3-saml.
This library is the professional’s choice for a reason. It gives you granular control over the authentication flow without the "black box" vendor lock-in. But let's be honest—it has scars. It demands low-level C dependencies, strict configuration, and some architectural gymnastics if you're running modern async frameworks like FastAPI.
If you’d rather skip the build and grab a managed solution, check out our Enterprise SSO Solutions. But if you’re ready to own your stack, this is how you implement python3-saml in a 2026 production environment without losing your mind.
Prerequisite: Conquering "Dependency Hell" (xmlsec1)
Before you write a single line of Python, you have to get the underlying C libraries to compile. This is where most developers rage-quit. python3-saml relies on dm.xmlsec.binding, which is a wrapper around libxml2 and libxmlsec1.
If you just try to pip install python3-saml without prepping your OS, your build will fail with a wall of cryptic GCC errors. Here is how to fix it on the platforms that actually matter.
Debian / Ubuntu / Official Python Docker Images
Most production environments run on Debian-based images. The trick here is that you need the development headers, not just the binaries.
apt-get update && apt-get install -y \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl
Docker (Alpine Linux)
In 2026, we all love lightweight containers. But Alpine is notoriously difficult with C extensions. It is possible, but you have to force apk to pull the build base.
RUN apk add --no-cache \
xmlsec-dev \
libxml2-dev \
libxmlsec1-dev \
gcc \
musl-dev \
libffi-dev
macOS (Apple Silicon)
Developing locally on an M3/M4 chip? Homebrew installs libxmlsec1, but Python often acts like it can't see the OpenSSL bindings.
brew install libxmlsec1
# You may need to export flags if pip fails to link:
export CPPFLAGS="-I/opt/homebrew/include"
export LDFLAGS="-L/opt/homebrew/lib"
pip install python3-saml
The Architecture: How the Handshake Actually Works
To debug python3-saml, you need to understand that the library is essentially just a translator. It takes an incoming XML POST request, cryptographically verifies it, and spits out a Python dictionary of user attributes.
Here is the flow you are building. You are the Service Provider (SP). The customer's Okta/Google setup is the Identity Provider (IdP).
For the official source code and latest release notes, always refer to the SAML-Toolkits/python3-saml GitHub repository. It remains the source of truth for patch updates.
Configuration: The Art of settings.json
The settings.json file is the heart of this library. A misconfigured setting here doesn't just break login; it opens you up to XML Signature Wrapping (XSW) attacks.
In 2026, "it works" isn't enough. It has to be secure.
The "Strict" Mandate
{
"strict": true,
"debug": true,
...
}
Never run production with strict: false. I don't care if it fixes your error. This parameter forces the library to validate that the XML response matches the request ID and that the signature covers the critical assertions. We strictly follow the SAML Security Cheat Sheet by OWASP to ensure our configuration prevents these common bypasses.
The Algorithm Shift (SHA-256)
SHA-1 is cryptographically broken and dead. Modern IdPs (Entra ID, Okta) will scream at you if you use it. You must explicitly define SHA-256 in your security settings.
"security": {
"nameIdEncrypted": false,
"authnRequestsSigned": true,
"logoutRequestSigned": true,
"logoutResponseSigned": true,
"signMetadata": true,
"wantMessagesSigned": true,
"wantAssertionsSigned": true,
"wantNameId": true,
"wantNameIdEncrypted": false,
"wantAssertionsEncrypted": true,
"signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256"
}
Implementation: The Code
The library was originally designed for the synchronous world of Django and Flask. We will cover the standard implementation first, and then tackle the modern FastAPI integration.
Initialization
You need to construct a OneLogin_Saml2_Auth object for every single request. This object requires the request data (headers, query parameters, and body) to be formatted in a specific way.
from onelogin.saml2.auth import OneLogin_Saml2_Auth
def init_saml_auth(request_data):
# request_data is a dict containing specific keys:
# https, http_host, script_name, get_data, post_data
return OneLogin_Saml2_Auth(request_data, custom_base_path='/path/to/settings/')
The "Alpha" Section: FastAPI Integration
This is where most documentation fails you. python3-saml is a blocking, synchronous library. If you call auth.process_response() directly inside an async def FastAPI endpoint, you will block the event loop. This degrades performance for your entire API, not just the login route.
To fix this, you must offload the heavy XML processing to a thread pool. FastAPI (via Starlette) provides run_in_threadpool for exactly this purpose.
from fastapi import Request, HTTPException
from fastapi.concurrency import run_in_threadpool
from onelogin.saml2.auth import OneLogin_Saml2_Auth
async def acs_endpoint(request: Request):
# 1. Prepare the request data synchronously
req_info = await prepare_fastapi_request(request)
<span class="hljs-comment"># 2. Initialize Auth</span>
auth = OneLogin_Saml2_Auth(req_info, custom_base_path=SETTINGS_PATH)
<span class="hljs-comment"># 3. CRITICAL: Run the heavy XML processing in a thread</span>
<span class="hljs-comment"># This prevents blocking the async event loop</span>
<span class="hljs-keyword">await</span> run_in_threadpool(auth.process_response)
errors = auth.get_errors()
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> errors:
<span class="hljs-keyword">if</span> auth.is_authenticated():
user_data = auth.get_attributes()
<span class="hljs-comment"># Create your JWT/Session here</span>
<span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"authorized"</span>, <span class="hljs-string">"user"</span>: user_data}
<span class="hljs-comment"># Handle errors</span>
reason = auth.get_last_error_reason()
<span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">403</span>, detail=<span class="hljs-string">f"SAML Error: <span class="hljs-subst">{reason}</span>"</span>)
Implementing the endpoint is step one, but ensure you follow broader Django/Flask Security Best Practices regarding session management once the user is actually inside your system.
Troubleshooting: Why is my Assertion Invalid?
The error invalid_response is the generic "Check Engine" light of SAML. It tells you absolutely nothing. To fix it, you need to look closer.
1. Clock Skew
Servers drift. If your server time is 2 seconds ahead of the IdP's server time, the assertion might arrive "from the future" or "expired."
- Fix: Use the
accepted_time_diffparameter in yourinitor settings to allow a 60-120 second window.
2. Audience Mismatch
This is the most common configuration error. The IdP sends the XML intended for a specific EntityID (usually your metadata URL). If your settings.json has sp.entityId set to https://myapp.com/metadata but the IdP is sending it to https://myapp.com, the library will reject it. These strings must match character-for-character.
3. Certificate Rotation
If login works on Monday and fails on Tuesday, the IdP likely rotated their signing key. For a deeper dive into decoding specific XML errors, the WorkOS SAML Debugging Guide is an excellent resource for visualizing where the handshake broke.
Advanced Patterns: Key Rollover and Metadata
Zero-Downtime Rotation
When an Enterprise customer updates their certificate, they don't want downtime. You cannot simply overwrite the old cert in settings.json.
python3-saml supports x509certMulti, an array in the settings that allows you to define multiple valid IdP certificates. The library will attempt to validate the signature against any of the certs in the list, allowing you to support the Old and New keys simultaneously during the transition.
Dynamic Metadata
Do not hand-code your SP XML metadata. It is prone to syntax errors. The library can generate this for you, ensuring your EntityID and ACS URLs are perfectly aligned with your settings.
settings = auth.get_settings()
metadata = settings.get_sp_metadata()
errors = settings.validate_metadata(metadata)
if len(errors) == 0:
print(metadata)
Remember, SAML only handles identity. For managing what users can actually do once logged in, read our guide on Authentication vs. Authorization.
FAQ: Common python3-saml Questions
Q: python3-saml vs. pysaml2: Which should I use in 2026?
python3-saml is preferred for its higher-level API and ease of use. pysaml2 is powerful and supports more obscure SAML profiles, but it is notoriously difficult to configure and maintain. Unless you have a very specific edge case, stick to python3-saml.
Q: Does python3-saml support async (FastAPI)?
Not natively. It is a synchronous library. As shown in the implementation section above, you must run the process_response method in a thread pool to prevent blocking your application's event loop.
Q: How do I handle "Invalid Assertion" errors?
Always enable debug: true in settings first. Then check auth.get_last_error_reason(). The most common culprits are clock drift (fix with accepted_time_diff), mismatched EntityIDs, or a missing xmlsec1 dependency on the server.
Q: Can I use this library with Azure AD (Entra ID) and Okta?
Yes. python3-saml is IdP-agnostic and works perfectly with all major providers including Entra ID, Okta, and Google Workspace. The XML standard is the same; only the values in settings.json change.