/** * SAML Authentication Provider * Supports Active Directory integration via ADFS */ const passport = require('passport'); const SamlStrategy = require('passport-saml').Strategy; const fs = require('fs'); const path = require('path'); class SAMLAuth { constructor() { this.strategies = new Map(); } /** * Configure SAML strategy for a tenant */ configureSAMLStrategy(tenantId, config) { const strategyName = `saml-${tenantId}`; const samlOptions = { // SAML Service Provider (SP) Configuration callbackUrl: `${process.env.BASE_URL}/auth/saml/${tenantId}/callback`, entryPoint: config.sso_url, // ADFS SSO URL issuer: config.issuer || `urn:${process.env.DOMAIN_NAME}:${tenantId}`, // Identity Provider (IdP) Configuration cert: config.certificate, // ADFS signing certificate // Optional: Encryption privateCert: config.private_key, decryptionPvk: config.private_key, // Attribute mapping attributeConsumingServiceIndex: false, disableRequestedAuthnContext: true, // ADFS specific settings identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', acceptedClockSkewMs: 5000, // Logout logoutUrl: config.logout_url, logoutCallbackUrl: `${process.env.BASE_URL}/auth/saml/${tenantId}/logout` }; const strategy = new SamlStrategy(samlOptions, async (profile, done) => { try { // Extract user information from SAML assertion const externalUser = { id: profile.nameID, username: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] || profile.nameID, email: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] || profile.email, firstName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'] || profile.givenName, lastName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'] || profile.surname, displayName: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname'] || profile.displayName, groups: this.extractGroups(profile), roles: this.extractRoles(profile) }; return done(null, { tenantId, externalUser, provider: 'saml' }); } catch (error) { return done(error, null); } }); passport.use(strategyName, strategy); this.strategies.set(tenantId, { strategy, config }); return strategyName; } /** * Extract Active Directory groups from SAML assertion */ extractGroups(profile) { const groupClaims = [ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups', 'http://schemas.xmlsoap.org/claims/Group', 'groups' ]; for (const claim of groupClaims) { if (profile[claim]) { return Array.isArray(profile[claim]) ? profile[claim] : [profile[claim]]; } } return []; } /** * Extract roles from SAML assertion */ extractRoles(profile) { const roleClaims = [ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role', 'http://schemas.xmlsoap.org/claims/Role', 'roles' ]; for (const claim of roleClaims) { if (profile[claim]) { return Array.isArray(profile[claim]) ? profile[claim] : [profile[claim]]; } } return []; } /** * Initiate SAML authentication */ async authenticate(req, res, next) { const tenantId = req.tenant.id; const strategyName = `saml-${tenantId}`; // Check if strategy is configured for this tenant if (!this.strategies.has(tenantId)) { const config = req.tenant.authConfig.config; this.configureSAMLStrategy(tenantId, config); } // Use passport to authenticate passport.authenticate(strategyName, { additionalParams: { RelayState: req.query.returnUrl || '/' } })(req, res, next); } /** * Handle SAML callback */ async handleCallback(req, res, next) { const tenantId = req.params.tenantId; const strategyName = `saml-${tenantId}`; passport.authenticate(strategyName, async (err, authResult) => { if (err) { console.error('SAML authentication error:', err); return res.redirect(`/login?error=auth_failed&tenant=${tenantId}`); } if (!authResult) { return res.redirect(`/login?error=auth_cancelled&tenant=${tenantId}`); } try { const { MultiTenantAuth } = require('./multi-tenant-auth'); const multiAuth = new MultiTenantAuth(); // Create or update user const user = await multiAuth.createOrUpdateExternalUser( tenantId, authResult.externalUser, req.tenant.authConfig ); // Generate JWT token const token = multiAuth.generateJWTToken(user, tenantId); // Redirect to application with token const returnUrl = req.body.RelayState || '/'; res.redirect(`${returnUrl}?token=${token}&tenant=${tenantId}`); } catch (error) { console.error('SAML user creation error:', error); return res.redirect(`/login?error=user_creation_failed&tenant=${tenantId}`); } })(req, res, next); } /** * Generate SAML metadata for SP configuration */ generateMetadata(tenantId, config) { const metadata = ` urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress `; return metadata; } } module.exports = SAMLAuth;