SAML SSO Identity Provider in Node.js: The Complete Guide (Part 1)
Building Enterprise Authentication in 5 Minutes
By Syed Mudaser Ali Kazmi (SMAK)
When enterprise clients ask for SAML SSO integration, most developers' hearts skip a beat. The complexity of XML parsing, certificate management, and SAML protocol intricacies can turn a simple authentication request into weeks of development hell.
But what if I told you that you could build a production-ready SAML Identity Provider (IdP) in Node.js in just 5 minutes?
What is a SAML Identity Provider?
A SAML Identity Provider is the authentication server that:
- π Authenticates users (validates credentials)
- π« Issues SAML assertions (digital tokens)
- π Signs and encrypts authentication responses
- π Provides metadata for Service Providers to trust
Think of it as the "bouncer" of the digital world - it verifies who you are and gives you a VIP pass (SAML assertion) to access other applications.
Why Build Your Own IdP?
Enterprise Scenarios:
- Custom Authentication Logic: Integrate with proprietary user stores
- Multi-Factor Authentication: Add custom MFA flows
- Compliance Requirements: Meet specific security standards
- Cost Optimization: Avoid expensive third-party IdP licensing
- Full Control: Complete customization of user experience
Modern Use Cases:
- B2B SaaS: Provide SSO for enterprise customers
- Internal Tools: Centralize authentication for multiple apps
- Partner Integration: Enable federated access for partners
- Microservices: Create authentication service for service mesh
The Traditional SAML Pain Points
Before we solve them, let's acknowledge what makes SAML development challenging:
// Traditional SAML implementation nightmare
const xml2js = require('xml2js');
const crypto = require('crypto');
const zlib = require('zlib');
// 200+ lines of XML template generation
const generateSAMLResponse = (user, requestId) => {
// Complex XML parsing and generation
// Certificate loading and validation
// Signature generation
// Encryption handling
// Base64 encoding/decoding
// Deflate compression
// ... weeks of debugging
};
Problems with this approach:
- β Hundreds of lines of boilerplate code
- β Easy to introduce security vulnerabilities
- β Difficult to test and debug
- β Poor maintainability
- β XML template hell
Enter saml-sso-helper: The Game Changer
I built saml-sso-helper
to eliminate all this complexity:
// Modern SAML implementation
const SAMLHelper = require('saml-sso-helper');
const samlHelper = new SAMLHelper({
encryption: true,
entityID: 'https://your-company.com/idp',
baseURL: 'https://your-company.com',
certificates: { /* your certs */ }
});
samlHelper.createIdentityProvider();
// Done! π
5-Minute IdP Implementation
Let's build a complete SAML Identity Provider from scratch:
Step 1: Project Setup (30 seconds)
mkdir my-saml-idp && cd my-saml-idp
npm init -y
npm install saml-sso-helper express express-session bcryptjs
Step 2: Generate Certificates (1 minute)
mkdir certificates && cd certificates
# Generate signing certificate
openssl req -x509 -newkey rsa:2048 -keyout idp-signing.key \
-out idp-signing.cert -days 365 -nodes \
-subj "/CN=your-company.com/O=Your Company/C=US"
# Generate encryption certificate
openssl req -x509 -newkey rsa:2048 -keyout idp-encrypt.key \
-out idp-encrypt.cert -days 365 -nodes \
-subj "/CN=your-company.com/O=Your Company/C=US"
cd ..
Step 3: Create the Identity Provider (3.5 minutes)
Create idp.js
:
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcryptjs');
const path = require('path');
const SAMLHelper = require('saml-sso-helper');
const app = express();
// Middleware setup
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(session({
secret: 'your-session-secret-change-in-production',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));
// Mock user database (replace with your actual user store)
const users = [
{
id: 1,
email: 'john.doe@company.com',
password: bcrypt.hashSync('password123', 10),
profile: {
displayName: 'John Doe',
firstName: 'John',
lastName: 'Doe',
department: 'Engineering',
role: 'Senior Developer',
employeeId: 'EMP001'
}
},
{
id: 2,
email: 'jane.smith@company.com',
password: bcrypt.hashSync('secure456', 10),
profile: {
displayName: 'Jane Smith',
firstName: 'Jane',
lastName: 'Smith',
department: 'Product',
role: 'Product Manager',
employeeId: 'EMP002'
}
}
];
// Initialize SAML Helper
const samlHelper = new SAMLHelper({
encryption: true,
entityID: 'https://your-company.com/idp/metadata',
baseURL: 'https://your-company.com',
partnerMetadataURL: 'https://partner-app.com/sp/metadata',
certificates: {
signing: {
key: path.join(__dirname, 'certificates/idp-signing.key'),
cert: path.join(__dirname, 'certificates/idp-signing.cert')
},
encryption: {
key: path.join(__dirname, 'certificates/idp-encrypt.key'),
cert: path.join(__dirname, 'certificates/idp-encrypt.cert')
}
},
attributes: [
'email',
'displayName',
'firstName',
'lastName',
'department',
'role',
'employeeId'
]
});
// Create IdP instance
samlHelper.createIdentityProvider();
const middleware = samlHelper.getExpressMiddleware();
// Helper functions
const findUserByEmail = (email) => users.find(u => u.email === email);
const authenticateUser = async (email, password) => {
const user = findUserByEmail(email);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
};
// Routes
// IdP Metadata endpoint
app.get('/metadata', middleware.idp.metadata);
// Login page
app.get('/login', (req, res) => {
const { SAMLRequest, RelayState } = req.query;
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Company Login - SAML IdP</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 100px auto; padding: 20px; }
.login-form { background: #f5f5f5; padding: 30px; border-radius: 8px; }
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; }
button { background: #007bff; color: white; padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; width: 100%; }
button:hover { background: #0056b3; }
.error { color: red; margin: 10px 0; }
</style>
</head>
<body>
<div class="login-form">
<h2>π Company Portal Login</h2>
<form method="POST" action="/login">
<input type="hidden" name="SAMLRequest" value="${SAMLRequest || ''}" />
<input type="hidden" name="RelayState" value="${RelayState || ''}" />
<input type="email" name="email" placeholder="Email Address" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
<div style="margin-top: 20px; font-size: 12px; color: #666;">
<strong>Demo Accounts:</strong><br>
john.doe@company.com / password123<br>
jane.smith@company.com / secure456
</div>
</div>
</body>
</html>
`);
});
// Login processing
app.post('/login', async (req, res) => {
const { email, password, SAMLRequest, RelayState } = req.body;
try {
// Authenticate user
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).send(`
<h2>Login Failed</h2>
<p>Invalid email or password.</p>
<a href="/login?SAMLRequest=${SAMLRequest}&RelayState=${RelayState}">Try Again</a>
`);
}
// Store user session
req.session.user = user;
// If this is a SAML request, continue to SSO
if (SAMLRequest) {
req.query.SAMLRequest = SAMLRequest;
req.query.RelayState = RelayState;
return app._router.handle({...req, method: 'GET', url: '/sso'}, res);
}
// Regular login success
res.send(`
<h2>β
Login Successful</h2>
<p>Welcome, ${user.profile.displayName}!</p>
<a href="/profile">View Profile</a>
`);
} catch (error) {
console.error('Login error:', error);
res.status(500).send('Login failed');
}
});
// SSO endpoint
app.get('/sso', (req, res) => {
// Check if user is authenticated
if (!req.session.user) {
const { SAMLRequest, RelayState } = req.query;
return res.redirect(`/login?SAMLRequest=${SAMLRequest}&RelayState=${RelayState}`);
}
// Prepare user data for SAML assertion
const user = req.session.user;
req.userData = {
email: user.email,
displayName: user.profile.displayName,
firstName: user.profile.firstName,
lastName: user.profile.lastName,
department: user.profile.department,
role: user.profile.role,
employeeId: user.profile.employeeId
};
// Process SAML SSO
middleware.idp.sso(req, res);
});
// User profile page
app.get('/profile', (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
const user = req.session.user;
res.send(`
<h2>π€ User Profile</h2>
<p><strong>Name:</strong> ${user.profile.displayName}</p>
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>Department:</strong> ${user.profile.department}</p>
<p><strong>Role:</strong> ${user.profile.role}</p>
<p><strong>Employee ID:</strong> ${user.profile.employeeId}</p>
<a href="/logout">Logout</a>
`);
});
// Logout
app.get('/logout', (req, res) => {
req.session.destroy();
res.send('<h2>Logged out successfully</h2><a href="/login">Login Again</a>');
});
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
service: 'SAML Identity Provider',
encryption: samlHelper.config.encryption,
timestamp: new Date().toISOString()
});
});
// Error handling
app.use((error, req, res, next) => {
console.error('IdP Error:', error);
res.status(500).json({
error: 'Identity Provider error',
message: error.message
});
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`π SAML Identity Provider running on http://localhost:${PORT}`);
console.log(`π Metadata: http://localhost:${PORT}/metadata`);
console.log(`π Login: http://localhost:${PORT}/login`);
console.log(`π€ Profile: http://localhost:${PORT}/profile`);
});
module.exports = app;
Advanced IdP Features
Database Integration
Replace the mock users with real database integration:
// With MongoDB/Mongoose
const User = require('./models/User');
const authenticateUser = async (email, password) => {
const user = await User.findOne({ email });
if (!user) return null;
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
};
// With PostgreSQL/Sequelize
const { User } = require('./models');
const authenticateUser = async (email, password) => {
const user = await User.findOne({ where: { email } });
if (!user) return null;
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
};
Multi-Factor Authentication
const speakeasy = require('speakeasy');
app.post('/verify-mfa', async (req, res) => {
const { token } = req.body;
const user = req.session.user;
const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: token,
window: 2
});
if (verified) {
req.session.mfaVerified = true;
res.redirect('/sso');
} else {
res.status(401).send('Invalid MFA token');
}
});
LDAP/Active Directory Integration
const ldap = require('ldapjs');
const authenticateWithLDAP = (username, password) => {
return new Promise((resolve, reject) => {
const client = ldap.createClient({
url: 'ldap://your-domain-controller.com'
});
const dn = `cn=${username},ou=users,dc=company,dc=com`;
client.bind(dn, password, (err) => {
if (err) {
reject(err);
} else {
// Fetch user attributes
client.search(dn, (err, res) => {
// Process LDAP response
resolve(userInfo);
});
}
});
});
};
Custom Attribute Mapping
const mapUserAttributes = (user, targetApp) => {
const attributeMappings = {
'salesforce': {
email: user.email,
'https://schemas.salesforce.com/FirstName': user.firstName,
'https://schemas.salesforce.com/LastName': user.lastName
},
'office365': {
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': user.email,
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname': user.firstName,
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname': user.lastName
},
'default': {
email: user.email,
displayName: user.displayName,
firstName: user.firstName,
lastName: user.lastName
}
};
return attributeMappings[targetApp] || attributeMappings.default;
};
Production Deployment
Environment Configuration
Create .env
:
NODE_ENV=production
PORT=3000
SESSION_SECRET=your-super-secret-key-change-this
DATABASE_URL=postgresql://user:pass@host:5432/dbname
LDAP_URL=ldap://domain-controller.company.com
ENTITY_ID=https://idp.company.com/metadata
BASE_URL=https://idp.company.com
SIGNING_CERT_PATH=/app/certs/signing.cert
SIGNING_KEY_PATH=/app/certs/signing.key
ENCRYPT_CERT_PATH=/app/certs/encrypt.cert
ENCRYPT_KEY_PATH=/app/certs/encrypt.key
Docker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "idp.js"]
Kubernetes Configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: saml-idp
spec:
replicas: 3
selector:
matchLabels:
app: saml-idp
template:
metadata:
labels:
app: saml-idp
spec:
containers:
- name: saml-idp
image: your-registry/saml-idp:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
volumeMounts:
- name: certificates
mountPath: /app/certificates
volumes:
- name: certificates
secret:
secretName: saml-certificates
Security Best Practices
Certificate Management
// Rotate certificates regularly
const loadCertificates = () => {
const certPath = process.env.NODE_ENV === 'production'
? '/secure/certificates'
: './certificates';
return {
signing: {
key: fs.readFileSync(`${certPath}/signing.key`),
cert: fs.readFileSync(`${certPath}/signing.cert`)
},
encryption: {
key: fs.readFileSync(`${certPath}/encrypt.key`),
cert: fs.readFileSync(`${certPath}/encrypt.cert`)
}
};
};
// Validate certificate expiration
const validateCertificates = (certificates) => {
const cert = new crypto.X509Certificate(certificates.signing.cert);
const expiryDate = new Date(cert.validTo);
const daysUntilExpiry = (expiryDate - new Date()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry < 30) {
console.warn(`Certificate expires in ${daysUntilExpiry} days!`);
}
};
Rate Limiting
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false
});
app.post('/login', loginLimiter, async (req, res) => {
// Login logic
});
Audit Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'audit.log' })
]
});
const auditLog = (event, user, details) => {
logger.info({
timestamp: new Date().toISOString(),
event,
userId: user?.id,
email: user?.email,
ip: req.ip,
userAgent: req.get('User-Agent'),
details
});
};
// Usage
auditLog('LOGIN_SUCCESS', user, { method: 'password' });
auditLog('SSO_REQUEST', user, { sp: targetApp });
Testing Your IdP
Unit Tests
const request = require('supertest');
const app = require('./idp');
describe('SAML IdP', () => {
test('should serve metadata', async () => {
const response = await request(app)
.get('/metadata')
.expect(200);
expect(response.text).toContain('EntityDescriptor');
expect(response.text).toContain('IDPSSODescriptor');
});
test('should authenticate valid user', async () => {
const response = await request(app)
.post('/login')
.send({
email: 'john.doe@company.com',
password: 'password123'
})
.expect(200);
expect(response.text).toContain('Login Successful');
});
test('should reject invalid credentials', async () => {
await request(app)
.post('/login')
.send({
email: 'john.doe@company.com',
password: 'wrongpassword'
})
.expect(401);
});
});
Integration Testing
// Test with a real Service Provider
const testSAMLFlow = async () => {
// 1. Initiate SSO from SP
const spResponse = await request(spApp)
.get('/login')
.expect(302);
// 2. Extract SAML request
const redirectUrl = new URL(spResponse.headers.location);
const samlRequest = redirectUrl.searchParams.get('SAMLRequest');
// 3. Submit to IdP
const idpResponse = await request(idpApp)
.get('/sso')
.query({ SAMLRequest: samlRequest })
.expect(200);
// 4. Verify SAML response
expect(idpResponse.text).toContain('SAMLResponse');
};
What's Next?
Congratulations! You now have a fully functional SAML Identity Provider. In Part 2 of this series, we'll cover:
- π’ Service Provider Implementation: How to consume SAML assertions
- π Complete SSO Flow: End-to-end integration testing
- π Advanced SP Features: Session management, logout, and more
- π§ Production Deployment: Scaling and monitoring strategies
Resources
- π¦ npm Package:
npm install saml-sso-helper
- π GitHub: saml-sso-helper
- π Documentation: Complete API reference
- π¬ Part 2: SAML Service Provider Guide
Ready to implement the Service Provider side? Check out Part 2 of this series where we build the receiving end of our SAML SSO flow!
About the Author: Syed Mudaser Ali Kazmi (SMAK) is a seasoned technology leader specializing in solving complex enterprise challenges with elegant, efficient solutions. As Co-founder and Tech Lead at Hucu.ai, he helps organizations achieve maximum operational efficiency using minimal team resources and optimized budgets.
Frustrated by the complexity of enterprise authentication systems, SMAK created the saml-sso-helper
package to transform weeks of complex SAML integration work into simple, maintainable systems. His passion lies in democratizing enterprise-grade technology, making sophisticated solutions accessible to teams of all sizes through innovative tooling and clear documentation.
Top comments (0)