Skip to main content
Technology & EngineeringAuth Patterns255 lines

Sso Saml

SSO and SAML integration for enterprise identity federation and service provider setup

Quick Summary27 lines
You are an expert in Single Sign-On (SSO) and SAML 2.0 for enterprise identity federation. You understand Service Provider (SP) and Identity Provider (IdP) roles, SAML assertion processing, attribute mapping, and integrating with enterprise directory services.

## Key Points

- **Identity Provider (IdP)**: The authoritative system that authenticates users and issues SAML assertions (e.g., Okta, Azure AD, PingFederate).
- **Service Provider (SP)**: Your application, which consumes SAML assertions to authenticate users.
- **SAML Assertion**: A signed XML document containing authentication statements, attribute statements, and authorization decision statements.
- **SP-Initiated Flow**: The user starts at the SP, which redirects to the IdP for authentication.
- **IdP-Initiated Flow**: The user starts at the IdP portal and is directed to the SP with an assertion.
- **Assertion Consumer Service (ACS)**: The SP endpoint that receives and processes SAML responses.
- **Metadata**: XML documents describing the configuration of an IdP or SP (endpoints, certificates, bindings).
- **NameID**: The primary subject identifier in the assertion (typically email or a persistent opaque ID).
- **Attribute Mapping**: Mapping IdP-provided attributes (e.g., `firstName`, `department`, `groups`) to SP user fields.
- **RelayState**: An opaque value that preserves the user's intended destination across the SSO redirect.
1. **Always validate the SAML assertion signature** against the IdP's known certificate. Never skip signature verification.
2. **Require encrypted assertions** (`allow_unencrypted_assertion: false`) for production deployments.

## Quick Example

```javascript
app.get('/saml/metadata', (req, res) => {
  res.type('application/xml');
  res.send(sp.create_metadata());
});
```
skilldb get auth-patterns-skills/Sso SamlFull skill: 255 lines
Paste into your CLAUDE.md or agent config

SSO and SAML Integration — Authentication & Authorization

You are an expert in Single Sign-On (SSO) and SAML 2.0 for enterprise identity federation. You understand Service Provider (SP) and Identity Provider (IdP) roles, SAML assertion processing, attribute mapping, and integrating with enterprise directory services.

Core Philosophy

Overview

SAML 2.0 (Security Assertion Markup Language) is an XML-based standard for exchanging authentication and authorization data between an Identity Provider (IdP) and a Service Provider (SP). It enables enterprise Single Sign-On: users authenticate once with their organization's IdP (e.g., Okta, Azure AD, OneLogin) and gain access to multiple SPs without re-entering credentials. SAML remains the dominant SSO protocol in enterprise environments, though OIDC is increasingly used for newer integrations.

Core Concepts

  • Identity Provider (IdP): The authoritative system that authenticates users and issues SAML assertions (e.g., Okta, Azure AD, PingFederate).
  • Service Provider (SP): Your application, which consumes SAML assertions to authenticate users.
  • SAML Assertion: A signed XML document containing authentication statements, attribute statements, and authorization decision statements.
  • SP-Initiated Flow: The user starts at the SP, which redirects to the IdP for authentication.
  • IdP-Initiated Flow: The user starts at the IdP portal and is directed to the SP with an assertion.
  • Assertion Consumer Service (ACS): The SP endpoint that receives and processes SAML responses.
  • Metadata: XML documents describing the configuration of an IdP or SP (endpoints, certificates, bindings).
  • NameID: The primary subject identifier in the assertion (typically email or a persistent opaque ID).
  • Attribute Mapping: Mapping IdP-provided attributes (e.g., firstName, department, groups) to SP user fields.
  • RelayState: An opaque value that preserves the user's intended destination across the SSO redirect.

Implementation Patterns

SP Configuration (Node.js with saml2-js)

const saml2 = require('saml2-js');
const fs = require('fs');

// Service Provider configuration
const sp = new saml2.ServiceProvider({
  entity_id: 'https://app.example.com/saml/metadata',
  private_key: fs.readFileSync('./keys/sp-private.pem', 'utf8'),
  certificate: fs.readFileSync('./keys/sp-cert.pem', 'utf8'),
  assert_endpoint: 'https://app.example.com/saml/acs',
  sign_get_request: true,
  allow_unencrypted_assertion: false,
});

// Identity Provider configuration (from IdP metadata)
function createIdpFromMetadata(tenantConfig) {
  return new saml2.IdentityProvider({
    sso_login_url: tenantConfig.ssoUrl,
    sso_logout_url: tenantConfig.sloUrl,
    certificates: [tenantConfig.idpCertificate],
  });
}

SP-Initiated Login Flow

// Step 1: Redirect user to IdP
app.get('/saml/login', async (req, res) => {
  const tenant = await getTenantByDomain(req.query.domain);
  if (!tenant?.samlConfig) {
    return res.status(404).json({ error: 'SSO not configured for this domain' });
  }

  const idp = createIdpFromMetadata(tenant.samlConfig);
  const relayState = req.query.returnTo || '/dashboard';

  sp.create_login_request_url(idp, { relay_state: relayState }, (err, loginUrl) => {
    if (err) return res.status(500).json({ error: 'Failed to create SAML request' });
    res.redirect(loginUrl);
  });
});

// Step 2: Process SAML Response at ACS endpoint
app.post('/saml/acs', async (req, res) => {
  // Determine which IdP issued this response
  // Parse the response to extract the issuer before full validation
  const issuer = extractIssuerFromResponse(req.body.SAMLResponse);
  const tenant = await getTenantByIdpIssuer(issuer);

  if (!tenant?.samlConfig) {
    return res.status(403).json({ error: 'Unknown identity provider' });
  }

  const idp = createIdpFromMetadata(tenant.samlConfig);

  sp.post_assert(idp, { request_body: req.body }, async (err, samlResponse) => {
    if (err) {
      console.error('SAML assertion validation failed:', err.message);
      return res.status(403).json({ error: 'Invalid SAML response' });
    }

    const nameId = samlResponse.user.name_id;
    const attributes = samlResponse.user.attributes;

    // Map SAML attributes to user fields
    const email = attributes.email?.[0] || nameId;
    const firstName = attributes.firstName?.[0] || attributes.givenName?.[0];
    const lastName = attributes.lastName?.[0] || attributes.surname?.[0];
    const groups = attributes.groups || attributes.memberOf || [];

    // Find or create user in the tenant
    let user = await findUserByEmail(tenant.id, email);
    if (!user) {
      user = await createUser({
        tenantId: tenant.id,
        email,
        firstName,
        lastName,
        ssoNameId: nameId,
        authMethod: 'saml',
      });
    } else {
      // Update attributes on each login (IdP is source of truth)
      await updateUser(user.id, { firstName, lastName, lastLoginAt: new Date() });
    }

    // Sync group-based role assignments
    await syncRolesFromGroups(user.id, tenant.id, groups);

    // Create application session
    req.session.regenerate((err) => {
      req.session.userId = user.id;
      req.session.tenantId = tenant.id;
      req.session.authMethod = 'saml';

      const relayState = req.body.RelayState || '/dashboard';
      res.redirect(relayState);
    });
  });
});

SP Metadata Endpoint

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

Group-to-Role Mapping

async function syncRolesFromGroups(userId, tenantId, idpGroups) {
  const tenant = await getTenant(tenantId);
  const roleMapping = tenant.samlConfig.groupRoleMapping;
  // Example mapping: { "Engineering": "developer", "Admins": "admin", "All Staff": "member" }

  const assignedRoles = new Set();
  for (const group of idpGroups) {
    const role = roleMapping[group];
    if (role) assignedRoles.add(role);
  }

  // Default role if no groups matched
  if (assignedRoles.size === 0) {
    assignedRoles.add('member');
  }

  // Replace existing SAML-sourced roles (preserve manually assigned roles)
  await db.userRoles.deleteMany({
    userId,
    tenantId,
    source: 'saml',
  });

  for (const roleName of assignedRoles) {
    const role = await db.roles.findOne({ name: roleName, tenantId });
    if (role) {
      await db.userRoles.create({
        userId,
        tenantId,
        roleId: role.id,
        source: 'saml',
      });
    }
  }
}

SAML Configuration Admin API

app.put('/api/admin/sso/saml', authenticate, requireRole('admin'), async (req, res) => {
  const { idpMetadataUrl, idpMetadataXml, groupRoleMapping } = req.body;

  let metadata;
  if (idpMetadataUrl) {
    // Fetch and parse IdP metadata from URL
    const response = await fetch(idpMetadataUrl);
    metadata = parseSamlMetadata(await response.text());
  } else if (idpMetadataXml) {
    metadata = parseSamlMetadata(idpMetadataXml);
  } else {
    return res.status(400).json({ error: 'Provide idpMetadataUrl or idpMetadataXml' });
  }

  await db.tenants.updateOne(
    { _id: req.user.tenantId },
    {
      $set: {
        'samlConfig.ssoUrl': metadata.ssoUrl,
        'samlConfig.sloUrl': metadata.sloUrl,
        'samlConfig.idpCertificate': metadata.certificate,
        'samlConfig.idpIssuer': metadata.entityId,
        'samlConfig.groupRoleMapping': groupRoleMapping || {},
        'samlConfig.enabled': true,
      },
    }
  );

  res.json({
    message: 'SAML SSO configured',
    acsUrl: 'https://app.example.com/saml/acs',
    metadataUrl: 'https://app.example.com/saml/metadata',
    entityId: 'https://app.example.com/saml/metadata',
  });
});

Best Practices

  1. Always validate the SAML assertion signature against the IdP's known certificate. Never skip signature verification.
  2. Require encrypted assertions (allow_unencrypted_assertion: false) for production deployments.
  3. Validate audience restriction — the assertion's Audience must match your SP entity ID.
  4. Check assertion timing: Validate NotBefore and NotOnOrAfter conditions, accounting for reasonable clock skew (60 seconds max).
  5. Consume each assertion only once — track the assertion ID (InResponseTo) to prevent replay attacks.
  6. Support IdP certificate rotation: Accept multiple certificates during a rollover period. Monitor for upcoming expirations.
  7. Implement Just-In-Time (JIT) provisioning: Create user accounts on first SAML login rather than requiring pre-provisioning.
  8. Treat the IdP as the source of truth for user attributes and group memberships. Sync on every login.

Common Pitfalls

  • Not validating the XML signature properly: XML Signature Wrapping (XSW) attacks can inject malicious content. Use a well-maintained SAML library, not custom XML parsing.
  • Accepting assertions from any IdP: Always verify that the assertion's issuer matches the expected IdP for the tenant. A multi-tenant SP must look up the correct IdP configuration per tenant.
  • Ignoring IdP certificate expiration: SAML IdP certificates expire (often annually). If the certificate is not updated, all SSO logins fail simultaneously.
  • Hard-coding a single IdP: In multi-tenant applications, each tenant needs its own IdP configuration.
  • No fallback authentication: If the IdP is down, locked-out admins need an alternative way to access the application (e.g., a break-glass admin account with MFA).
  • Not implementing Single Logout (SLO): Users expect that logging out of the IdP also logs them out of your application. Without SLO, sessions persist after IdP logout.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add auth-patterns-skills

Get CLI access →