SAML 2.0 — The Enterprise SSO Standard
If OIDC is the modern, JSON-based protocol that powers "Sign in with Google," then SAML is its older, XML-heavy sibling that runs the corporate world. It's been around since 2005, it's verbose, it uses XML signatures, and enterprise IT departments love it.
Every time you click an app icon in your corporate Okta or Azure AD dashboard and magically land on Salesforce or Jira without typing a password — that's SAML doing the heavy lifting behind the scenes.
In the previous article, we covered OIDC and briefly compared it with SAML. Now let's go deep into how SAML actually works — the assertions, the bindings, the flows, and most importantly, the security attacks that make SAML a fascinating target for security researchers.
What is SAML and Why Does It Still Matter?
SAML stands for Security Assertion Markup Language. It's an XML-based open standard for exchanging authentication and authorization data between parties — specifically between an Identity Provider (IdP) and a Service Provider (SP).
SAML 2.0 was standardized by OASIS in 2005. That's ancient by internet standards. So why does it still matter?
- Enterprise entrenchment — Thousands of enterprise applications support SAML. Salesforce, AWS Console, Workday, ServiceNow — they all speak SAML. Migrating to OIDC would require coordinated changes across hundreds of integrations.
- Compliance and auditing — Many compliance frameworks reference SAML-based federation. Changing the protocol means re-certifying.
- Vendor support — Enterprise IdPs like ADFS (Active Directory Federation Services), Okta, and Ping Identity have deep SAML support with years of battle-tested configurations.
OIDC is gaining ground, but SAML isn't going anywhere soon.
SAML Terminology
| Term | Meaning |
|---|---|
| Identity Provider (IdP) | The system that authenticates users and issues assertions (Okta, ADFS, WSO2 IS) |
| Service Provider (SP) | The application that consumes assertions and grants access (Salesforce, Jira, AWS Console) |
| Principal | The user being authenticated |
| Assertion | An XML document containing statements about the user |
| Binding | How SAML messages are transported (HTTP POST, HTTP Redirect, SOAP) |
| Metadata | XML configuration documents that establish trust between IdP and SP |
| AuthnRequest | The SP's request to the IdP for authentication |
| SAML Response | The IdP's response containing the assertion |
| RelayState | A URL that tells the SP where to redirect the user after SSO |
| ACS (Assertion Consumer Service) | The SP endpoint that receives SAML responses |
SAML Assertions — The Core Artifact
A SAML assertion is an XML document that makes statements about a user. There are three types of statements:
- Authentication Statement — "This user was authenticated at this time using this method"
- Attribute Statement — "This user has these attributes (name, email, roles)"
- Authorization Decision Statement — "This user is authorized to do X on resource Y" (rarely used in practice)
Here's a simplified SAML assertion:
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_assertion123"
Version="2.0"
IssueInstant="2024-05-20T10:30:00Z">
<saml:Issuer>https://idp.company.com</saml:Issuer>
<ds:Signature>
<!-- XML Digital Signature over this assertion -->
</ds:Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
[email protected]
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
NotOnOrAfter="2024-05-20T10:35:00Z"
Recipient="https://app.example.com/saml/acs"
InResponseTo="_request456" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2024-05-20T10:29:00Z"
NotOnOrAfter="2024-05-20T10:35:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://app.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2024-05-20T10:30:00Z"
SessionIndex="_session789">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>[email protected]</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="role">
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="department">
<saml:AttributeValue>Engineering</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
Let's break down the key elements:
| Element | Purpose |
|---|---|
Issuer |
The IdP that created this assertion |
Signature |
XML digital signature — proves the assertion wasn't tampered with |
Subject/NameID |
The user's identity (usually email or an opaque ID) |
SubjectConfirmation |
How the SP should confirm the subject (bearer = just present the token) |
InResponseTo |
Links the assertion to a specific AuthnRequest (prevents replay) |
Conditions |
Time window and audience restrictions for the assertion |
AudienceRestriction |
Which SP this assertion is intended for |
AuthnStatement |
When and how the user authenticated |
AttributeStatement |
User attributes passed to the SP |
SP-Initiated SSO Flow
This is the most common SAML flow. The user starts at the Service Provider and gets redirected to the IdP for authentication.
Browser Service Provider (SP) Identity Provider (IdP)
| | |
1. | --- Visit app -------> | |
| | |
2. | | (User not authenticated) |
| | Generate AuthnRequest |
| | |
3. | <-- Redirect to IdP -- | |
| (with AuthnRequest) | |
| | |
4. | --- AuthnRequest ------------------------------> |
| | |
5. | <--- Login page -------------------------------- |
| | |
6. | --- Credentials ----------------------------------> |
| | |
7. | | IdP authenticates |
| | Creates SAML Response |
| | Signs assertion |
| | |
8. | <-- Auto-submit form (HTTP POST) with SAMLResponse |
| | |
9. | --- POST SAMLResponse -> | |
| | |
10. | | Validate signature |
| | Check conditions |
| | Extract user attributes |
| | Create session |
| | |
11. | <-- Access granted --- | |
Step 3 — The AuthnRequest:
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="_request456"
Version="2.0"
IssueInstant="2024-05-20T10:29:50Z"
Destination="https://idp.company.com/saml/sso"
AssertionConsumerServiceURL="https://app.example.com/saml/acs"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
<saml:Issuer>https://app.example.com</saml:Issuer>
<samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" />
</samlp:AuthnRequest>
This AuthnRequest says: "I'm app.example.com. Please authenticate the user and send the response to my ACS URL using HTTP POST."
Step 8-9 — The SAML Response (via HTTP POST):
The IdP creates an HTML page with a hidden form that auto-submits:
<html>
<body onload="document.forms[0].submit()">
<form method="POST" action="https://app.example.com/saml/acs">
<input type="hidden" name="SAMLResponse" value="PHNhbWxwOl..." />
<input type="hidden" name="RelayState" value="https://app.example.com/dashboard" />
</form>
</body>
</html>
The SAMLResponse value is a base64-encoded XML document containing the signed assertion.
IdP-Initiated SSO Flow
In this flow, the user starts at the IdP — typically a corporate dashboard (Okta, Azure AD portal) — and clicks an app icon.
The IdP sends a SAML Response directly to the SP without a preceding AuthnRequest. This is simpler but less secure because:
- There's no
InResponseTovalue (no request to bind the response to) - This makes it harder to prevent replay attacks
- The SP must trust the assertion based solely on its signature and conditions
Despite the security trade-offs, IdP-initiated SSO is widely used in corporate environments because it provides a convenient "app launcher" experience.
SAML Bindings
Bindings define how SAML messages are transported over HTTP.
HTTP Redirect Binding
The SAML message is deflated (compressed), base64-encoded, and sent as a URL query parameter:
GET https://idp.company.com/saml/sso?
SAMLRequest=fZJNT8MwEITvSPwH...
&RelayState=https://app.example.com/dashboard
&SigAlg=http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
&Signature=dGhpcyBpcyBh...
Used for AuthnRequests (small messages). Not suitable for SAML Responses because they're too large for URLs.
HTTP POST Binding
The SAML message is base64-encoded and sent in a form POST body:
POST https://app.example.com/saml/acs
Content-Type: application/x-www-form-urlencoded
SAMLResponse=PHNhbWxwOlJlc3BvbnNlIHhtbG5z...&RelayState=https://app.example.com/dashboard
Used for SAML Responses (large messages with assertions and signatures). The auto-submitting HTML form pattern makes this seamless for the user.
XML Signatures in SAML
The signature is what makes SAML assertions trustworthy. Without it, anyone could craft an assertion claiming to be any user.
SAML uses XML Digital Signatures (XMLDSig), which can be placed at two levels:
- Signed Assertion — The
<Assertion>element itself is signed. This is the most important — it proves the assertion content hasn't been tampered with. - Signed Response — The entire
<Response>wrapper is signed. Provides integrity for the response metadata.
Best practice: sign the assertion (always), and optionally sign the response as well.
The signature structure looks like this:
<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/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_assertion123">
<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/2001/04/xmlenc#sha256" />
<ds:DigestValue>base64-encoded-digest</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>base64-encoded-signature</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>base64-encoded-certificate</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
The SP validates the signature using the IdP's public certificate (obtained through metadata exchange during setup).
SAML Metadata
Trust between IdP and SP is established through metadata exchange. Both sides publish XML metadata documents describing their endpoints, certificates, and capabilities.
IdP Metadata (abbreviated):
<EntityDescriptor entityID="https://idp.company.com"
xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIICpDCCAYwC...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://idp.company.com/saml/sso" />
</IDPSSODescriptor>
</EntityDescriptor>
SP Metadata (abbreviated):
<EntityDescriptor entityID="https://app.example.com"
xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"
AuthnRequestsSigned="true">
<AssertionConsumerService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://app.example.com/saml/acs"
index="0" />
</SPSSODescriptor>
</EntityDescriptor>
Typically, you import the IdP's metadata into your SP configuration (or vice versa), and the trust relationship is established automatically.
SAML Security — Attacks and Defenses
This is where SAML gets really interesting from a security perspective. The complexity of XML parsing and signature validation creates a large attack surface.
XML Signature Wrapping (XSW)
This is the most dangerous SAML attack. It exploits a fundamental weakness in how XML signatures work.
The XML signature signs a specific element identified by a URI reference (e.g., #_assertion123). But what if the attacker moves the signed element to a different location in the XML tree and inserts a forged element in the original location?
The signature validation passes (because the signed element still exists and hasn't been modified). But the application processes the forged element (because it's in the expected location).
Before the attack:
<Response>
<Assertion ID="_assertion123"> <!-- This is signed and processed -->
<Subject>[email protected]</Subject>
</Assertion>
</Response>
After XSW attack:
<Response>
<Assertion ID="_forged"> <!-- FORGED — this gets processed -->
<Subject>[email protected]</Subject>
</Assertion>
<Assertion ID="_assertion123"> <!-- Original — signature still valid -->
<Subject>[email protected]</Subject>
</Assertion>
</Response>
The SP's XML parser finds the first <Assertion> element and processes it — the forged one. The signature validator finds the element with ID="_assertion123" and validates it — the original one. Both succeed, but on different elements.
Defense: After signature validation, ensure that the exact element that was signed is the one you process. Use strict XML schema validation. Libraries like xmlsec handle this correctly when configured properly.
Replay Attack
An attacker captures a valid SAML Response and replays it later to gain unauthorized access.
Defense:
- Check
NotOnOrAfter— reject assertions that have expired - Check
InResponseTo— match the response to a specific AuthnRequest you sent - Track
AssertionID— reject assertions you've already seen - Use
OneTimeUsecondition when available
XXE (XML External Entity)
SAML messages are XML, which means they're vulnerable to XXE if the XML parser isn't hardened:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<samlp:Response>
<saml:Assertion>
<saml:Subject>
<saml:NameID>&xxe;</saml:NameID>
</saml:Subject>
</saml:Assertion>
</samlp:Response>
If the SP's XML parser processes external entities, the attacker can read files from the server, perform SSRF, or cause denial of service.
Defense: Disable external entity processing in your XML parser. In Java:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Golden SAML
If an attacker compromises the IdP's signing certificate (the private key), they can forge SAML assertions for any user — including administrators — across every SP that trusts that IdP.
This is analogous to a Golden Ticket attack in Kerberos. The attacker doesn't need to compromise the SP or the user's credentials. They just forge an assertion and present it.
This was famously used in the SolarWinds supply chain attack (2020), where the attackers compromised the ADFS signing certificate and forged SAML assertions to access cloud resources.
Defense:
- Protect signing keys with HSMs (Hardware Security Modules)
- Monitor for anomalous SAML assertions (unexpected users, unusual attributes)
- Rotate signing certificates periodically
- Implement anomaly detection on the SP side (unexpected source IPs, unusual access patterns)
Open Redirect via RelayState
The RelayState parameter tells the SP where to redirect the user after SSO. If the SP doesn't validate this URL, an attacker can set it to an external domain:
RelayState=https://attacker.com/phishing
Defense: Validate that RelayState points to a URL within your application's domain.
SAML vs OIDC — Detailed Comparison
| Aspect | SAML 2.0 | OIDC |
|---|---|---|
| Year | 2005 | 2014 |
| Token format | XML (verbose) | JSON/JWT (compact) |
| Token size | 5-20KB | ~1KB |
| Transport | HTTP Redirect + POST | HTTP Redirect + back-channel |
| Signature | XML Digital Signatures | JWS (JSON Web Signature) |
| Mobile support | Poor (XML parsing, large tokens) | Excellent |
| API support | Limited | Native (Bearer tokens) |
| Complexity | High (XML, canonicalization, schemas) | Lower |
| Discovery | Metadata XML | .well-known/openid-configuration |
| Enterprise adoption | Dominant | Growing |
| Use case | Enterprise SSO, legacy apps | Modern web, mobile, APIs |
When to use SAML:
- Integrating with enterprise applications that only support SAML
- Your IdP and SPs already have SAML configured
- Compliance requirements specify SAML
When to use OIDC:
- Building new applications
- Mobile or SPA clients
- API-first architectures
- You want simplicity and modern tooling
Testing Tools
| Tool | Purpose |
|---|---|
| SAML Tracer | Browser extension that captures SAML requests/responses in real time |
| SAMLTool.com | Online tool for encoding/decoding SAML messages, validating signatures |
| SAMLRaider | Burp Suite extension for SAML security testing (XSW attacks, signature manipulation) |
| OneLogin SAML Toolkit | Libraries for implementing SAML SP in various languages |
Final Thoughts
SAML is complex, verbose, and XML-heavy. But it's also battle-tested, widely supported, and deeply embedded in enterprise infrastructure. If you work in enterprise security, you will encounter SAML — and understanding its internals is essential for both implementing it correctly and testing it for vulnerabilities.
The XML Signature Wrapping attack alone has affected hundreds of SAML implementations over the years. It's a beautiful example of how the gap between "signature validates" and "the right thing is signed" can lead to complete authentication bypass.
Whether you're configuring SAML in WSO2 Identity Server, testing it with Burp Suite, or planning a migration to OIDC — the fundamentals covered here will serve you well.
Stay secure!