Files
drone-detector/server/middleware/saml-auth.js
2025-09-12 12:11:14 +02:00

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;