正在加载,请稍候…

Enterprise SSO: SAML 2.0 and Enterprise Identity Integration

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