Often, in an enterprise setting, an application needs to identify a user from a central authentication service, allowing it to make sure the right information is displayed to the right person. While OpenID Connect is the new go-to protocol to do so, a lot of enterprises still rely on its predecessor, SAML (Security Assertion Markup Language). While categorized as a “predecessor”, this one is still highly relevant. However, it needs to be properly configured as some potential security risks have been discovered throughout the years.
Now the big question is, how can we use this protocol within a Streamlit application, as a way to ID our users. Let’s take a look on a simplifed, but fully functional example. In this article, we will go through:
- The SAML Protocol
- An overview of how it works
- Some important terminology
- An example of a request
- Potential weaknesses
- We will build a working prototype to authenticate users in a Streamlit application.
SAML Protocol
Overview
SAML (Security Assertion Markup Language) is an XML-based open standard for exchanging authentication and authorization data between parties, particularly between an identity provider (IdP), the enterprise authentication server, and a service provider (SP), your application for example. It enables single sign-on (SSO) experiences across multiple applications and domains. SAML works by transferring user identity information securely between the IdP and SP, allowing the SP to trust the authentication performed by the IdP. This protocol is widely used in enterprise environments due to its flexibility, security features, and ability to work across different platforms and technologies.
SAML Terms
Identity Provider (IdP)
: The system responsible for authenticating users and providing identity information to service providers.Service Provider (SP)
: The application or service that relies on the IdP for user authentication and requires access to user information.SAML Request
: A message sent by the SP to the IdP, requesting authentication of a user. Also known as an “AuthnRequest”.SAML Response
: The message sent by the IdP back to the SP, containing authentication results and user attributes.Assertion
: A statement within the SAML Response that contains information about the user’s identity, authentication status, and attributes.- Assertions are signed with XML Digital Signatures (XMLDSig) to ensure integrity and authenticity.
Assertion Consumer Service (ACS)
: An endpoint on the SP that receives and processes the SAML Response from the IdP.Attribute
: Specific pieces of information about the user included in the SAML Response, such as username, email, or role.Relay State
: A mechanism to maintain the user’s original destination or state within the SP before initiating the SAML flow. It helps redirect the user to the correct location after authentication.SAML Trust
: The configuration and agreement between the IdP and SP that allows them to securely communicate and trust each other’s messages.- Includes exchange of metadata and certificates for message validation.
Metadata
: XML documents containing configuration information for IdPs and SPs, facilitating easier setup and interoperability between SAML entities.Single Sign-On (SSO)
: A feature of SAML that allows users to authenticate once with the IdP and access multiple SPs without re-authenticating.
Let’s examine the authentication flow when a user clicks on a login button in an application using SAML:
This diagram illustrates the SAML protocol in action. The browser is redirected between the Service Provider (SP) and Identity Provider (IdP), exchanging SAML requests and responses. Each component has the responsibility to verify the SAML request and response to ensure the validity of the sender. This verification process uses internal configurations, such as checking that each element is properly signed and that the signature is valid.
Key steps in this flow:
- User initiates login at the SP
- SP generates a SAML request and redirects the user to the IdP
- IdP authenticates the user and generates a SAML response
- User is redirected back to the SP with the SAML response
- SP verifies the response and grants access if valid
This process ensures secure, federated authentication across different systems and domains.
Request Example
SAML Request
Let’s examine a typical SAML request to understand its structure and key components:
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="ONELOGIN_809707f0030a5d00620c9d9df97f627afe9dcc24"
Version="2.0"
ProviderName="SP test"
IssueInstant="2014-07-16T23:52:45Z"
Destination="http://idp.example.com/SSOService.php"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="http://sp.example.com/demo1/index.php?acs">
<saml:Issuer>http://sp.example.com/demo1/metadata.php</saml:Issuer>
<samlp:NameIDPolicy
Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
AllowCreate="true" />
<samlp:RequestedAuthnContext Comparison="exact">
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnRequest>
Looking through it, the important elements are:
- ID: Generated by the SP. The IdP will include this ID in the SAML response for correlation.
- IssueInstant: Allows IdP/SP to only accept requests within a specific timeframe.
- AssertionConsumerServiceURL: The endpoint where the IdP will send the user back after authentication.
- Issuer: Pre-defined in SAML trust configuration. Must match for the handshake to succeed.
This request will be stringified, encoded, and passed in the query string to the IdP when it redirects the user.
SAML Response
Once the authentication process is completed on the Identity Provider (IdP) side, the user is redirected to the Service Provider’s (SP) Assertion Consumer Service
(ACS) URL. The SP then needs to process and validate the SAML response to confirm the user’s identity and retrieve any additional attributes.
<samlp:Response
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"
Version="2.0"
IssueInstant="2014-07-17T01:01:48Z"
Destination="http://sp.example.com/demo1/index.php?acs"
InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
ID="pfx4023244c-8eaf-f3a8-1bd4-6a5e4898c71a"
Version="2.0"
IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfx4023244c-8eaf-f3a8-1bd4-6a5e4898c71a"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>GQQHvRSu7NPWRrXcx7mdBJj+8fM=</ds:DigestValue></ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>jKH4P5nndfAQl879zBZjEUdbshVUoIv3rDNZDBO8RRdG5laEVVGRlOddtzk+A12PvHQrV9KGbgsy/gkE7VwdkfFbLK/iJKncsztBT8x0sJzcnLFjEIPcKKT/pVTluexnQI7MJZquwtRXPCyCv/7FY57vXZzV+CcmE3bJp6KXsqM=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate></ds:X509Data></ds:KeyInfo>
</ds:Signature>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
Here, the ACS endpoint is responsible for:
- Receiving and processing the SAML response
- Validating the response’s integrity and authenticity
- Extracting user information from the assertion
- Creating a local session for the authenticated user
Key elements of the SAML response:
ID
: Should match the original ID we sent in the requestDestination
: Must match our SP’s ACS URLsaml:Issuer
: To be compared with our trust configurationsaml:Assertion
: Contains information about the user; each assertion should be signed (e.g.,<ds:Signature>
) for securitysaml:Subject
: Contains the user ID or username of the authenticated usersaml:Conditions
: Specifies a validity window, outside of which the assertion should not be acceptedsaml:Attribute
: Contains additional attributes about the user
The ACS endpoint must carefully validate all these elements to ensure the response’s authenticity and prevent security vulnerabilities such as replay attacks or token forgery.
Trust Configuration in SAML
The trust configuration is a crucial aspect of SAML that establishes the secure relationship between the Service Provider (SP) and the Identity Provider (IdP). It involves exchanging metadata and certificates between the two parties, allowing them to recognize and trust each other’s communications. The trust configuration typically includes:
- Entity IDs: Unique identifiers for both the SP and IdP.
- Assertion Consumer Service (ACS) URL: Where the IdP sends the SAML response.
- Single Sign-On (SSO) URL: Where the SP sends authentication requests.
- Certificates: Used for signing and, in some cases, encrypting SAML messages.
- Attribute Mappings: Defines how user attributes are shared.
This configuration ensures that SAML requests and responses are only accepted from trusted sources, maintaining the security and integrity of the authentication process. Both the SP and IdP must carefully manage and update their trust configurations to maintain a secure SAML ecosystem.
It’s worth noting that while HTTPS is generally recommended for secure communication, SAML does not require publicly signed certificates. This is because the trust relationship in SAML is established directly between the SP and IdP. The SP is configured to trust the IdP based on a specific certificate, eliminating the need for third-party verification of the IdP’s identity. This direct trust model simplifies certificate management in SAML implementations.
Who keeps track of the user’s data ?
Provisioning is a crucial aspect of SAML implementation that ensures user identities and attributes are synchronized between the Identity Provider (IdP) and Service Provider (SP). This process maintains consistency and up-to-date user information across systems. In SAML, the IdP is always considered the authoritative source of truth for user information.
There are two main approaches to provisioning:
Just-in-Time (JIT) Provisioning: The SP updates what it knows about the user when it receives a response from the IdP. This approach is reactive and updates user information only during authentication events.
Real-time Provisioning: The IdP has the responsibility to keep the SP up-to-date in real-time. This ensures that any changes to user attributes or permissions are immediately reflected in the SP. For implementing real-time provisioning, you might want to look into the System for Cross-domain Identity Management (SCIM) protocol, which provides a standardized way for identity provisioning between systems.
Potential weaknesses
The major weak points of SAML can be summarized into three main elements:
- Flawed assertion validation
- XML Parsers vulnerability
- Insecure libraries
With this in mind, a developer should take extra consideration on each potential attack vector.
Some possible attack vectors
- Limit XML Parser
- Disable Document Type Definition (DTD) Fetching
- Defends against XML External Entities (XXE) attacks
- Use Canonicalized XML
- Normalizes XML via specified method / algorithm
- Protects against inner comment node manipulation
- Validate the Schema
- Defends against XML Signature Wrapping (XSW) attacks
- Adhere to a specific schema outside of the signature
- Use explicit XPath expressions
- Validate every signature
- Ensure all assertions are signed, not just the response
- Limit Accepted Algorithms
- Hardcode accepted encryption and signature algorithms
- Avoid weak or deprecated algorithms (e.g., SHA-1)
- HTTPS only
- Ensure all SAML-related communications use TLS
- Validate Parties
- Verify the destination, audience, recipients, and issuer match
- Protects against IdP impersonation
- Protects against unauthorized lateral movement
- Enforce Validation Window
- Implement strict time-based validation for assertions
- Historical Cache
- Track all unique IDs to detect replay attacks
- Limit Buffer size
- Protect against DDOS attacks of oversized SAML responses / XML
- Implement Proper Session Management
- Use secure, randomly generated session tokens
- Input Validation and Sanitization
- Sanitize all user-supplied data, even if it comes from the IdP
Implementing SAML in Streamlit Applications
Now that we understand SAML’s fundamentals, let’s explore its integration with Streamlit through a practical example. ⚠️ Important Security Notice This example is intended for educational purposes only and demonstrates basic SAML concepts. The code provided:
- Omits essential security features
- Should not be deployed to production environments
- Is meant to illustrate core concepts rather than provide a complete solution
Need a production-ready SAML implementation for your Streamlit application? Contact us at Ploomber for managed authentication solutions that prioritize security and reliability.
Quick review of Streamlit architecture
Streamlit operates on a Client-Server architecture where Python computations are executed on a server (either your local machine or a remote server) and the results are rendered as HTML and JavaScript in the client’s browser.
However, Streamlit’s built-in routing capabilities are limited to handling GET requests, which poses a challenge for SAML authentication. SAML requires the ability to process POST requests when the Identity Provider (IdP) redirects users back to your application with their authentication response.
To overcome this limitation, our solution combines two servers: the main Streamlit application server and a supplementary Flask server. The Flask server handles the SAML authentication flow, managing both the initial redirect to the IdP and the subsequent processing of the authentication response, while Streamlit continues to serve the application’s core functionality.
The final authentication flow will be the following:
Streamlit App
Let’s explore how to implement SAML authentication in a Streamlit application. We’ll use Streamlit’s session_state
to manage user data, including the auth_session
token that facilitates communication with our Flask authentication server.
import streamlit as st
import requests
# Initialize session state
if 'authenticated' not in st.session_state:
st.session_state.authenticated = False
st.session_state.user_info = None
Streamlit Login Flow
To initiate authentication, we need a login mechanism that redirects users to our Flask server. Since Streamlit runs server-side Python code, we’ll use a small JavaScript snippet to handle the client-side redirect.
# Login if the user is not connected
if not st.session_state.authenticated:
# Unknown user
st.title("Streamlit App with SAML Example")
if st.button("Login with SAML"):
# Redirect the browser to the Flask server's login endpoint
js = """
<meta http-equiv="refresh" content="0;url=http://localhost:5000/login">
"""
st.markdown(js, unsafe_allow_html=True)
else:
# User is authenticated
...
Managing Session Validation
After successful authentication, our Flask server generates an auth_token
. This token serves as a key to validate the user’s session and retrieve essential information such as identity and access privileges. The validation process involves extracting the token from the URL and verifying it with the Flask server.
# Check if the authentication token is in the URL → If so, validate it and connect the user
if 'auth_token' in st.query_params and not st.session_state.authenticated:
# Validate token with Flask server
try:
response = requests.get(
'http://localhost:5000/validate_token',
params={'token': st.query_params.auth_token}
)
if response.status_code == 200:
st.session_state.authenticated = True
st.session_state.user_info = response.json()
st.query_params.clear() # Clear the token from the url
st.rerun()
else:
st.error("Authentication failed. Please try logging again.")
except Exception as e:
st.error(f"Error while validating session token: {str(e)}")
After successful authentication, we can access and display the user’s information in our application.
# Check if the authentication token is in the URL → If so, validate it and connect the user
if 'auth_token' in st.query_params and not st.session_state.authenticated:
... # Previous Auth code
# Main application logic
if not st.session_state.authenticated:
st.title("Streamlit App with SAML Example")
... # Previous login code
else:
# User is authenticated, we can now display the user's name and more
st.title(f"Welcome {st.session_state.user_info.get('name')}")
st.write("Your profile:", st.session_state.user_info)
Handling User Logout
An important consideration in SAML authentication is the logout process. Note that implementing logout functionality is optional, as SAML typically manages single sign-out across all connected applications. When a user logs out, they’ll be signed out of all SAML-authenticated applications in their organization. You might prefer to leave logout handling to your organization’s central authentication platform.
if st.session_state.authenticated:
if st.sidebar.button("Logout"):
# Include the token in the logout request to be able to invalidate it in the server
token = st.session_state.user_info.get('session_index')
js = f"""
<meta http-equiv="refresh" content="0;url=http://localhost:5000/logout?token={token}">
"""
st.markdown(js, unsafe_allow_html=True)
st.session_state.authenticated = False
st.session_state.user_info = None
Flask Authentication Server
Now comes the more complex part: implementing the SAML protocol for our Service Provider. Our Flask server needs to handle five critical responsibilities:
- Accept login requests from the Streamlit application
- Generate and send SAML requests to the Identity Provider (IdP)
- Process incoming SAML responses
- Manage user sessions securely
- Redirect authenticated users back to Streamlit with appropriate authentication tokens
We’ll use python3-saml
to implement the SAML protocol in our Service Provider. While PySAML2
is another excellent option, it’s primarily designed for Identity Provider implementations. For our Service Provider needs, python3-saml
offers a more streamlined solution.
"""
IMPORTANT: This is a simplified example for demonstration purposes only. This code lacks several crucial security features.
"""
from flask import Flask, request, redirect, session, jsonify, url_for
from onelogin.saml2.auth import OneLogin_Saml2_Auth
import requests
import os
from urllib.parse import urlparse
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
# Server-side token storage
valid_tokens = {}
STREAMLIT_SERVER_URL = "http://localhost:8501"
AUTH_PROXY_SERVER_URL = "http://localhost:5000"
# Config
AUTH0_CLIENT_ID = "..."
AUTH0_ENTITY_ID = "....us.auth0.com"
def read_cert_from_file(filename):
"""Helper to load the certificate needed to validate the signature"""
with open(filename, 'r') as cert_file:
return cert_file.read().strip()
@app.route('/metadata')
def metadata():
raise NotImplementedError
def get_saml_settings():
raise NotImplementedError
@app.route('/login')
def login():
""" Initiate a SAML request """
raise NotImplementedError
@app.route('/acs', methods=['POST'])
def acs():
""" Assertion Consumer Service - Process the SAML response """
raise NotImplementedError
@app.route('/validate_token')
def validate_token():
raise NotImplementedError
@app.route('/logout')
def logout():
raise NotImplementedError
@app.route('/sls', methods=['POST'])
def sls():
""" Single Logout Service: Process the SAML Response """
raise NotImplementedError
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def proxy(path):
""" Proxy all other requests to Streamlit """
...
if __name__ == '__main__':
""" Debug Server - Do not use in production """
app.run(host='0.0.0.0', port=5000, debug=True)
Setting Up SAML Metadata
The SAML configuration requires careful setup to ensure proper communication between our Service Provider and the Identity Provider. For this, we need to implement two key components:
- The
/metadata
endpoint that allows the IdP to recognize our Service Provider - The
get_settings()
function that maintains our SAML configuration
def get_saml_settings():
""" General config for our SP """
return {
"strict": True,
"debug": True,
# Service Provider config
"sp": {
"entityId": f"{AUTH_PROXY_SERVER_URL}/metadata",
"assertionConsumerService": {
"url": f"{AUTH_PROXY_SERVER_URL}/acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
},
# Identidy Provider config
"idp": {
"entityId": f"urn:{AUTH0_ENTITY_ID}",
"singleSignOnService": {
"url": f"https://{AUTH0_ENTITY_ID}/samlp/{AUTH0_CLIENT_ID}",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"x509cert": read_cert_from_file('./key.pem') # To validate the signature
}
}
@app.route('/metadata')
def metadata():
""" SP Metadata endpoint """
auth = OneLogin_Saml2_Auth(prepare_flask_request(), get_saml_settings())
settings = auth.get_settings()
metadata = settings.get_sp_metadata()
errors = settings.validate_metadata(metadata)
if len(errors) == 0:
return metadata, 200, {'Content-Type': 'text/xml'}
return "Error: " + ', '.join(errors), 400
Managing the Login Process
When users initiate login from Streamlit, they’re redirected to our Flask server’s /login
endpoint. This endpoint transforms the standard request into a SAML request and redirects users to the Identity Provider for authentication. Using python3-saml
, this part is as easy as processing the settings previously stated and redirecting the user to the IdP.
@app.route('/login')
def login():
req = prepare_flask_request()
auth = OneLogin_Saml2_Auth(req, get_saml_settings())
return redirect(auth.login())
At this point, the user is sent to the Identity Provider, and if they aren’t already connected, the IdP will challenge the user by asking for its credentials. Once the IdP has identified the user, it prepares a POST request, which will be sent to our Flask server by redirecting the user. More precisely, the user will be sent to the url specified in "assertionConsumerService": { "url": f"{base_url}/acs", ... }
, which we defined in the config. This is where the SAML response is processed, which contains the XML with all the attributes.
@app.route('/acs', methods=['POST'])
def acs():
""" Assertion Consumer Service: Process the SAML response & redirect the user back to Streamlit """
req = prepare_flask_request()
auth = OneLogin_Saml2_Auth(req, get_saml_settings())
auth.process_response() # Verif the SAML response and its assertion
errors = auth.get_errors()
if not errors:
if auth.is_authenticated():
samlUserdata = auth.get_attributes()
samlNameId = auth.get_nameid()
samlSessionIndex = auth.get_session_index()
# ...
# Store for communication with Streamlit server later
token = samlSessionIndex
valid_tokens[token] = {
'email': samlNameId,
'attributes': samlUserdata,
'session_index': samlSessionIndex
}
# Redirect to Streamlit with authentication token (unsecure but valid for this demo)
return redirect(f'{STREAMLIT_SERVER_URL}/?auth_token={samlSessionIndex}')
return f"Error: {', '.join(errors)}", 400
Once the request is validated, we send the user back to our Streamlit application, but with extra information to allow Streamlit to query this server to retrieve user information.
Only one part is missing before we can see the Authentication process in action! Which is allowing the Streamlit server to retrieve the attributes of the user, for displaying the name, or looking for his access level by example.
@app.route('/validate_token')
def validate_token():
token = request.args.get('token')
if token and token in valid_tokens:
return jsonify(valid_tokens[token])
return jsonify({'error': 'Invalid token'}), 401
Another version of the authentication process is to encode everything in the URL and parse it from Streamlit, never maintaining the authentication state of the user on the server.
- In both cases, you should remove the
auth_token
from the URL and save it in the Streamlitsession_state
. This way, the user token won’t be exposed, and it can’t be sent to another user if someone copies and pastes the URL.
SAML Authentication Demo
One last thing, logout
When the user has finished with our service, if they want to log out, we need to:
- Provide a logout button in Streamlit that clears local session data
- Initiate a SAML logout request through our Flask server
- Process the logout response and redirect users appropriately
# First we need to add in logout service in the SAML config
def get_saml_settings():
""" General config for our SP """
return {
...
"sp": {
...
"singleLogoutService": {
"url": f"{AUTH_PROXY_SERVER_URL}/sls",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
...
},
"idp": {
...
"singleLogoutService": {
"url": f"https://{AUTH0_ENTITY_ID}/samlp/{AUTH0_CLIENT_ID}/logout",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
...
}
}
# Then implement the route to initialize the SAML Request
@app.route('/logout')
def logout():
""" Single Logout Service: Process the SAML Response & logout the user """
req = prepare_flask_request()
auth = OneLogin_Saml2_Auth(req, get_saml_settings())
token = request.args.get('token')
if token and token in valid_tokens:
name_id = valid_tokens[token]['email']
session_index = token
return redirect(auth.logout(
name_id=name_id,
session_index=session_index,
return_to=url_for('sls', _external=True),
))
return redirect(f'{STREAMLIT_SERVER_URL}/')
# Then implement the route to process the SAML Response
@app.route('/sls', methods=['POST'])
def sls():
req = prepare_flask_request()
# INFO: process_slo expect a GET, but auth0 return a POST
req["get_data"], req["post_data"] = req["post_data"], req["get_data"]
auth = OneLogin_Saml2_Auth(req, get_saml_settings())
url = auth.process_slo(
delete_session_cb=lambda: session.clear()
)
errors = auth.get_errors()
if len(errors) == 0:
if url is not None:
return redirect(url)
return redirect(f'{STREAMLIT_SERVER_URL}/')
return "Error: " + ', '.join(errors), 400
With all this work, we now have a working SAML implementation which will work on a local machine.
Security Considerations
⚠️ Important Security Notice
This implementation serves as an educational example to demonstrate SAML integration with Streamlit. It intentionally omits several critical security measures required for production environments. SAML, being an XML-based protocol, requires careful security configuration to prevent vulnerabilities.
Professional Alternative
Rather than implementing SAML authentication from scratch, consider using a managed service that handles authentication for your deployed applications. At Ploomber, we offer enterprise-grade authentication as part of our Teams license for Ploomber Cloud.
Our managed authentication solution supports:
- Streamlit applications
- Dash applications
- Docker containers
- And more…
With our solution, there’s no need to modify your app’s source code. We handle the complexities of SAML authentication in prior of the user reaching your application, and that with your IdP, ensuring a secure and seamless with your work place.