What SAML is and how it handles SSO
Communication between different data centres and SSO made possible across different domains.
The Security Assertion Markup Language (SAML) is a standard that defines an XML-based framework commonly used for Single-Sign On (SSO).
It allows communication between different data centres by relying on browser redirects or HTTP-based message exchanges, thus making SSO possible across multiple domains where the identity store and the application never communicate with each other directly. The user agent (e.g. web browser) bridges the gap between them.
sequenceDiagram
actor User
participant SP as SP (app.acme.com)
participant IdP as IdP (idp.corp.com)
User->>SP: GET /dashboard
SP-->>User: 302 → IdP + SAMLRequest
User->>IdP: GET /sso?SAMLRequest=...
IdP-->>User: Login page
User->>IdP: POST credentials
IdP-->>User: POST /saml/acs (SAMLResponse)
User->>SP: POST /saml/acs (SAMLResponse)
SP-->>User: Session created → /dashboard
As an example of a SAML Identity Provider we have Microsoft Azure’s Active Directory Federation Server (ADFS).
There are a few names that we will hear regularly. Let’s make it clear what they mean before anything else:
- Identity Provider (IdP): also known as the asserting party, this is the source of truth, where all the information is stored when it comes to defining an identity: name, email, groups, roles, etc.;
- Subject: also known as principal, is usually a human user, but may be a service or any other system;
- Service Provider (SP): also known as the relying party, this is any application that the subject intends to use, and which will consume and trust the information provided by the IdP.
This is the establishment of trust between an IdP and an SP when they are governed by different authorities. Each has its own security domain and data isolation, but both agree on a shared name identifier for a specific user, so that the identity established at the IdP is honoured by the SP without separate credentials.
As an example, a username may be referenced as john.doe on the IdP and as john_doe at the SP, and yet both parties recognise them as the same person because both hold the same name identifier (usually opaque, such as a UUID).
An assertion allows for one party to assert security information in the form of statements about a subject. Example: a subject named “John Doe” has an email address of john.doe@example.com and is a member of the “engineering” group.
There are three kinds that can be carried with an assertion:
- Authentication Statements: created by the identity provider, it describes the particular means and the specific time for the authentication;
- Attribute Statements: contain specific identifying attributes about the subject (e.g. “John Doe” belongs to the “engineering” group);
- Authorization Decision Statements: define something that the subject is entitled to do (e.g. “John Doe” may delete “comments”).
The <saml:Assertion> element is the envelope where all statements are included, along with other data regarding the issuer or the conditions on which the assertion is valid.
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="_a75adf55-01d7-40cc-929f-dbd8372ebdfc" IssueInstant="2024-03-28T10:42:00Z">
<!-- WHO issued the assertion --> <saml:Issuer>https://idp.example.com</saml:Issuer>
<!-- WHOM the assertion is about (the Subject) --> <saml:Subject> <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"> a7f3c9e1-4b2d-4f8a-9c1e-2d6b8f0a3c5d </saml:NameID> </saml:Subject>
<!-- WHEN is the assertion valid --> <saml:Conditions NotBefore="2025-03-28T10:42:00Z" NotOnOrAfter="2024-03-28T10:52:00Z"/>
<!-- AUTHENTICATION STATEMENT: "logged in this way, at this time" --> <saml:AuthnStatement AuthnInstant="2025-06-28T10:42:00Z"> <saml:AuthnContext> <saml:AuthnContextClassRef> urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport </saml:AuthnContextClassRef> </saml:AuthnContext> </saml:AuthnStatement>
<!-- ATTRIBUTE STATEMENT: "facts about the subject" (e.g. roles and groups) --> <saml:AttributeStatement> <saml:Attribute Name="email"> <saml:AttributeValue>john.doe@example.com</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="group"> <saml:AttributeValue>engineering</saml:AttributeValue> </saml:Attribute> </saml:AttributeStatement>
</saml:Assertion>Assertions should be signed and may be encrypted to guarantee authenticity, integrity, and confidentiality.
Signing: performed by the IdP using its private key. The SP verifies the signature using the IdP’s public key (obtained from the IdP Metadata). This proves the assertion was issued by a trusted IdP and was not tampered with in transit.
Encryption: the assertion is encrypted with a one-time symmetric key, and that key is itself encrypted with the SP’s public key, so that only the SP can unwrap it. Since SAML signs then encrypts, the signature ends up inside the encrypted text, and therefore the SP must first decrypt and then verify.
The participants talk to each other by exchanging SAML Requests and SAML Responses. A request is sent from the SP to the IdP and the response from the IdP to the SP.
It is worth noting that any communication initiated by the IdP is called a SAML Response, even in the case of an IdP-initiated SSO, for example, which has no previous SAML Request to which to respond.
These are abstract protocol messages and how they’re serialised depends on the Binding being used (e.g. SAMLRequest and SAMLResponse for HTTP parameters or body elements for SOAP.
Here is an example that is likely the most common request: <samlp:AuthnRequest>. This is what the SP sends to start the login. It identifies the SP (Issuer), tells the IdP where to send the answer (AssertionConsumerServiceURL), and how (ProtocolBinding):
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_abc123" Version="2.0" IssueInstant="2026-06-29T12:00:00Z" AssertionConsumerServiceURL="https://app.acme.com/saml/acs" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"> <saml:Issuer>https://app.acme.com</saml:Issuer></samlp:AuthnRequest>The IdP answers with a <samlp:Response>, containing <samlp:Status>, and correlation fields such as InResponseTo that contains the ID of the original request and the destination (the SP’s ACS endpoint), as well as the assertions.
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_response123" InResponseTo="_abc123" Destination="https://app.acme.com/saml/acs" IssueInstant="2026-06-29T12:00:05Z" Version="2.0">
<saml:Issuer>https://idp.corp.com</saml:Issuer>
<!-- Whether authentication succeeded --> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </samlp:Status>
<!-- The assertion carrying the statements about the subject --> <saml:Assertion ...> ... </saml:Assertion>
</samlp:Response>Note that InResponseTo="_abc123" matches the request’s ID="_abc123", as that is how the SP correlates the response to the request it sent.
SAML defines protocols that standardise how a message should be structured, what fields may be used and what they mean:
- Authentication Request Protocol: how a subject, or an agent on behalf of the subject (e.g. web browser) may request authentication statements to the IdP. The Web Browser SSO Profile uses this protocol when redirecting a user from an SP to an IdP, but we’ll talk more about that later;
- Single Logout Protocol: propagates the logout across all sessions, for example logging out of an email app would make the IdP send a logout request to every other SP to which the user is signed in;
- Assertion Query and Request Protocol: defines queries by which SAML assertions may be obtained, such as a request from the SP in order to fetch all groups to which a subject belongs on demand;
- Artifact Resolution Protocol: used for communication by reference instead of value, this protocol can be useful for hiding sensitive information from the agent. Instead of sending the whole message, the IdP sends a token, with which the SP can obtain the full message directly;
- Name Identifier Management Protocol: used for changing the name identifier or entirely terminating the federation of a subject;
- Name Identifier Mapping Protocol: since each subject’s name identifier is opaque and unique for each IdP and SP federation, an SP “A” that needs to act on behalf of the user at SP “B” has no way to determine what the name identifier on SP “B” is. This protocol lets SP “A” ask the IdP for the identifier by which the subject is known at SP “B”. When previously authorised by policy, the IdP returns the encrypted name identifier so that SP “A” can forward it to SP “B” but never read it. This preserves privacy/isolation between SPs while still enabling delegation.
Below is an example of the Single Logout Protocol:
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_logout789" Version="2.0" IssueInstant="2026-06-29T13:30:00Z" Destination="https://idp.corp.com/slo"> <saml:Issuer>https://app.acme.com</saml:Issuer> <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"> a7f3c9e1-4b2d-4f8a-9c1e-2d6b8f0a3c5d </saml:NameID> <samlp:SessionIndex>_session456</samlp:SessionIndex></samlp:LogoutRequest>It shows the two things a logout message needs: who to log out (NameID) and which session to terminate (SessionIndex).
A binding defines how messages should be transported:
- HTTP Redirect Binding: how messages are transported in an HTTP redirect URL, compressed to fit within URL length limits;
- HTTP Post Binding: how messages are transported inside a hidden HTML form field, where there is no length limit;
- HTTP Artifact Binding: how an artifact is transported between sender and receiver using HTTP form control or a query string in the URL;
- SAML SOAP Binding: how messages are transported with SOAP 1.1.
- Reverse SOAP (PAOS) Binding: how message exchange permits an HTTP client to be a SOAP responder;
- SAML URI Binding: how to retrieve existing SAML assertions by resolving a URI;
Below is an example of an HTTP Post Binding:
<form method="POST" action="https://app.acme.com/saml/acs"> <input type="hidden" name="SAMLResponse" value="PHNhbWxwOl..." /></form>This is what allows self-configuration between the IdP and the SP. It is an XML document that describes a SAML participant: its endpoints, supported bindings, and public keys. IdPs and SPs exchange metadata outside the SAML protocol itself (e.g. by URL or file upload), and with that they establish trust before any assertion is ever exchanged.
On the IdP’s side, a typical metadata document informs the following to the SP:
- the SSO endpoint URL (where to send
AuthnRequest); - the Single Logout endpoint URL;
- the Public key certificate (used by the SP to verify assertion signatures).
And on the SP’s side, a typical metadata document tells the IdP:
- the ACS (Assertion Consumer Service) URL (where to POST the
SAMLResponse); - the public key certificate (used by the IdP to encrypt assertions).
The following is an example of a Metadata published by the IdP and consumed by the SP:
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://idp.corp.com"> <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<!-- public key the SP uses to verify assertion signatures --> <md:KeyDescriptor use="signing"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data><ds:X509Certificate>MIICaj...</ds:X509Certificate></ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor>
<!-- where the SP should send the AuthnRequest --> <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.corp.com/sso"/>
<!-- where the SP should send the logout requests --> <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.corp.com/slo"/>
</md:IDPSSODescriptor></md:EntityDescriptor>And now an example of Metadata published by the SP and consumed by the IdP:
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://app.acme.com"> <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<!-- public key the IdP uses to encrypt assertions --> <md:KeyDescriptor use="encryption"> <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> <ds:X509Data><ds:X509Certificate>MIICbb...</ds:X509Certificate></ds:X509Data> </ds:KeyInfo> </md:KeyDescriptor>
<!-- where the IdP POSTs the SAMLResponse --> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://app.acme.com/saml/acs" index="0"/>
</md:SPSSODescriptor></md:EntityDescriptor>A profile serves as a recipe, defining how protocols and bindings may be combined to solve well-known operations. Anyone may create a profile and make it public, as it is merely an implementation strategy.
- Web Browser SSO Profile: SP-initiated or IdP-initiated login flow via a web browser;
- Enhanced Client and Proxy (ECP) Profile: SSO for non-browser clients (e.g. CLI tools);
- Identity Provider Discovery Profile: how a user’s IdP is discovered when there are multiple IdPs available;
- Single Logout Profile: propagates logout across all active SP sessions via the IdP;
- Assertion Query/Request Profile: how an SP queries the IdP for assertions on demand;
- Name Identifier Management Profile: how name identifiers are updated or federation is terminated;
- Name Identifier Mapping Profile: how one SP obtains the name identifier of a subject as known by another SP.
Below is an example of the Single Logout Profile:
sequenceDiagram
actor User
participant SP as SP (app.acme.com)
participant IdP as IdP (idp.corp.com)
participant SP2 as SP2 (other.acme.com)
User->>SP: GET /logout
SP->>IdP: LogoutRequest [Redirect Binding]
IdP->>SP2: LogoutRequest [Redirect Binding]
SP2-->>IdP: LogoutResponse
IdP-->>SP: LogoutResponse
SP-->>User: Logged out
SAML was designed to assert who we are to an application we’re logging into. It was not designed, however, to let one service act on behalf of a user when calling another service. This becomes a problem in three common scenarios: a front-end application calling multiple service APIs, where each service would have to independently hold and validate the IdP metadata; a back-end application calling an API owned by a third party, where there is no way to impose that requirement; and scheduled jobs, where there is no user in the loop at all.
SAML is great for SSO, especially across different data centres. However, it was designed for authentication. Even though it is possible to secure REST APIs with it in most cases, it is also technically cumbersome. If we need to delegate or make service-to-service calls, it might be the wrong tool.
This is, however, addressed by OAuth 2.0: a delegation framework that allows a user to grant a third-party service limited access to their resources without sharing credentials. OAuth does not replace SAML, though. They may complement one another, as SAML handles who the user is and OAuth handles what a service may do on their behalf.
SAML may also not be the best fit for modern single-page applications and mobile clients, given that they cannot participate in browser-redirect flows as naturally as web browsers. This is addressed by OpenID Connect, which is a lightweight identity layer built on top of OAuth, and serves as a modern alternative to SAML SSO. It uses JSON and JWT instead of XML, and integrates naturally with the same token-based flows that OAuth already defines.