Fix jwt-token
This commit is contained in:
197
server/middleware/saml-auth.js
Normal file
197
server/middleware/saml-auth.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user