Implement enterprise SSO with SAML 2.0, Active Directory, LDAP, and identity providers. Learn SP-initiated flow, attribute mapping, and session management.
Enterprise SSO with SAML 2.0
SAML 2.0 Architecture
User Browser <-> Service Provider (SP) <-> Identity Provider (IdP)
(Your App) (Okta, Azure AD, G Suite)
SP-Initiated Flow:
1. User accesses SP (your app)
2. SP generates AuthnRequest, redirects to IdP
3. User authenticates at IdP
4. IdP sends SAML Response (assertion) to SP via browser
5. SP validates assertion, creates session
Node.js SAML with passport-saml
import passport from 'passport'
import { Strategy as SamlStrategy } from '@node-saml/passport-saml'
import express from 'express'
const samlConfig = {
callbackUrl: 'https://app.example.com/auth/saml/callback',
entryPoint: 'https://company.okta.com/app/example/sso/saml',
issuer: 'https://app.example.com',
// IdP certificate (from IdP metadata)
cert: process.env.IDP_CERT,
// Optionally sign requests
privateKey: process.env.SP_PRIVATE_KEY,
signatureAlgorithm: 'sha256',
// Attribute mapping
wantAssertionsSigned: true,
wantAuthnResponseSigned: true,
}
passport.use(new SamlStrategy(samlConfig, async (profile, done) => {
try {
// Extract user from SAML attributes
const user = {
id: profile.nameID,
email: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
name: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'],
groups: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/groups'] || [],
}
// Upsert user in database
const dbUser = await upsertUser(user)
return done(null, dbUser)
} catch (err) {
return done(err)
}
}))
const app = express()
// SP-initiated login
app.get('/auth/saml', passport.authenticate('saml', { failureRedirect: '/login' }))
// ACS (Assertion Consumer Service)
app.post('/auth/saml/callback',
passport.authenticate('saml', { failureRedirect: '/login' }),
(req, res) => {
req.session.user = req.user
res.redirect('/dashboard')
}
)
// SP metadata endpoint (needed to configure IdP)
app.get('/auth/saml/metadata', (req, res) => {
const strategy = passport._strategy('saml')
const metadata = strategy.generateServiceProviderMetadata(null, process.env.SP_CERT)
res.set('Content-Type', 'text/xml')
res.send(metadata)
})
LDAP Authentication
import ldap3
from ldap3 import Server, Connection, NTLM, SASL, KERBEROS
class LDAPAuthenticator:
def __init__(self, ldap_server: str, base_dn: str):
self.server = Server(ldap_server, get_info='ALL', use_ssl=True)
self.base_dn = base_dn
def authenticate(self, username: str, password: str) -> dict:
user_dn = f"uid={username},{self.base_dn}"
try:
conn = Connection(
self.server,
user=user_dn,
password=password,
authentication='SIMPLE',
auto_bind=True,
)
# Get user attributes
conn.search(
user_dn,
'(objectclass=*)',
attributes=['cn', 'mail', 'memberOf', 'department']
)
if conn.entries:
entry = conn.entries[0]
return {
'id': username,
'name': str(entry.cn),
'email': str(entry.mail),
'groups': [g.split(',')[0].replace('CN=', '') for g in entry.memberOf],
'department': str(entry.department),
}
return None
except ldap3.core.exceptions.LDAPBindError:
return None # Invalid credentials
finally:
try:
conn.unbind()
except Exception:
pass
def get_group_members(self, group_dn: str) -> list:
with Connection(self.server, auto_bind=True) as conn:
conn.search(
group_dn,
'(objectclass=group)',
attributes=['member']
)
return [m for m in conn.entries[0].member]
JIT (Just-In-Time) User Provisioning
async function upsertUser(samlProfile) {
const { email, name, groups } = samlProfile
const existingUser = await db.users.findOne({ email })
if (existingUser) {
// Update user from IdP attributes
return db.users.update(existingUser.id, {
name,
saml_groups: groups,
last_sso_login: new Date(),
})
}
// Just-In-Time provisioning
const newUser = await db.users.create({
email,
name,
saml_groups: groups,
source: 'saml',
created_at: new Date(),
})
// Auto-assign roles based on SAML groups
const roles = await mapGroupsToRoles(groups)
await db.userRoles.bulkCreate(roles.map(role => ({
user_id: newUser.id,
role,
})))
return newUser
}
function mapGroupsToRoles(groups: string[]): string[] {
const groupRoleMap = {
'Engineering': ['developer'],
'DevOps': ['developer', 'deployer'],
'Management': ['manager'],
'IT-Admins': ['admin'],
}
return [...new Set(
groups.flatMap(g => groupRoleMap[g] || [])
)]
}
Security Considerations
| Risk |
Mitigation |
| XML injection |
Use vetted SAML libraries |
| Signature wrapping |
Validate assertion placement |
| Open redirect |
Validate RelayState/redirect URLs |
| Session fixation |
Create new session after SSO |
| Token replay |
Check NotOnOrAfter, use assertion cache |