Implement SSO with SAML
How to implement SAML 2.0 single sign-on as a Service Provider with XML signature verification, IdP metadata handling, and secure session management in Python, Node.js, and Java.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
SAML 2.0 is the dominant enterprise single sign-on protocol. It allows an organization to authenticate users in a central Identity Provider (IdP) — Okta, Azure AD, Keycloak, or ADFS — and then assert their identity to downstream Service Providers (SPs) via signed XML documents. Implementing SAML correctly requires handling XML parsing securely, validating signatures, managing metadata exchange, and preventing replay attacks.
When to Use
-
For alternatives, see Complete Guide to Authentication Patterns.
-
Your application serves enterprise customers who already have a centralized IdP
-
You need federated identity across multiple organizations (multi-tenant SaaS)
-
The organization requires protocol-level interoperability with existing SSO infrastructure
-
You need to support Just-In-Time provisioning (creating accounts from SAML assertions)
-
Compliance requirements mandate a standard protocol rather than OAuth 2.0 / OIDC
When NOT to Use
- You are building a consumer-facing application — use OAuth 2.0 + OIDC instead
- The IdP supports OIDC (most modern IdPs do) — OIDC is simpler, JSON-based, and more widely supported by developer libraries
- You need mobile or SPA authentication — SAML is web-browser-centric and awkward for native apps
- The overhead of XML security (signature verification, schema validation) exceeds your team’s expertise
Step-by-Step Implementation
Python (python3-saml / onelogin)
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from flask import Flask, request, session, redirect, url_for
import os
# SAML settings (store certificates securely, not in code)
SAML_CONFIG = {
"sp": {
"entityId": "https://app.example.com/saml/metadata",
"assertionConsumerService": {
"url": "https://app.example.com/saml/acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"singleLogoutService": {
"url": "https://app.example.com/saml/sls",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": os.environ["SP_CERT"],
"privateKey": os.environ["SP_PRIVATE_KEY"]
},
"idp": {
"entityId": os.environ["IDP_ENTITY_ID"],
"singleSignOnService": {
"url": os.environ["IDP_SSO_URL"],
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": os.environ["IDP_CERT"]
},
"security": {
"nameIdEncrypted": False,
"authnRequestsSigned": True,
"logoutRequestSigned": True,
"wantAssertionsSigned": True,
"wantMessagesSigned": True,
"wantNameId": True,
"requestedAuthnContext": True,
"signMetadata": True
}
}
app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
def init_saml_auth(req):
return OneLogin_Saml2_Auth(req, SAML_CONFIG)
@app.route("/saml/login")
def saml_login():
req = {
"https": "on" if request.is_secure else "off",
"http_host": request.host,
"script_name": request.path,
"server_port": request.environ.get("SERVER_PORT"),
"get_data": request.args.copy(),
"post_data": request.form.copy()
}
auth = init_saml_auth(req)
return redirect(auth.login())
@app.route("/saml/acs", methods=["POST"])
def saml_acs():
req = {
"https": "on" if request.is_secure else "off",
"http_host": request.host,
"script_name": request.path,
"server_port": request.environ.get("SERVER_PORT"),
"get_data": request.args.copy(),
"post_data": request.form.copy()
}
auth = init_saml_auth(req)
auth.process_response()
errors = auth.get_errors()
if errors:
app.logger.error(f"SAML errors: {errors}")
return "Authentication failed", 401
if not auth.is_authenticated():
return "Not authenticated", 401
# Extract attributes and create session
session["saml_user"] = {
"email": auth.get_nameid(),
"attributes": auth.get_attributes(),
"session_index": auth.get_session_index()
}
# Optional: JIT provisioning
user = find_or_create_user(
email=auth.get_nameid(),
name=auth.get_attributes().get("firstName", [""])[0],
groups=auth.get_attributes().get("groups", [])
)
session["user_id"] = user.id
return redirect("/dashboard")
@app.route("/saml/sls")
def saml_sls():
req = {
"https": "on" if request.is_secure else "off",
"http_host": request.host,
"script_name": request.path,
"server_port": request.environ.get("SERVER_PORT"),
"get_data": request.args.copy(),
"post_data": request.form.copy()
}
auth = init_saml_auth(req)
url = auth.process_slo(delete_session_cb=lambda: session.clear())
return redirect(url or "/")
Node.js (passport-saml)
import passport from 'passport';
import { Strategy as SamlStrategy } from 'passport-saml';
import fs from 'fs';
const samlConfig = {
entryPoint: process.env.IDP_SSO_URL,
issuer: 'https://app.example.com/saml/metadata',
callbackUrl: 'https://app.example.com/saml/acs',
logoutUrl: 'https://app.example.com/saml/sls',
cert: fs.readFileSync(process.env.IDP_CERT_PATH, 'utf-8'),
privateKey: fs.readFileSync(process.env.SP_PRIVATE_KEY_PATH, 'utf-8'),
decryptionPvk: fs.readFileSync(process.env.SP_PRIVATE_KEY_PATH, 'utf-8'),
signatureAlgorithm: 'sha256',
digestAlgorithm: 'sha256',
validateInResponseTo: true,
disableRequestedAuthnContext: false,
acceptedClockSkewMs: 300000 // 5 minutes
};
passport.use(new SamlStrategy(samlConfig, (profile, done) => {
// JIT provisioning
const user = {
email: profile.nameID,
name: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] || profile.nameID,
groups: profile['http://schemas.xmlsoap.org/claims/Group'] || [],
sessionIndex: profile.sessionIndex
};
return done(null, user);
}));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
// Express routes
app.get('/saml/login', passport.authenticate('saml'));
app.post('/saml/acs',
passport.authenticate('saml', { failureRedirect: '/login' }),
(req, res) => res.redirect('/dashboard')
);
app.get('/saml/logout', (req, res) => {
samlStrategy.logout(req, (err, url) => {
if (err) return res.status(500).send('Logout failed');
req.logout(() => res.redirect(url));
});
});
Java (Spring Security SAML)
@Configuration
@EnableWebSecurity
public class SamlSecurityConfig {
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("https://idp.example.com/metadata.xml")
.registrationId("okta")
.entityId("https://app.example.com/saml/metadata")
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
.signingX509Credentials(c -> c.add(
Saml2X509Credential.signing(loadPrivateKey(), loadCertificate())
))
.decryptionX509Credentials(c -> c.add(
Saml2X509Credential.decryption(loadPrivateKey(), loadCertificate())
))
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.loginPage("/saml/login")
.defaultSuccessUrl("/dashboard", true)
)
.saml2Logout(withDefaults());
return http.build();
}
// Custom user mapping from SAML assertion
@Bean
public GrantedAuthoritiesMapper authoritiesMapper() {
return authorities -> {
Set<GrantedAuthority> mapped = new HashSet<>();
for (GrantedAuthority auth : authorities) {
if (auth.getAuthority().startsWith("GROUP_")) {
mapped.add(new SimpleGrantedAuthority(
"ROLE_" + auth.getAuthority().substring(6)
));
}
}
return mapped;
};
}
}
What Works
- Validate the SAML response signature before parsing any assertions. XML Signature Wrapping (XSW) attacks inject malicious assertions that bypass signature checks if the code looks in the wrong XML node.
- Use HTTPS everywhere. SAML assertions contain authentication tokens; transmitting them over HTTP allows interception and replay.
- Store private keys in a hardware security module (HSM) or secret manager. Never commit SP private keys to git or expose them in environment variables on developer machines.
- Set a short assertion lifetime (5-10 minutes). Assertions are bearer tokens; a stolen assertion can be replayed until it expires. The
NotOnOrAfterfield must be strictly validated. - Implement single logout (SLO). Without SLO, logging out of the SP does not terminate the IdP session, allowing the user to re-authenticate silently via another SP.
Common Mistakes
- Disabling signature validation in development and forgetting to re-enable. The most common production SAML vulnerability is
wantAssertionsSigned: falseleft over from local testing. - Using string-based XML parsing instead of a secure XML library. Standard DOM parsers are vulnerable to XXE attacks. Use SAML-specific libraries that disable DTD processing.
- Trusting the NameID as the only user identifier. If an attacker can change their NameID at the IdP, they can impersonate another user. Map to a stable, internal user ID.
- Ignoring clock skew. IdP and SP clocks that differ by more than the assertion lifetime cause legitimate logins to fail. Allow a small skew (1-5 minutes) but log warnings.
- Not validating the
InResponseTofield. Without this check, an attacker can capture a legitimate assertion and replay it against a different authentication request.
Troubleshooting
- Login works for some users but not others: check identity provider configuration, user claims, and role mappings. Look for case sensitivity in identifiers.
- Token expires too quickly: verify token lifetime, refresh logic, and clock skew. Short tokens with secure refresh are preferred.
- Session is not shared across subdomains: set the cookie domain and SameSite policy correctly.
- Brute force attempts increase: implement rate limiting, account lockout, and CAPTCHA.
- OIDC flow fails with invalid_state: ensure the state parameter is stored, transmitted, and validated in the same user session.
Key Takeaways
- Apply implement sso with saml when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Copying the example without adapting it to real data volumes and failure modes.
- Skipping load and error-injection tests before the first production deployment.
- Hard-coding values that should be configurable per environment.
- Forgetting to add logging and monitoring at each step.
- Deploying without a rollback plan or a tested backup strategy.
- Assuming the minimal example will scale without adding caching or batching.
- Not documenting the version and configuration used in production.
- Letting the recipe sit unchanged when dependencies or scale evolve.
Related Resources
Implement RBAC
How to implement role-based access control with hierarchical roles, permission grants, and middleware enforcement across Python, Node.js, and Java.
RecipeImplement ABAC
How to implement attribute-based access control with policy engines, live context evaluation, and fine-grained authorization decisions across Python, Node.js, and Java.
GuideSecrets Management: Vault, Cloud Managers
A practical guide to secrets management: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager with rotation, access control, and CI/CD integration.
Frequently Asked Questions
- What is the difference between SAML and OAuth 2.0?
- SAML is an XML-based standard for authentication and authorization, often used for enterprise SSO. OAuth 2.0 is a framework for delegated authorization, commonly used for API access and social login.
- What are the roles in SAML?
- The Service Provider (SP) is the application users want to access. The Identity Provider (IdP) authenticates users and issues SAML assertions.
- How do I secure SAML assertions?
- Sign SAML assertions and responses with XML signatures, encrypt assertions in transit, validate the destination and timestamps, and enforce strict certificate pinning.