← Back to DevBytes

SAML Protocol: A Complete Reference Guide

Introduction to SAML

Security Assertion Markup Language (SAML) is an XML-based open standard for exchanging authentication and authorization data between parties, particularly between an identity provider (IdP) and a service provider (SP). Developed by the OASIS Security Services Technical Committee, SAML has become the backbone of enterprise single sign-on (SSO) implementations since its introduction in the early 2000s.

At its core, SAML solves a fundamental problem: how can a user authenticate once with a trusted authority and then access multiple independent applications without re-entering credentials? This is achieved through a carefully designed flow of XML documents called "assertions" that carry statements about a user's identity, attributes, and authorization decisions.

Key Terminology

Before diving deeper, it is essential to understand the vocabulary that permeates every SAML discussion:

Why SAML Matters

In modern enterprise environments, employees routinely interact with dozens of applications — HR systems, CRM platforms, code repositories, communication tools, and more. Without a federated identity solution, each application would require its own credential store, leading to password fatigue, increased support costs, and security risks from weak or reused passwords.

SAML addresses these challenges by centralizing authentication at the IdP while allowing applications to remain decoupled from the authentication mechanism. This separation yields several concrete benefits:

SAML Architecture and Message Flow

The most common SAML use case is Web Browser SSO, which comes in two primary initiation patterns: SP-initiated and IdP-initiated. Understanding both flows is critical for implementing and debugging SAML integrations.

SP-Initiated Flow

In an SP-initiated flow, the user attempts to access a protected resource on the service provider directly. The flow proceeds as follows:

IdP-Initiated Flow

In an IdP-initiated flow, the user starts at the IdP's portal and clicks a link to an application. The IdP generates an unsolicited assertion and posts it directly to the SP's ACS URL. This pattern is simpler but less common in modern architectures because it lacks the AuthnRequest correlation that helps prevent certain attacks.

The SAML Assertion Structure

A SAML assertion is an XML document with a well-defined schema. Below is a representative example of a SAML response containing an authentication assertion:

<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                ID="_8e8dc5f69a98cc4c1ff3427e5ce38606"
                Version="2.0"
                IssueInstant="2024-01-15T10:30:00Z"
                Destination="https://sp.example.com/acs">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
    https://idp.example.com
  </saml:Issuer>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <!-- XML Digital Signature covering the assertion -->
  </ds:Signature>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                  ID="_d71a3a8e9fcc102c0327e5ce38606"
                  Version="2.0"
                  IssueInstant="2024-01-15T10:30:00Z">
    <saml:Issuer>https://idp.example.com</saml:Issuer>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
        user@example.com
      </saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData
            NotOnOrAfter="2024-01-15T10:35:00Z"
            Recipient="https://sp.example.com/acs"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2024-01-15T10:29:00Z"
                     NotOnOrAfter="2024-01-15T10:35:00Z">
      <saml:AudienceRestriction>
        <saml:Audience>https://sp.example.com</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2024-01-15T10:30:00Z"
                         SessionIndex="_be9967abd894f2cc0327e5ce38606">
      <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="firstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue>Jane</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="lastName">
        <saml:AttributeValue>Doe</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="role">
        <saml:AttributeValue>admin</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

Each element plays a specific role. The Issuer identifies the IdP. The Subject identifies the authenticated principal via a NameID. The Conditions element defines the time window during which the assertion is valid and restricts which SPs may consume it. The AuthnStatement describes the authentication event, and the AttributeStatement carries claims about the user that the SP can use for authorization decisions.

SAML Bindings and Profiles

SAML defines several bindings that dictate how messages are transported over HTTP. The three most relevant for web applications are:

The Web Browser SSO profile combines these bindings into a coherent flow. The canonical pattern uses HTTP-Redirect for the AuthnRequest and HTTP-POST for the Response, though both directions can use POST as well.

Implementing a SAML Service Provider

To illustrate how SAML works in practice, let's build a minimal SAML service provider using Node.js and the saml2-js library. This example demonstrates generating an AuthnRequest, consuming the response, and validating the assertion.

Project Setup

First, initialize the project and install the required dependencies:

mkdir saml-sp-demo
cd saml-sp-demo
npm init -y
npm install express saml2-js body-parser

Configuring the Service Provider

Create a file named app.js and begin by configuring the SP with its entity ID, ACS URL, and signing certificate:

const express = require('express');
const saml2 = require('saml2-js');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.urlencoded({ extended: false }));

// Service Provider configuration
const sp_options = {
  entity_id: 'https://sp.example.com/metadata.xml',
  private_key: fs.readFileSync('./sp-key.pem', 'utf8'),
  certificate: fs.readFileSync('./sp-cert.pem', 'utf8'),
  assert_endpoint: 'https://sp.example.com/acs',
  force_authn: false,
  allow_unencrypted_assertion: true
};

const sp = new saml2.ServiceProvider(sp_options);

// Identity Provider configuration
const idp_options = {
  sso_login_url: 'https://idp.example.com/sso',
  sso_logout_url: 'https://idp.example.com/slo',
  certificates: fs.readFileSync('./idp-cert.pem', 'utf8'),
  force_authn: false,
  sign_get_request: false
};

const idp = new saml2.IdentityProvider(idp_options);

Initiating Authentication

Next, create a route that triggers the SP-initiated login flow by generating and sending an AuthnRequest to the IdP:

app.get('/login', (req, res) => {
  sp.create_login_request_url(idp, {}, (err, login_url, request_id) => {
    if (err) {
      console.error('Error creating login request:', err);
      return res.status(500).send('Authentication request failed');
    }
    // Store request_id to correlate with the response
    req.session.saml_request_id = request_id;
    res.redirect(login_url);
  });
});

Consuming the Assertion

The ACS endpoint receives the SAML response from the IdP. This is where validation and attribute extraction happen:

app.post('/acs', (req, res) => {
  const options = {
    request_body: req.body,
    allow_unencrypted_assertion: true
  };

  sp.post_assert(idp, options, (err, saml_response) => {
    if (err) {
      console.error('SAML validation error:', err);
      return res.status(401).send('Authentication failed');
    }

    // Extract user information from the assertion
    const user = {
      name_id: saml_response.user.name_id,
      session_index: saml_response.user.session_index,
      attributes: saml_response.user.attributes
    };

    // Establish a local application session
    req.session.user = user;
    req.session.saml_session_index = user.session_index;

    console.log('User authenticated:', user.name_id);
    console.log('Attributes:', user.attributes);

    res.redirect('/dashboard');
  });
});

Serving Metadata

Most IdPs require the SP to publish a metadata document containing its configuration. This endpoint generates that document dynamically:

app.get('/metadata.xml', (req, res) => {
  res.type('application/xml');
  res.send(sp.create_metadata());
});

Implementing Single Logout

Single Logout (SLO) ensures that terminating a session at one party propagates to all others. Here is a basic implementation:

app.get('/logout', (req, res) => {
  const options = {
    name_id: req.session.user.name_id,
    session_index: req.session.saml_session_index
  };

  sp.create_logout_request_url(idp, options, (err, logout_url) => {
    if (err) {
      console.error('Logout request error:', err);
      return res.status(500).send('Logout failed');
    }
    req.session.destroy();
    res.redirect(logout_url);
  });
});

Starting the Server

Finally, start the Express server:

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`SAML SP running on port ${PORT}`);
});

Validating SAML Responses

Validation is the most security-critical aspect of SAML integration. A failure to properly validate assertions can lead to authentication bypass. The following checks must be performed on every incoming response:

Below is a Python example using the python3-saml library that demonstrates these validation steps in a Flask application:

from flask import Flask, request, redirect, session
from saml2 import BINDING_HTTP_POST
from saml2.client import Saml2Client
from saml2.config import SPConfig
import os

app = Flask(__name__)
app.secret_key = os.urandom(32)

# Load SP configuration from a JSON or Python dict
sp_config = SPConfig()
sp_config.load({
    'entityid': 'https://sp.example.com/metadata',
    'service': {
        'sp': {
            'endpoints': {
                'assertion_consumer_service': [
                    ('https://sp.example.com/acs', BINDING_HTTP_POST)
                ],
                'single_logout_service': [
                    ('https://sp.example.com/slo', BINDING_HTTP_POST)
                ]
            },
            'allow_unsolicited': False,
            'authn_requests_signed': True,
            'logout_requests_signed': True,
            'want_assertions_signed': True,
            'want_response_signed': True,
        }
    },
    'metadata': {
        'remote': [
            {'url': 'https://idp.example.com/metadata.xml'}
        ]
    },
    'key_file': './sp-key.pem',
    'cert_file': './sp-cert.pem',
    'xmlsec_binary': '/usr/bin/xmlsec1',
    'accepted_time_diff': 60,
})

saml_client = Saml2Client(config=sp_config)

@app.route('/login')
def login():
    req_id, authn_request = saml_client.prepare_for_authenticate()
    session['saml_request_id'] = req_id
    return redirect(authn_request)

@app.route('/acs', methods=['POST'])
def acs():
    authn_response = saml_client.parse_authn_request_response(
        request.form['SAMLResponse'],
        BINDING_HTTP_POST
    )

    if authn_response is None:
        return 'Invalid SAML response', 401

    # The library validates signature, timing, audience, and conditions
    session['user'] = {
        'name_id': authn_response.get_subject().text,
        'attributes': authn_response.ava,
        'session_index': authn_response.session_index
    }

    return redirect('/dashboard')

if __name__ == '__main__':
    app.run(port=5000)

Working with SAML Metadata

Metadata is the configuration contract between an IdP and an SP. It contains entity IDs, endpoint URLs, supported bindings, signing and encryption certificates, and other capabilities. Exchanging metadata is the standard way to establish trust between parties.

A typical SP metadata document looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
                      entityID="https://sp.example.com/metadata.xml">
  <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>MIIDXTCCAkWgAwIBAgIJALm...</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
    <md:AssertionConsumerService
        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        Location="https://sp.example.com/acs"
        index="0"
        isDefault="true"/>
    <md:SingleLogoutService
        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        Location="https://sp.example.com/slo"/>
  </md:SPSSODescriptor>
</md:EntityDescriptor>

When integrating with a new IdP, you typically download their metadata XML and register it in your SP configuration. Conversely, you provide your SP metadata to the IdP administrator. Many platforms support metadata URLs that are polled automatically, ensuring certificate rotations are picked up without manual intervention.

Best Practices

Security Best Practices

Operational Best Practices

NameID and Attribute Mapping

One of the most common sources of integration bugs is mismatched NameID formats and attribute names. Agree on these details with the IdP administrator before implementation:

Debugging SAML Issues

SAML's XML-heavy nature and browser-redirect flow can make debugging challenging. The following techniques help diagnose common problems:

Here is a quick Node.js snippet to decode a base64-encoded SAML response for manual inspection:

const zlib = require('zlib');

function decodeSamlResponse(encoded) {
  // SAML responses sent via POST are base64-encoded (not deflated)
  const xml = Buffer.from(encoded, 'base64').toString('utf8');
  return xml;
}

function decodeSamlRequest(encoded) {
  // SAML requests sent via Redirect are base64-encoded AND deflated
  const deflated = Buffer.from(encoded, 'base64');
  const xml = zlib.inflateRawSync(deflated).toString('utf8');
  return xml;
}

// Usage:
// const response = decodeSamlResponse(req.body.SAMLResponse);
// console.log(response);

SAML vs. Alternatives

While SAML remains dominant in enterprise SSO, it is important to understand how it compares to newer protocols:

In practice, many organizations run SAML and OIDC side by side, using SAML for legacy enterprise applications and OIDC for modern web and mobile apps. Most major IdPs support both protocols simultaneously.

Conclusion

SAML is a mature, battle-tested protocol that continues to power enterprise single sign-on across thousands of organizations. While its XML verbosity and complex message flows can feel daunting compared to modern alternatives like OpenID Connect, a solid understanding of its architecture, bindings, and validation requirements enables developers to build secure and reliable federated identity integrations. By leveraging well-maintained libraries, adhering to security best practices around signature validation and replay prevention, and establishing clear metadata exchange and attribute mapping agreements with identity providers, you can implement SAML-based authentication that provides a seamless user experience while maintaining a strong security posture. As you integrate SAML into your applications, remember that the protocol's strength lies not just in its technical design but in the disciplined operational practices that surround it — careful certificate management, thorough logging, and proactive testing across IdP implementations will ensure your integration remains robust for years to come.

— Ad —

Google AdSense will appear here after approval

← Back to all articles