Fix jwt-token

This commit is contained in:
2025-09-12 12:11:14 +02:00
parent 8b0234986d
commit d8bba047bb
14 changed files with 3236 additions and 1 deletions

View File

@@ -0,0 +1,325 @@
/**
* LDAP Authentication Provider
* Direct integration with Active Directory LDAP
*/
const ldap = require('ldapjs');
const crypto = require('crypto');
class LDAPAuth {
constructor() {
this.connections = new Map();
}
/**
* Create LDAP client connection
*/
createLDAPClient(config) {
const clientOptions = {
url: config.url, // ldap://dc.company.com:389 or ldaps://dc.company.com:636
timeout: config.timeout || 5000,
connectTimeout: config.connect_timeout || 10000,
tlsOptions: {
rejectUnauthorized: config.tls_reject_unauthorized !== false
}
};
if (config.ca_cert) {
clientOptions.tlsOptions.ca = [config.ca_cert];
}
return ldap.createClient(clientOptions);
}
/**
* Authenticate user against LDAP
*/
async authenticateUser(tenantId, username, password, config) {
return new Promise((resolve, reject) => {
const client = this.createLDAPClient(config);
// Build user DN
const userDN = this.buildUserDN(username, config);
client.bind(userDN, password, (err) => {
if (err) {
client.unbind();
return reject(new Error('Invalid credentials'));
}
// Search for user details
this.searchUser(client, username, config)
.then(userInfo => {
client.unbind();
resolve(userInfo);
})
.catch(searchErr => {
client.unbind();
reject(searchErr);
});
});
});
}
/**
* Build user Distinguished Name (DN)
*/
buildUserDN(username, config) {
// Method 1: UPN format (user@domain.com)
if (config.user_dn_format === 'upn') {
return `${username}@${config.domain}`;
}
// Method 2: CN format (CN=username,OU=Users,DC=domain,DC=com)
if (config.user_dn_template) {
return config.user_dn_template.replace('{username}', username);
}
// Method 3: sAMAccountName format (DOMAIN\\username)
if (config.domain) {
return `${config.domain}\\${username}`;
}
// Default: assume username is already a DN
return username;
}
/**
* Search for user information in LDAP
*/
async searchUser(client, username, config) {
return new Promise((resolve, reject) => {
const baseDN = config.base_dn || 'dc=example,dc=com';
const searchFilter = config.user_search_filter || `(sAMAccountName=${username})`;
const searchOptions = {
filter: searchFilter.replace('{username}', username),
scope: 'sub',
attributes: [
'sAMAccountName',
'userPrincipalName',
'mail',
'givenName',
'sn',
'displayName',
'telephoneNumber',
'memberOf',
'objectClass',
'distinguishedName'
]
};
client.search(baseDN, searchOptions, (err, searchRes) => {
if (err) {
return reject(err);
}
let userEntry = null;
searchRes.on('searchEntry', (entry) => {
userEntry = entry.object;
});
searchRes.on('error', (err) => {
reject(err);
});
searchRes.on('end', (result) => {
if (!userEntry) {
return reject(new Error('User not found in directory'));
}
// Extract user information
const userInfo = {
id: userEntry.distinguishedName,
username: userEntry.sAMAccountName || userEntry.userPrincipalName,
email: userEntry.mail,
firstName: userEntry.givenName,
lastName: userEntry.sn,
displayName: userEntry.displayName,
phoneNumber: userEntry.telephoneNumber,
groups: this.extractGroups(userEntry.memberOf),
distinguishedName: userEntry.distinguishedName,
raw: userEntry
};
resolve(userInfo);
});
});
});
}
/**
* Extract group names from memberOf attribute
*/
extractGroups(memberOf) {
if (!memberOf) {
return [];
}
const groups = Array.isArray(memberOf) ? memberOf : [memberOf];
return groups.map(dn => {
// Extract CN from DN (e.g., "CN=Domain Admins,CN=Users,DC=domain,DC=com" -> "Domain Admins")
const cnMatch = dn.match(/^CN=([^,]+)/i);
return cnMatch ? cnMatch[1] : dn;
});
}
/**
* Test LDAP connection
*/
async testConnection(config) {
return new Promise((resolve, reject) => {
const client = this.createLDAPClient(config);
const testDN = config.bind_dn || config.admin_dn;
const testPassword = config.bind_password || config.admin_password;
if (!testDN || !testPassword) {
client.unbind();
return reject(new Error('Admin credentials required for connection test'));
}
client.bind(testDN, testPassword, (err) => {
client.unbind();
if (err) {
reject(new Error(`LDAP connection failed: ${err.message}`));
} else {
resolve(true);
}
});
});
}
/**
* Handle LDAP authentication request
*/
async authenticate(req, res, next) {
// LDAP authentication is typically used for login forms
// This middleware would be used in login POST endpoint
if (req.method !== 'POST' || !req.body.username || !req.body.password) {
return res.status(400).json({
success: false,
message: 'Username and password required for LDAP authentication'
});
}
try {
const tenantId = req.tenant.id;
const config = req.tenant.authConfig.config;
const { username, password } = req.body;
// Authenticate against LDAP
const userInfo = await this.authenticateUser(tenantId, username, password, config);
const { MultiTenantAuth } = require('./multi-tenant-auth');
const multiAuth = new MultiTenantAuth();
// Create or update user
const user = await multiAuth.createOrUpdateExternalUser(
tenantId,
userInfo,
req.tenant.authConfig
);
// Generate JWT token
const token = multiAuth.generateJWTToken(user, tenantId);
res.json({
success: true,
data: {
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
first_name: user.first_name,
last_name: user.last_name
},
token,
expires_in: '24h'
},
message: 'LDAP authentication successful'
});
} catch (error) {
console.error('LDAP authentication error:', error);
res.status(401).json({
success: false,
message: 'LDAP authentication failed',
error: process.env.NODE_ENV === 'development' ? error.message : 'Invalid credentials'
});
}
}
/**
* Sync users from LDAP directory
*/
async syncUsers(tenantId, config) {
return new Promise((resolve, reject) => {
const client = this.createLDAPClient(config);
// Bind with admin credentials
const adminDN = config.bind_dn || config.admin_dn;
const adminPassword = config.bind_password || config.admin_password;
client.bind(adminDN, adminPassword, (err) => {
if (err) {
client.unbind();
return reject(new Error('Failed to bind with admin credentials'));
}
const baseDN = config.base_dn;
const userFilter = config.user_sync_filter || '(objectClass=user)';
const searchOptions = {
filter: userFilter,
scope: 'sub',
attributes: [
'sAMAccountName', 'userPrincipalName', 'mail', 'givenName',
'sn', 'displayName', 'telephoneNumber', 'memberOf', 'distinguishedName'
]
};
const users = [];
client.search(baseDN, searchOptions, (err, searchRes) => {
if (err) {
client.unbind();
return reject(err);
}
searchRes.on('searchEntry', (entry) => {
const userEntry = entry.object;
users.push({
id: userEntry.distinguishedName,
username: userEntry.sAMAccountName,
email: userEntry.mail,
firstName: userEntry.givenName,
lastName: userEntry.sn,
displayName: userEntry.displayName,
phoneNumber: userEntry.telephoneNumber,
groups: this.extractGroups(userEntry.memberOf)
});
});
searchRes.on('error', (err) => {
client.unbind();
reject(err);
});
searchRes.on('end', () => {
client.unbind();
resolve(users);
});
});
});
});
}
}
module.exports = LDAPAuth;

View File

@@ -0,0 +1,284 @@
/**
* Multi-Tenant Authentication Middleware
* Routes authentication requests to appropriate providers
*/
const jwt = require('jsonwebtoken');
const { User, Tenant } = require('../models');
const { AuthConfig, AuthProviders } = require('../config/auth-providers');
const SAMLAuth = require('./saml-auth');
const OAuthAuth = require('./oauth-auth');
const LDAPAuth = require('./ldap-auth');
class MultiTenantAuth {
constructor() {
this.authConfig = new AuthConfig();
this.providers = new Map();
// Initialize authentication providers
this.initializeProviders();
}
/**
* Initialize all authentication providers
*/
initializeProviders() {
this.providers.set(AuthProviders.SAML, new SAMLAuth());
this.providers.set(AuthProviders.OAUTH, new OAuthAuth());
this.providers.set(AuthProviders.LDAP, new LDAPAuth());
}
/**
* Determine tenant from request
* Can be from subdomain, header, or JWT
*/
async determineTenant(req) {
// Method 1: Subdomain (tenant.yourapp.com)
const subdomain = req.hostname.split('.')[0];
if (subdomain && subdomain !== 'www' && subdomain !== 'api') {
return subdomain;
}
// Method 2: Custom header
const tenantHeader = req.headers['x-tenant-id'];
if (tenantHeader) {
return tenantHeader;
}
// Method 3: From JWT token (for existing sessions)
const token = req.headers.authorization?.split(' ')[1];
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return decoded.tenantId;
} catch (error) {
// Token invalid, continue with other methods
}
}
// Method 4: Query parameter (for redirects)
if (req.query.tenant) {
return req.query.tenant;
}
// Default to 'default' tenant for backward compatibility
return 'default';
}
/**
* Get authentication configuration for tenant
*/
async getTenantAuthConfig(tenantId) {
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
if (!tenant) {
// Return default local auth for unknown tenants
return {
type: AuthProviders.LOCAL,
enabled: true,
config: {},
userMapping: this.authConfig.getDefaultUserMapping(),
roleMapping: this.authConfig.getDefaultRoleMapping()
};
}
return this.authConfig.getProvider(tenantId);
}
/**
* Authenticate user based on tenant configuration
*/
async authenticate(req, res, next) {
try {
const tenantId = await this.determineTenant(req);
const authConfig = await this.getTenantAuthConfig(tenantId);
// Attach tenant info to request
req.tenant = { id: tenantId, authConfig };
// Route to appropriate authentication provider
switch (authConfig.type) {
case AuthProviders.LOCAL:
return this.authenticateLocal(req, res, next);
case AuthProviders.SAML:
return this.authenticateSAML(req, res, next);
case AuthProviders.OAUTH:
return this.authenticateOAuth(req, res, next);
case AuthProviders.LDAP:
return this.authenticateLDAP(req, res, next);
default:
return this.authenticateLocal(req, res, next);
}
} catch (error) {
console.error('Multi-tenant auth error:', error);
return res.status(500).json({
success: false,
message: 'Authentication service error'
});
}
}
/**
* Local JWT authentication (existing system)
*/
async authenticateLocal(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
success: false,
message: 'Access token required'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.userId, {
attributes: ['id', 'username', 'email', 'role', 'is_active', 'tenant_id']
});
if (!user || !user.is_active) {
return res.status(401).json({
success: false,
message: 'Invalid or inactive user'
});
}
// Verify user belongs to tenant
if (user.tenant_id !== req.tenant.id) {
return res.status(403).json({
success: false,
message: 'User not authorized for this tenant'
});
}
req.user = user;
next();
} catch (error) {
console.error('JWT verification error:', error);
return res.status(403).json({
success: false,
message: 'Invalid or expired token'
});
}
}
/**
* SAML authentication for Active Directory
*/
async authenticateSAML(req, res, next) {
const provider = this.providers.get(AuthProviders.SAML);
return provider.authenticate(req, res, next);
}
/**
* OAuth 2.0/OpenID Connect authentication
*/
async authenticateOAuth(req, res, next) {
const provider = this.providers.get(AuthProviders.OAUTH);
return provider.authenticate(req, res, next);
}
/**
* LDAP authentication
*/
async authenticateLDAP(req, res, next) {
const provider = this.providers.get(AuthProviders.LDAP);
return provider.authenticate(req, res, next);
}
/**
* Create or update user from external provider
*/
async createOrUpdateExternalUser(tenantId, externalUser, authConfig) {
const userMapping = authConfig.userMapping;
const roleMapping = authConfig.roleMapping;
// Map external user attributes to internal user fields
const userData = {
username: this.getAttributeValue(externalUser, userMapping.username),
email: this.getAttributeValue(externalUser, userMapping.email),
first_name: this.getAttributeValue(externalUser, userMapping.firstName),
last_name: this.getAttributeValue(externalUser, userMapping.lastName),
phone_number: this.getAttributeValue(externalUser, userMapping.phoneNumber),
tenant_id: tenantId,
is_active: true,
external_provider: authConfig.type,
external_id: externalUser.id || externalUser.sub || externalUser.nameID,
last_login: new Date()
};
// Map roles from external provider
const externalRoles = externalUser.roles || externalUser.groups || [];
userData.role = this.mapExternalRole(externalRoles, roleMapping);
// Find or create user
const [user, created] = await User.findOrCreate({
where: {
external_id: userData.external_id,
tenant_id: tenantId
},
defaults: userData
});
// Update existing user
if (!created) {
await user.update(userData);
}
return user;
}
/**
* Get attribute value from external user object
*/
getAttributeValue(externalUser, attributeNames) {
if (!Array.isArray(attributeNames)) {
attributeNames = [attributeNames];
}
for (const attrName of attributeNames) {
if (externalUser[attrName]) {
return externalUser[attrName];
}
}
return null;
}
/**
* Map external roles to internal roles
*/
mapExternalRole(externalRoles, roleMapping) {
for (const externalRole of externalRoles) {
if (roleMapping[externalRole]) {
return roleMapping[externalRole];
}
}
return roleMapping.default || 'viewer';
}
/**
* Generate JWT token for authenticated user
*/
generateJWTToken(user, tenantId) {
return jwt.sign(
{
userId: user.id,
username: user.username,
role: user.role,
tenantId: tenantId,
provider: user.external_provider || 'local'
},
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
}
}
module.exports = MultiTenantAuth;

View File

@@ -0,0 +1,245 @@
/**
* OAuth 2.0/OpenID Connect Authentication Provider
* Supports modern identity providers like Azure AD, Google, etc.
*/
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2');
const OpenIDConnectStrategy = require('passport-openidconnect');
class OAuthAuth {
constructor() {
this.strategies = new Map();
}
/**
* Configure OAuth strategy for a tenant
*/
configureOAuthStrategy(tenantId, config) {
const strategyName = `oauth-${tenantId}`;
// Determine if this is OpenID Connect or plain OAuth2
const isOpenIDConnect = config.discovery_url || config.issuer;
if (isOpenIDConnect) {
return this.configureOpenIDConnectStrategy(tenantId, config);
} else {
return this.configureOAuth2Strategy(tenantId, config);
}
}
/**
* Configure OpenID Connect strategy (recommended for modern providers)
*/
configureOpenIDConnectStrategy(tenantId, config) {
const strategyName = `oidc-${tenantId}`;
const oidcOptions = {
issuer: config.issuer || config.discovery_url,
clientID: config.client_id,
clientSecret: config.client_secret,
callbackURL: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`,
// Scopes
scope: config.scopes || ['openid', 'profile', 'email'],
// Claims
skipUserProfile: false,
// Additional parameters
customHeaders: config.custom_headers || {},
passReqToCallback: true
};
const strategy = new OpenIDConnectStrategy(oidcOptions,
async (req, issuer, profile, accessToken, refreshToken, done) => {
try {
const externalUser = {
id: profile.id || profile.sub,
username: profile.preferred_username || profile.username || profile.email,
email: profile.email,
firstName: profile.given_name || profile.firstName,
lastName: profile.family_name || profile.lastName,
displayName: profile.name || profile.displayName,
roles: profile.roles || profile['custom:roles'] || [],
groups: profile.groups || profile['custom:groups'] || [],
raw: profile._json // Store raw profile for custom mapping
};
return done(null, { tenantId, externalUser, provider: 'oauth' });
} catch (error) {
return done(error, null);
}
});
passport.use(strategyName, strategy);
this.strategies.set(tenantId, { strategy, config, type: 'oidc' });
return strategyName;
}
/**
* Configure OAuth 2.0 strategy (for legacy providers)
*/
configureOAuth2Strategy(tenantId, config) {
const strategyName = `oauth2-${tenantId}`;
const oauth2Options = {
authorizationURL: config.authorization_url,
tokenURL: config.token_url,
clientID: config.client_id,
clientSecret: config.client_secret,
callbackURL: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`,
// Custom parameters
customHeaders: config.custom_headers || {},
scope: config.scopes || ['profile', 'email'],
passReqToCallback: true
};
const strategy = new OAuth2Strategy(oauth2Options,
async (req, accessToken, refreshToken, profile, done) => {
try {
// Fetch user profile from userinfo endpoint
const userProfile = await this.fetchUserProfile(accessToken, config.userinfo_url);
const externalUser = {
id: userProfile.sub || userProfile.id,
username: userProfile.preferred_username || userProfile.username || userProfile.email,
email: userProfile.email,
firstName: userProfile.given_name || userProfile.first_name,
lastName: userProfile.family_name || userProfile.last_name,
displayName: userProfile.name || userProfile.display_name,
roles: userProfile.roles || [],
groups: userProfile.groups || [],
raw: userProfile
};
return done(null, { tenantId, externalUser, provider: 'oauth' });
} catch (error) {
return done(error, null);
}
});
passport.use(strategyName, strategy);
this.strategies.set(tenantId, { strategy, config, type: 'oauth2' });
return strategyName;
}
/**
* Fetch user profile from OAuth userinfo endpoint
*/
async fetchUserProfile(accessToken, userinfoUrl) {
const axios = require('axios');
try {
const response = await axios.get(userinfoUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('Error fetching user profile:', error);
throw new Error('Failed to fetch user profile');
}
}
/**
* Initiate OAuth authentication
*/
async authenticate(req, res, next) {
const tenantId = req.tenant.id;
// Check if strategy is configured for this tenant
if (!this.strategies.has(tenantId)) {
const config = req.tenant.authConfig.config;
this.configureOAuthStrategy(tenantId, config);
}
const strategyInfo = this.strategies.get(tenantId);
const strategyName = strategyInfo.type === 'oidc' ? `oidc-${tenantId}` : `oauth2-${tenantId}`;
// Store return URL in session
req.session.returnUrl = req.query.returnUrl || '/';
// Use passport to authenticate
passport.authenticate(strategyName, {
state: req.query.returnUrl || '/'
})(req, res, next);
}
/**
* Handle OAuth callback
*/
async handleCallback(req, res, next) {
const tenantId = req.params.tenantId;
const strategyInfo = this.strategies.get(tenantId);
if (!strategyInfo) {
return res.redirect(`/login?error=strategy_not_found&tenant=${tenantId}`);
}
const strategyName = strategyInfo.type === 'oidc' ? `oidc-${tenantId}` : `oauth2-${tenantId}`;
passport.authenticate(strategyName, async (err, authResult) => {
if (err) {
console.error('OAuth 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.session.returnUrl || req.query.state || '/';
res.redirect(`${returnUrl}?token=${token}&tenant=${tenantId}`);
} catch (error) {
console.error('OAuth user creation error:', error);
return res.redirect(`/login?error=user_creation_failed&tenant=${tenantId}`);
}
})(req, res, next);
}
/**
* Get authorization URL for manual OAuth flow
*/
getAuthorizationUrl(tenantId, returnUrl) {
const strategyInfo = this.strategies.get(tenantId);
if (!strategyInfo) {
throw new Error(`OAuth strategy not configured for tenant: ${tenantId}`);
}
const config = strategyInfo.config;
const params = new URLSearchParams({
client_id: config.client_id,
response_type: 'code',
redirect_uri: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`,
scope: (config.scopes || ['openid', 'profile', 'email']).join(' '),
state: returnUrl || '/'
});
return `${config.authorization_url}?${params.toString()}`;
}
}
module.exports = OAuthAuth;

View 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;