SAML: What Developers Need to Know
Have you heard of SAML?
SAML is a battle-tested backbone of enterprise authentication. While modern solutions like OpenID Connect are gaining popularity, SAML is still widely used in corporate environments.
As a developer, knowing how to integrate and troubleshoot SAML can be a valuable skill — especially when building or integrating with enterprise-grade applications. Want to know more? Stay with me on this one.
By the way, you can always revisit other articles about authentication:
- Authentication history (Basic, Digest, Cookie, Session, Token, JWT, SSO, API key, HMAC)
- Authorization models (IBAC, RBAC, PBAC, ABAC, ReBAC, ACL, DAC, MAC)
- Token gang (Bearer token, Reference token, Opaque token, Self-contained token, JWT, Access token, ID token, Refresh token)
- OAuth 2.0 and OpenID in simple terms
- SAML: What Developers Need to Know
Investigate how to implement authentication in C#:
- Cookie-based Authentication in ASP
- Authorization in ASP
- JWT Authentication
- Identity Server integration with ASP MVC
- Identity Server for microservices
- Identity Server integration with SPA
- SSO with Identity server
Do not forget to check out some other related articles:
- What is the authentication schema in ASP about?
- Dynamic Authorization Policies in ASP
- Encapsulate authentication with DelegatingHandler
- How to parse token on client side
Meanwhile, we are starting.
What is SAML (Secure Assertion Markup Language)?
Before, users had to register multiple accounts across different systems. Imagine how inconvenient it was 😫.
The Single Sign-On (SSO) allows users to have an account in one system (let’s say Google) and log in to multiple apps (mail, drive, calendar, etc).
One of the first protocols that adopted SSO was SAML.
While SAML 1.0 and 1.1 are dead (as they relied on SOAP), you can still use SAML 2.0.
The idea behind SAML is simple: you still have a user and the application. However, when a user needs to login, he is redirected to another server.
SAML introduces specific wording for those applications:
- Service Provider (SP) — an application that provides services to the users
- Identity Provider (IDP) — a centralized service that creates, maintains, and manages the identity information of the users
A decoupled Identity Provider means:
- reuse of the login flow across different applications
- a session can be shared. Once the user is logged in, he does not need to enter his credentials in another application
It also means:
- separation of concerns
- encapsulation
- single responsibility
- divide and conquer principles
- other fancy words we developers like🙃
So, here you have it:
SAML is a standard that allows service providers to authenticate users through identity providers
PreRequirements
Before applying SAML, there are a few prerequisites that should be met.
Both SP and IdP should support it. For that, the next endpoints have to be exposed:
- Identity Provider (IdP) — needs to accept SAML request and display login form
- Service Provider (SP) — must support SAML response through Assertion Consumer Service URL (ACS)
Additionaly, applications must exchange metadata:
- SP needs to share:
- ACS URL (e.g.http://sp-app.com/saml/acs)
- Entity ID — a unique identifier for your app. You can use any string, but developers often use the server URL (e.g.http://sp-app.com) - IdP, on its end, will provide
- SSO Login URL (e.ghttps://idp-server.com/saml/log-in)
- X.509 certificate — a document that SP should store to verify IdP response:
-----BEGIN CERTIFICATE-----
EqHMwbsxqYlXzJCcqwebPTZVHPUnYzpoBbOnoPBvDHJjvEGRccybPTZVHPUnYzp
IaXpNbwTQebQdhUSizfIubcadJgUfHEoBbOnoPBvDHJZnALwzrZnALwzrZnALwz
bnAOVyIugZnaeLzrDyKnnfWqpZcwAenMoBbOnoPBvDHJjvEGRccycwAenMoBbcw
NYdreFvnZxYmAQTLQXszTAcxAfapoPONYdreFvnZxYmAQmTLQzTzTAcxAfapoPO
foIVqWzgBFRBxqaAQhoavfryfRRIvHQEUxyqQaXntRHfpTOBOwkywDZzQiwUYLs
EUxyqQaXntRHfpOBOwkywDZzQiwUYLsgeHGOXMvKMrrGnWmMDGnjXpqqsKcEJsa
pAiHdeGytCouRwjFCtvxjRrenBqXVXgOXMvKMrrGnWmMDGnjXpqqsKcEJasdasd
JnmBgzIFVnyZGTMclDsUAXoTOKFmxlgJnmBgzIFVnyZGTXMclDsUAXoTOKFmxlg
DCabxtOMGwoFNydZJhtoyAVYOdxcjSpDCabxtOMGwoFNyKdZJhtoyAVYOdxcjSp
geHGOXMvKMrrGngMDGnjXpqqsKcEJtNhFwhzkpxXAzNWlwXQLdFxeEVmSiCOBcy
HYwdlHoPqZepWTmSEqHMwbsxqYlXzJDCczPxWgUPPkNlGBhowOflHpFRPXUcdbg
CczPxWgUPPkNlGBowOflHpFRPXUcdbghFwhzkpxXAzNWlwXQLdFxeEVmSiCOBcy
hFwhzkpxXAzNWlwQLdFxeEVmSiCOBcyZCtVNcVhnTKBCyVfOaBpkOgeLDpRDDbg
dWHJivHfRpDbsFNAlqFtsKBghygcoZqzwzrmfPJgwvGghGMqGXZCtVNcVhnTKBC
zwzrmfPJgwvGghGqGXZCtVNcVhnTKBCyVfOaBpkOgeLDpRDDbgMYpwlTNDJwBSB
yVfOaBpkOgeLDpRDbgMYpwlTNDJwBSBhFwhzkpxXAzNWlwXQLdFxeEVmSiCOBcy
-----END CERTIFICATE-----When configurations are exchanged, you can start using SAML.
Do not stress too much about it. You will see below how everything is used.
SAML Flow
Let’s assume that the user has an account in the Identity Provider. A typical SAML flow will have the following look:
- First, the Service Provider should generate a SAML request (AuthnRequest).
The AuthnRequest is an XML document that needs to be sent to the IdP.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
Here’s a simplified example of how this request might be constructed:
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="unique-request-id"
Version="2.0"
IssueInstant="2013-01-25T00:00:00Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="https://sp-app.com/saml/acs">
<saml:Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
your-app-entity-id
</saml:Issuer>
<samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"/>
</samlp:AuthnRequest>Note the following parameters:
ID— a unique identifier for this request. The IdP will reference this ID in its response. Used to prevent replay attacks and track the sessionIssueInstant— the timestamp when the request was generated. Important for validity window — most IdPs reject outdated requestsAssertionConsumerServiceURL— the ACS callback URL (on your app) where the IdP will send the SAML Response after login
if you want to persist state between redirects, you can add
RelayStateparameter to the URL that IdP must return unmodified
e.g.,https://idp.com/sso?SAMLRequest=...&RelayState=tenantId2702
<saml:Issuer>— this identifies your application (the SP). Must match the SP EntityId that the IdP trusts
2. Once the AuthnRequest is generated, you need to redirect the user to the Identity Provider login URL:
HTTP GET: https://idp-server.com/saml/log-in?SAMLRequest={base64EncodedSamlRequest}IdP will display a login form. In this form, the user enters their login and password.
Upon entering, the SAML IdP verifies the credentials against its Database
3. Once verified, a SAML response is sent to the Service Provider Assertion Consumer Service (ACS) endpoint.
This will typically be a POST request with a SAMLResponse parameter, which is a base64-encoded XML document.
Content-Type: application/x-www-form-urlencoded // format is important!
// Service Provider
HTTP POST: https://sp-app.com/saml/acs
SAMLResponse={xmlResponse}Once decoded, you will get the following XML document:
<samlp:Response
ID="_a4958bfd-e107-4e67-b06d-0d85ade2e76a"
Version="2.0"
IssueInstant="2013-03-18T07:38:15.144Z"
Destination="https://sp-app.com/saml/acs"
InResponseTo="unique-request-id"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
<Assertion
ID="_bf9c623d-cc20-407a-9a59-c2d0aee84d12"
IssueInstant="2013-03-18T07:38:15.144Z"
Version="2.0"
xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
<Issuer>
https://idp-server.com
</Issuer>
<Conditions NotBefore="2013-03-18T07:38:15.128Z" NotOnOrAfter="2013-03-18T08:48:15.128Z">
<AudienceRestriction>
<Audience>https://sp-app.com</Audience>
</AudienceRestriction>
</Conditions>
<ds:Signature xmlns:ds="https://www.w3.org/2000/09/xmldsig#">
...
</ds:Signature>
<AttributeStatement>
...
</AttributeStatement>
</Assertion>
</samlp:Response>Yeah. I know. Ducking XML😒.
However, there is a good side to it. SAML flow is over 😀. We just need to understand and properly handle its response. And in fact, this XML document is not much harder to comprehend than JWT. Let’s see.
Making sense of the SAML response
Response
Everything starts with a Response element.
<samlp:Response
ID="_a4958bfd-e107-4e67-b06d-0d85ade2e76a"
Version="2.0"
IssueInstant="2013-03-18T07:38:15.144Z"
Destination="https://sp-app.com/saml/acs"
InResponseTo="unique-request-id"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
...
</samlp:Response>There is almost nothing interesting here:
InResponseTo—is a unique ID that was sent in the AuthnRequest to prevent replay attacksIssueInstant— the timestamp when the response was generated. You can reject outdated responses
Other than that, we you won’t find anything useful here.
Status
Inside the Response you have Status.
As expected, it conveys the success or failure of sign-on. But can also include additional information, like StatusCode and StatusMessage.
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester">
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported" />
</samlp:StatusCode>
<samlp:StatusMessage>
An error occurred while processing a SAML2 Authentication request
</samlp:StatusMessage>
</samlp:Status>Assertion
This part is similar to the JWT claims. It contains information that can be used not only to verify the response but also to obtain the user’s data.
<Assertion
ID="_bf9c623d-cc20-407a-9a59-c2d0aee84d12"
IssueInstant="2013-03-18T07:38:15.144Z"
Version="2.0"
xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
...
</Assertion>For example, in Assertion, you can find information about the issuer (usually the URL of the server that issued the token):
<Issuer>
https://idp-server.com
</Issuer>Under what conditions the response is considered as accepted.
<Conditions
NotBefore="2013-03-18T07:38:15.128Z"
NotOnOrAfter="2013-03-18T08:48:15.128Z">
<AudienceRestriction>
<Audience>https://sp-app.com</Audience>
</AudienceRestriction>
</Conditions>NotBeforeandNotOnOrAfter— specify the interval during which the assertion is valid<Audience>— the recipient for which the response is intended (usually the URL of the server where the response will be used)
Also, we have the Signature element. It contains a digital signature that can be used to verify the integrity of the assertion.
<ds:Signature xmlns:ds="https://www.w3.org/2000/09/xmldsig#">
digital_signature_here
</ds:Signature>For the final, we have AttributeStatement.
This is nothing more than a good old list of user claims 😌.
<AttributeStatement>
<saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">
John
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">
Doe
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml:AttributeValue xsi:type="xs:string">
john.doe@mail.com
</saml:AttributeValue>
</saml:Attribute>
...
</AttributeStatement>Handling SAML Response
The way SP implements the ACS endpoint is up to you. Typically, you will:
- decode and parse the response
- validate the SAML response
- extract user information (such as the user’s name, email, roles, etc.) from
AttributeStatementsection of the response - for the final, create a user session in your application with Cookies, JWT, opaque token, or whatever suits your needs
[HttpPost("saml/acs")]
public IActionResult ACS([FromQuery]string state)
{
string samlResponse = Request.Form["SAMLResponse"];
// Decode the base64-encoded SAML response
string decodedResponse = Encoding.UTF8.GetString(Convert.FromBase64String(SAMLResponse));
// Validate the SAML response (signature, expiration, etc.)
if (!IsValidSamlResponse(decodedResponse))
{
return Unauthorized();
}
// Extract user information from the response
var user = ExtractUserInfo(decodedResponse);
// Create the user session (e.g., set cookies, session)
CreateSession(user);
return RedirectToAction("Home");
}The validation part may seem complex, but it is almost the same as what you have with JWT:
- verify the signature of the response to ensure it came from a trusted IdP
- check the
AudienceRestrictionto ensure the response is intended for your application - ensure the response is not expired
- verify the
Issuermatches your expected IdP - other checks you like 🙃
private bool IsValidSamlResponse(string response)
{
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
return
IsSignatureValid(xmlDoc) &&
IsValidAudience(xmlDoc) &&
IsValidIssuer(xmlDoc) &&
!IsExpired(xmlDoc);
}
private bool IsSignatureValid(XmlDocument xmlDoc)
{
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(idpCertificate));
var signedXml = new SignedXml(xmlDoc);
return signedXml.CheckSignature(certificate, verifySignatureOnly: true)
}
private bool IsExpired(XmlDocument xmlDoc)
{
var node = xmlDoc.SelectSingleNode("Conditions");
var expirationDate = DateTime.Parse(node.Attributes["NotOnOrAfter"].Value);
return _dateTimeService.UtcNow > expirationDate.ToUniversalTime();
}
. . .Just think about the SAML response token as an utterly obese JWT 😉.
Closing Remarks
While SAML is considered to be outdated, you can still find it appealing for its simplicity and robustness.
But it has some problems:
- it is designed for Server-side Web Apps
- poor support for JavaScript SPA or Native App like mobile App with URL limits (tough, but still doable though🙃)
- bulkiness caused by XML 😤
However, there are multiple Frameworks, NuGets, and libraries (like Sustainsys.Saml2) making SAML somehow more tolerable, making life more tolerable 😉.
👏 I hope that from now on, SAML is demystified for you. If so, clap a couple of times
☕️ You can support me using the link below
✅ Subscribe
🙃 And keep coding








