198 lines
6.3 KiB
JavaScript
198 lines
6.3 KiB
JavaScript
/**
|
|
* 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 = `<?xml version="1.0"?>
|
|
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
|
entityID="urn:${process.env.DOMAIN_NAME}:${tenantId}">
|
|
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true"
|
|
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
|
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
|
Location="${process.env.BASE_URL}/auth/saml/${tenantId}/callback"
|
|
index="1" />
|
|
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
Location="${process.env.BASE_URL}/auth/saml/${tenantId}/logout" />
|
|
</md:SPSSODescriptor>
|
|
</md:EntityDescriptor>`;
|
|
|
|
return metadata;
|
|
}
|
|
}
|
|
|
|
module.exports = SAMLAuth;
|