Service Provider vs Identity Provider Initiated SAML Flows
Last updated
Last updated
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.
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.
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.
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.
Let me share how I've implemented both flows using Keycloak as the IdP and Node.js as my Service Provider.
First, I configured Keycloak to support both flows:
I created a dedicated realm for my application
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
For IdP-initiated flow support, I configured:
IDP Initiated SSO URL Name: my-app
IDP Initiated SSO Relay State: /dashboard
I created a few test users and assigned them roles
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');
});
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.
After implementing both flows across many projects, here are the key differences I've discovered:
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
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
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
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
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!