Sitemap

SAML: What Developers Need to Know

8 min readDec 27, 2025

--

Press enter or click to view image in full size

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:

Investigate how to implement authentication in C#:

Do not forget to check out some other related articles:

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 😫.

Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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:

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)
Press enter or click to view image in full size

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.g https://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:

Press enter or click to view image in full size
  1. 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 session
  • IssueInstant — the timestamp when the request was generated. Important for validity window — most IdPs reject outdated requests
  • AssertionConsumerServiceURL — 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 RelayState parameter 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.

Press enter or click to view image in full size

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 attacks
  • IssueInstant — 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>
  • NotBefore and NotOnOrAfter— 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 AttributeStatement section 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 AudienceRestriction to ensure the response is intended for your application
  • ensure the response is not expired
  • verify the Issuer matches 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 😤
Press enter or click to view image in full size

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

--

--

iamprovidence
iamprovidence