Service Provider vs Identity Provider Initiated SAML Flows

I still remember the confusion I felt when I first encountered SAML authentication flows. "SP-initiated? IdP-initiated? Why are there different flows at all?" After implementing SAML in dozens of enterprise applications, I've developed a deep understanding of these flows and their important differences. Let me share what I've learned through my journey with SAML, Keycloak, and Node.js.

The "Click" Moment: Understanding the Two SAML Journeys

My lightbulb moment came when I visualized SAML flows as two different journeys to the same destination:

  • SP-initiated flow: The user starts their journey at your application and gets redirected to the identity provider for authentication

  • IdP-initiated flow: The user starts at the identity provider (like a company portal) and jumps directly to your application

This simple mental model completely changed how I approach SAML implementations. Let me walk you through both flows based on my real-world experience.

SP-Initiated Flow: The User Starts at Your Application

In my experience, SP-initiated flows are the most common scenario. Here's how I explain it:

Imagine you're visiting a museum (your application). When you try to enter a restricted exhibition, the guard (your app) asks for your membership card. Instead of verifying it themselves, they send you to the membership office (identity provider) where your identity is confirmed. You then return to the exhibition with proof of your membership, and the guard lets you in.

Here's the actual technical flow I implement:

What I love about SP-initiated flow is that it provides seamless authentication when users are already interacting with your application. The redirection to the IdP happens only when needed, making it feel like a natural part of the login process.

IdP-Initiated Flow: The User Starts at the Identity Provider

The IdP-initiated flow confused me at first, until I realized its value in enterprise environments. Here's how I visualize it:

Imagine you work at a large company with an internal portal (the IdP). From this portal, you can click on icons for various corporate applications. When you click on one, you're immediately taken to that application, already authenticated.

Here's the technical flow I've implemented in production systems:

The key difference I've learned is that in IdP-initiated flows, the SAML response is "unsolicited" - there's no corresponding AuthnRequest, which introduces some special handling requirements.

Real-World Enterprise Experience: Microsoft Entra (Azure AD) Integration

After years of working with Keycloak, I found myself in an enterprise environment where Microsoft Entra ID (formerly Azure AD) was the primary identity provider. The transition taught me valuable lessons about how different IdPs handle SAML flows and the unique challenges of enterprise-scale authentication.

My biggest "aha moment" came when I realized that Microsoft Entra ID's approach to SAML flows has some unique characteristics that differ from other IdPs. Let me share my experience implementing both SP-initiated and IdP-initiated flows with Microsoft Entra ID.

Setting Up Microsoft Entra Enterprise Application

When I first configured a SAML enterprise application in Microsoft Entra ID, I was impressed by how Microsoft has streamlined the process compared to other enterprise IdPs I've worked with. Here's my step-by-step approach:

  1. Created the Enterprise Application:

    • Navigated to Azure Portal → Entra ID → Enterprise Applications

    • Selected "New application" → "Create your own application"

    • Chose "Integrate any other application you don't find in the gallery (Non-gallery)"

  2. Configured SAML Settings:

Basic SAML Configuration:
- Identifier (Entity ID): https://myapp.com/saml/metadata
- Reply URL (ACS URL): https://myapp.com/saml/acs
- Sign on URL: https://myapp.com/login (for SP-initiated)
- Relay State: /dashboard (for IdP-initiated default landing)

Attributes & Claims:
- Name identifier format: Email address
- Name identifier value: user.mail
- Additional claims:
  - givenname: user.givenname
  - surname: user.surname
  - roles: user.assignedroles
  1. Downloaded the Federation Metadata:

    • This XML file contains all the certificates and endpoints I needed

    • Much more convenient than manually configuring each certificate

Microsoft Entra SP-Initiated Flow: My Implementation Experience

What fascinated me about Microsoft Entra's SP-initiated flow was how robust their AuthnRequest handling is. Here's the flow I observed in production:

What impressed me most was Microsoft's handling of Conditional Access policies during the authentication flow. Unlike simpler IdPs, Entra can enforce additional security checks like device compliance and location-based access, all transparently within the SAML flow.

Microsoft Entra IdP-Initiated Flow: The Enterprise Portal Experience

The IdP-initiated flow with Microsoft Entra really shines in enterprise environments. I've seen this used extensively with the Microsoft 365 portal and custom company portals. Here's the flow I implemented:

One thing that really stood out to me was how Microsoft Entra handles enterprise-specific claims in the IdP-initiated flow. I could configure it to include rich organizational data like department, manager hierarchy, and even custom extension attributes that were synced from on-premises Active Directory.

My Node.js Implementation for Microsoft Entra

When implementing the Node.js service provider for Microsoft Entra, I had to make several adjustments compared to my Keycloak implementation. Here's what I learned:

// entra-saml-config.js - Microsoft Entra specific configuration
const fs = require('fs');
const { Strategy } = require('passport-saml');

// Load the Federation Metadata XML from Microsoft Entra
// This is much easier than manually configuring certificates
const federationMetadata = fs.readFileSync('./entra-federation-metadata.xml', 'utf8');

const entraSamlStrategy = new Strategy({
  // Service Provider settings
  callbackUrl: 'https://myapp.com/saml/acs',
  entryPoint: 'https://login.microsoftonline.com/YOUR-TENANT-ID/saml2',
  issuer: 'https://myapp.com/saml/metadata',
  
  // Microsoft Entra specific settings I learned were important
  cert: extractCertFromFederationMetadata(federationMetadata), // Helper function
  signatureAlgorithm: 'sha256',
  digestAlgorithm: 'sha256',
  
  // Key differences for Microsoft Entra
  validateInResponseTo: true,
  requestIdExpirationPeriodMs: 7200000 // 2 hours (Entra tokens last longer)
}, (profile, done) => {
  // Microsoft Entra provides rich user information
  const user = {
    id: profile.nameID,
    email: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
    firstName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'],
    lastName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'],
    displayName: profile['http://schemas.microsoft.com/identity/claims/displayname'],
    
    // Enterprise-specific attributes that I found valuable
    department: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/department'],
    jobTitle: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/title'],
    officeLocation: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/office'],
    
    // Group memberships from Entra ID
    groups: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/groups'] || [],
    
    // Custom extension attributes (if configured)
    employeeId: profile['http://schemas.microsoft.com/identity/claims/extensionattribute1'],
    costCenter: profile['http://schemas.microsoft.com/identity/claims/extensionattribute2'],
    
    // Raw profile for debugging
    rawProfile: profile
  };
  
  return done(null, user);
});

// Helper function to extract certificate from Federation Metadata
function extractCertFromFederationMetadata(xml) {
  // In production, I use a proper XML parser like xml2js
  const certMatch = xml.match(/<X509Certificate>(.*?)<\/X509Certificate>/);
  if (certMatch) {
    return certMatch[1].replace(/\s/g, '');
  }
  throw new Error('Could not extract certificate from federation metadata');
}

module.exports = entraSamlStrategy;

Enterprise Integration Patterns I've Learned

Working with Microsoft Entra in large enterprises taught me several patterns that I now apply to other IdP integrations:

1. Handling Group-Based Authorization

// middleware/entra-authorization.js
const checkEntraGroups = (requiredGroups) => {
  return (req, res, next) => {
    if (!req.user || !req.user.groups) {
      return res.status(403).json({ error: 'No group information available' });
    }
    
    // Microsoft Entra groups come as GUIDs
    const userGroups = req.user.groups;
    const hasRequiredGroup = requiredGroups.some(group => userGroups.includes(group));
    
    if (!hasRequiredGroup) {
      return res.status(403).json({ 
        error: 'Insufficient permissions',
        requiredGroups: requiredGroups,
        userGroups: userGroups 
      });
    }
    
    next();
  };
};

// Usage in routes
app.get('/admin', 
  ensureAuthenticated,
  checkEntraGroups(['admin-group-guid', 'super-admin-group-guid']),
  (req, res) => {
    res.json({ message: 'Welcome to admin panel' });
  }
);

2. Handling Microsoft's Rich Claim Set

One challenge I encountered was that Microsoft Entra provides much richer user information compared to simpler IdPs. I learned to create a standardized user profile that could work across different IdPs:

// utils/user-profile-mapper.js
const mapEntraProfile = (samlProfile) => {
  return {
    // Standard fields
    id: samlProfile.nameID,
    email: samlProfile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
    name: samlProfile['http://schemas.microsoft.com/identity/claims/displayname'],
    
    // Enterprise fields
    organization: {
      department: samlProfile['http://schemas.microsoft.com/ws/2008/06/identity/claims/department'],
      title: samlProfile['http://schemas.microsoft.com/ws/2008/06/identity/claims/title'],
      office: samlProfile['http://schemas.microsoft.com/ws/2008/06/identity/claims/office'],
      manager: samlProfile['http://schemas.microsoft.com/identity/claims/manager']
    },
    
    // Security context
    groups: samlProfile['http://schemas.microsoft.com/ws/2008/06/identity/claims/groups'] || [],
    
    // Custom attributes
    custom: {
      employeeId: samlProfile['http://schemas.microsoft.com/identity/claims/extensionattribute1'],
      costCenter: samlProfile['http://schemas.microsoft.com/identity/claims/extensionattribute2']
    },
    
    // Metadata
    authMethod: 'entra-saml',
    lastLogin: new Date().toISOString()
  };
};

Lessons Learned from Microsoft Entra Integration

After implementing SAML with Microsoft Entra across several enterprise projects, here are my key takeaways:

  1. Federation Metadata is Your Friend: Unlike other IdPs, Microsoft provides comprehensive federation metadata that includes certificates, endpoints, and supported features. Always use this instead of manual configuration.

  2. Plan for Rich Claims: Microsoft Entra can provide extensive user information. Design your user profile schema to take advantage of organizational data like department, manager hierarchy, and custom attributes.

  3. Group Management is Critical: In enterprise environments, authorization is often group-based. Ensure your application can handle Microsoft's group GUIDs and consider implementing group-name mapping for better maintainability.

  4. Conditional Access Integration: Microsoft Entra's Conditional Access policies can impact your SAML flows. Be prepared to handle scenarios where users might be challenged for additional authentication even after successful SAML assertion.

  5. Token Lifetime Considerations: Microsoft Entra tokens typically have longer lifetimes than other IdPs. Adjust your session management accordingly.

The combination of Microsoft Entra's robust enterprise features and proper SAML implementation has enabled me to build applications that seamlessly integrate into large corporate environments while maintaining security and user experience standards.

My Real-World Implementation with Keycloak and Node.js

Let me share how I've implemented both flows using Keycloak as the IdP and Node.js as my Service Provider.

1. Setting Up Keycloak for Both Flows

First, I configured Keycloak to support both flows:

  1. I created a dedicated realm for my application

  2. I added a SAML client with these settings:

Client ID: my-nodejs-app
Client Protocol: saml
Valid Redirect URIs: https://myapp.com/saml/acs
Master SAML Processing URL: https://myapp.com/saml/acs
Force POST Binding: ON
Front Channel Logout: ON
Name ID Format: email
  1. For IdP-initiated flow support, I configured:

IDP Initiated SSO URL Name: my-app
IDP Initiated SSO Relay State: /dashboard
  1. I created a few test users and assigned them roles

2. My Node.js Implementation for SP-Initiated Flow

Here's how I implemented SP-initiated flow in Node.js (the more common scenario):

// app.js - SP-Initiated SAML with Keycloak
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const { Strategy } = require('passport-saml');
const fs = require('fs');
const path = require('path');

const app = express();

// Session configuration
app.use(session({
  secret: process.env.SESSION_SECRET || 'my-secret',
  resave: false,
  saveUninitialized: true,
  cookie: { 
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Load your private key for signing requests
const privateKey = fs.readFileSync(path.join(__dirname, 'certs', 'private.key'), 'utf8');

// Load the IdP's public certificate for verification
const idpCert = fs.readFileSync(path.join(__dirname, 'certs', 'idp.crt'), 'utf8');

// SAML Strategy configuration
const samlStrategy = new Strategy({
  // Service Provider settings
  callbackUrl: 'https://myapp.com/saml/acs',
  entryPoint: 'https://keycloak.mycompany.com/auth/realms/my-realm/protocol/saml',
  issuer: 'my-nodejs-app',
  signatureAlgorithm: 'sha256',
  privateKey: privateKey,
  
  // Identity Provider settings
  cert: idpCert,
  
  // Additional settings I've found important
  disableRequestedAuthnContext: true,
  acceptedClockSkewMs: 5000,
  
  // For better debugging during development
  validateInResponseTo: true,
  requestIdExpirationPeriodMs: 3600000 // 1 hour
}, (profile, done) => {
  // In production I do additional verification here
  // and often look up the user in a database
  return done(null, {
    id: profile.nameID,
    email: profile.email || profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
    name: profile.displayName,
    // Include any additional attributes from the SAML assertion
    attributes: profile
  });
});

passport.use(samlStrategy);

// Serialize/deserialize user to session
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));

app.use(passport.initialize());
app.use(passport.session());

// Middleware to check if user is authenticated
const ensureAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
};

// Route that initiates the SP-initiated SAML flow
app.get('/login', passport.authenticate('saml', {
  failureRedirect: '/login-failed',
  failureFlash: true
}));

// SAML Assertion Consumer Service endpoint
app.post('/saml/acs', 
  passport.authenticate('saml', { 
    failureRedirect: '/login-failed',
    failureFlash: true 
  }),
  (req, res) => {
    // After successful authentication
    res.redirect(req.session.returnTo || '/dashboard');
  }
);

// Protected route that requires authentication
app.get('/dashboard', ensureAuthenticated, (req, res) => {
  res.send(`
    <h1>Welcome, ${req.user.name}!</h1>
    <p>Your email: ${req.user.email}</p>
    <pre>${JSON.stringify(req.user, null, 2)}</pre>
    <a href="/logout">Logout</a>
  `);
});

// Metadata endpoint - I found this essential for easy Keycloak configuration
app.get('/metadata', (req, res) => {
  res.type('application/xml');
  res.send(samlStrategy.generateServiceProviderMetadata(
    fs.readFileSync(path.join(__dirname, 'certs', 'public.pem'), 'utf8')
  ));
});

app.get('/login-failed', (req, res) => {
  res.status(401).send('Authentication failed');
});

app.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/');
});

app.get('/', (req, res) => {
  res.send(`
    <h1>SAML SP Example</h1>
    <p>This is a public page.</p>
    <a href="/dashboard">Access Dashboard</a>
  `);
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

3. Adding Support for IdP-Initiated Flow

This is where things got interesting for me. To support IdP-initiated flows, I had to modify my SAML strategy configuration and ACS endpoint handler:

// Modified settings for the SAML strategy to support IdP-initiated flow
const samlStrategy = new Strategy({
  // Include all previous settings
  
  // This is the key setting for IdP-initiated flows
  validateInResponseTo: true,
  // For IdP-initiated flows, we need to disable strict validation
  // since there's no AuthnRequest to validate against
  allowUnsolicitedResponses: true
}, (profile, done) => {
  // Same callback as before
});

// Modified ACS endpoint to handle both SP and IdP initiated flows
app.post('/saml/acs', 
  passport.authenticate('saml', { 
    failureRedirect: '/login-failed',
    failureFlash: true 
  }),
  (req, res) => {
    // Check if this was an IdP-initiated flow (no returnTo in session)
    // or an SP-initiated flow (has returnTo)
    if (req.session.returnTo) {
      // SP-initiated flow - return to the original requested URL
      const returnTo = req.session.returnTo;
      delete req.session.returnTo;
      res.redirect(returnTo);
    } else {
      // IdP-initiated flow - go to the default landing page
      res.redirect('/dashboard');
    }
  }
);

I also had to ensure my session configuration properly handled IdP-initiated flows, which sometimes arrive without any prior session context.

The Key Differences I've Discovered Between the Flows

After implementing both flows across many projects, here are the key differences I've discovered:

1. Request Initiation and User Experience

SP-Initiated:

  • Starts when users attempt to access your application

  • Users see your application first, then get redirected to login

  • After authentication, users return to the specific resource they tried to access

  • Better for standalone applications with their own entry point

IdP-Initiated:

  • Starts from the identity provider's portal or dashboard

  • Users never see an unauthenticated version of your application

  • After authentication, users typically land on a default page in your application

  • Better for enterprise environments with a central application portal

2. Technical Implementation Differences

SP-Initiated:

  • Your application generates an AuthnRequest

  • SAML Response references the AuthnRequest ID

  • Prevents certain replay attacks through InResponseTo validation

  • Generally more secure due to the request-response pattern

IdP-Initiated:

  • No AuthnRequest is generated (unsolicited response)

  • SAML Response has no InResponseTo attribute

  • Requires special configuration to accept "unsolicited" responses

  • May need additional security measures like strong RelayState validation

  • Often requires special handling in your application code

3. Security Considerations

This was a big learning for me: IdP-initiated flows have some additional security considerations:

SP-Initiated:

  • The AuthnRequest ID serves as an anti-replay mechanism

  • The flow is harder to tamper with, as both sides have matching requests and responses

IdP-Initiated:

  • No AuthnRequest means one less verification point

  • Potentially more vulnerable to replay attacks

  • May require additional security measures like shorter assertion validity periods

  • Need to be careful with RelayState handling to prevent open redirects

My Decision Framework for Choosing Between Flows

After implementing both flows many times, here's my personal decision framework:

Use SP-Initiated When:

  • Your users primarily access your application directly

  • You need maximum security

  • You need to direct users to specific pages after authentication

  • You're building public-facing applications

Use IdP-Initiated When:

  • Your application is part of an enterprise suite

  • Users primarily access your app through a company portal

  • User experience priority is seamless access from the IdP

  • You need to support "deep linking" from an IdP dashboard

Support Both When:

  • You have enterprise customers who expect IdP-initiated flows

  • You also have users who bookmark your application directly

  • You want to provide maximum flexibility

Conclusion: The Flow Matters More Than You Think

When I first started implementing SAML, I didn't appreciate how much the choice of flow would impact both the user experience and security model. Now I understand that choosing the right flow isn't just a technical decision—it's a fundamental aspect of how users will interact with your application.

In most of my recent projects, I've chosen to support both flows with a preference for SP-initiated when possible. This gives my applications the security benefits of the request-response pattern while still accommodating enterprise customers who require IdP-initiated access from their company portals.

Whether you're just starting with SAML or looking to optimize your existing implementation, I hope my experiences help you navigate the sometimes confusing world of SAML authentication flows!

Last updated