Fix jwt-token

This commit is contained in:
2025-09-13 14:23:01 +02:00
parent 02b1b6f62f
commit 181bd9cfa4
4 changed files with 798 additions and 8 deletions

View File

@@ -457,16 +457,464 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
); );
}; };
const AuthenticationSettings = () => ( const AuthenticationSettings = ({ tenantConfig }) => {
const [authConfig, setAuthConfig] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(false);
useEffect(() => {
fetchAuthConfig();
}, []);
const fetchAuthConfig = async () => {
try {
const response = await api.get('/tenant/auth');
setAuthConfig(response.data.data);
} catch (error) {
console.error('Failed to fetch auth config:', error);
toast.error('Failed to load authentication settings');
} finally {
setLoading(false);
}
};
const saveAuthConfig = async (newConfig) => {
setSaving(true);
try {
const response = await api.put('/tenant/auth', newConfig);
setAuthConfig(response.data.data);
setEditing(false);
toast.success('Authentication settings updated successfully');
} catch (error) {
console.error('Failed to save auth config:', error);
toast.error(error.response?.data?.message || 'Failed to save authentication settings');
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="bg-white shadow rounded-lg"> <div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Authentication Settings</h3> <div className="animate-pulse">
<p className="mt-2 text-sm text-gray-500"> <div className="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
Authentication provider configuration will be available here. <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
</p> <div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
</div> </div>
</div> </div>
); );
}
const authProvider = tenantConfig?.auth_provider || 'local';
return (
<div className="space-y-6">
{/* Current Authentication Provider */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900">Authentication Provider</h3>
<p className="mt-1 text-sm text-gray-500">
Current authentication method for this tenant
</p>
</div>
<div className="flex items-center space-x-3">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
authProvider === 'local'
? 'bg-blue-100 text-blue-800'
: authProvider === 'saml'
? 'bg-purple-100 text-purple-800'
: authProvider === 'oauth'
? 'bg-green-100 text-green-800'
: authProvider === 'ldap'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`}>
{authProvider.toUpperCase()}
</span>
<button
onClick={() => setEditing(!editing)}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{editing ? 'Cancel' : 'Configure'}
</button>
</div>
</div>
{/* Provider Description */}
<div className="mt-4 p-4 bg-gray-50 rounded-md">
<p className="text-sm text-gray-600">
{authProvider === 'local' && 'Users are managed directly in this system with username/password authentication.'}
{authProvider === 'saml' && 'Users authenticate through SAML Single Sign-On (SSO) provider.'}
{authProvider === 'oauth' && 'Users authenticate through OAuth provider (Google, Microsoft, etc.).'}
{authProvider === 'ldap' && 'Users authenticate through LDAP/Active Directory.'}
{authProvider === 'ad' && 'Users authenticate through Active Directory.'}
</p>
</div>
</div>
</div>
{/* Authentication Configuration */}
{editing && (
<AuthProviderConfig
provider={authProvider}
config={authConfig}
onSave={saveAuthConfig}
onCancel={() => setEditing(false)}
saving={saving}
/>
)}
{/* User Role Mappings */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Role Mappings</h3>
<p className="mt-1 text-sm text-gray-500">
Configure how external users are assigned roles in your system
</p>
<div className="mt-6 space-y-4">
{authProvider === 'local' ? (
<div className="text-sm text-gray-500">
Role assignments for local users are managed in the Users tab.
</div>
) : (
<RoleMappingConfig provider={authProvider} config={authConfig} />
)}
</div>
</div>
</div>
{/* Session Settings */}
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">Session Settings</h3>
<p className="mt-1 text-sm text-gray-500">
Configure session timeout and security settings
</p>
<div className="mt-6">
<SessionConfig config={authConfig} onSave={saveAuthConfig} />
</div>
</div>
</div>
</div>
);
};
// Auth Provider Configuration Component
const AuthProviderConfig = ({ provider, config, onSave, onCancel, saving }) => {
const [formData, setFormData] = useState(config || {});
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
return (
<div className="bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
{provider.toUpperCase()} Configuration
</h3>
<form onSubmit={handleSubmit} className="mt-6 space-y-6">
{provider === 'saml' && (
<SAMLConfig formData={formData} setFormData={setFormData} />
)}
{provider === 'oauth' && (
<OAuthConfig formData={formData} setFormData={setFormData} />
)}
{provider === 'ldap' && (
<LDAPConfig formData={formData} setFormData={setFormData} />
)}
{provider === 'ad' && (
<ADConfig formData={formData} setFormData={setFormData} />
)}
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Cancel
</button>
<button
type="submit"
disabled={saving}
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? 'Saving...' : 'Save Configuration'}
</button>
</div>
</form>
</div>
</div>
);
};
// SAML Configuration
const SAMLConfig = ({ formData, setFormData }) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">SSO URL</label>
<input
type="url"
value={formData.sso_url || ''}
onChange={(e) => setFormData({...formData, sso_url: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="https://your-idp.com/sso"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Entity ID</label>
<input
type="text"
value={formData.entity_id || ''}
onChange={(e) => setFormData({...formData, entity_id: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="your-app-entity-id"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">X.509 Certificate</label>
<textarea
value={formData.certificate || ''}
onChange={(e) => setFormData({...formData, certificate: e.target.value})}
rows={4}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="-----BEGIN CERTIFICATE-----"
/>
</div>
</div>
);
// OAuth Configuration
const OAuthConfig = ({ formData, setFormData }) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Provider</label>
<select
value={formData.oauth_provider || 'google'}
onChange={(e) => setFormData({...formData, oauth_provider: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
>
<option value="google">Google</option>
<option value="microsoft">Microsoft</option>
<option value="github">GitHub</option>
<option value="custom">Custom OAuth Provider</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Client ID</label>
<input
type="text"
value={formData.client_id || ''}
onChange={(e) => setFormData({...formData, client_id: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Client Secret</label>
<input
type="password"
value={formData.client_secret || ''}
onChange={(e) => setFormData({...formData, client_secret: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
/>
</div>
</div>
);
// LDAP Configuration
const LDAPConfig = ({ formData, setFormData }) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">LDAP Server</label>
<input
type="text"
value={formData.ldap_server || ''}
onChange={(e) => setFormData({...formData, ldap_server: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="ldap://your-server.com:389"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Base DN</label>
<input
type="text"
value={formData.base_dn || ''}
onChange={(e) => setFormData({...formData, base_dn: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="dc=company,dc=com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Bind DN</label>
<input
type="text"
value={formData.bind_dn || ''}
onChange={(e) => setFormData({...formData, bind_dn: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="cn=admin,dc=company,dc=com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Bind Password</label>
<input
type="password"
value={formData.bind_password || ''}
onChange={(e) => setFormData({...formData, bind_password: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
/>
</div>
</div>
);
// Active Directory Configuration
const ADConfig = ({ formData, setFormData }) => (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Domain Controller</label>
<input
type="text"
value={formData.domain_controller || ''}
onChange={(e) => setFormData({...formData, domain_controller: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="dc.company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Domain</label>
<input
type="text"
value={formData.domain || ''}
onChange={(e) => setFormData({...formData, domain: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="COMPANY"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Service Account</label>
<input
type="text"
value={formData.service_account || ''}
onChange={(e) => setFormData({...formData, service_account: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="svc-account@company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Service Password</label>
<input
type="password"
value={formData.service_password || ''}
onChange={(e) => setFormData({...formData, service_password: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
/>
</div>
</div>
);
// Role Mapping Configuration
const RoleMappingConfig = ({ provider, config }) => (
<div className="space-y-4">
<div className="text-sm text-gray-600">
Configure how {provider.toUpperCase()} groups/attributes map to system roles:
</div>
<div className="space-y-3">
{['admin', 'user_admin', 'security_admin', 'branding_admin', 'operator', 'viewer'].map(role => (
<div key={role} className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div>
<span className="font-medium text-gray-900">{role}</span>
<span className="ml-2 text-sm text-gray-500">
{role === 'admin' && '(Full system access)'}
{role === 'user_admin' && '(User management only)'}
{role === 'security_admin' && '(Security settings only)'}
{role === 'branding_admin' && '(Branding settings only)'}
{role === 'operator' && '(Basic operations)'}
{role === 'viewer' && '(Read-only access)'}
</span>
</div>
<input
type="text"
placeholder={provider === 'saml' ? 'SAML attribute value' : 'Group name'}
className="ml-4 block w-48 border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
/>
</div>
))}
</div>
</div>
);
// Session Configuration
const SessionConfig = ({ config, onSave }) => {
const [sessionSettings, setSessionSettings] = useState({
session_timeout: config?.session_timeout || 480, // 8 hours default
require_mfa: config?.require_mfa || false,
allow_concurrent_sessions: config?.allow_concurrent_sessions || true
});
const handleSave = () => {
onSave({ ...config, ...sessionSettings });
};
return (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Session Timeout (minutes)</label>
<input
type="number"
value={sessionSettings.session_timeout}
onChange={(e) => setSessionSettings({...sessionSettings, session_timeout: parseInt(e.target.value)})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
min="15"
max="1440"
/>
<p className="mt-1 text-xs text-gray-500">Users will be logged out after this period of inactivity</p>
</div>
<div className="flex items-center">
<input
id="require-mfa"
type="checkbox"
checked={sessionSettings.require_mfa}
onChange={(e) => setSessionSettings({...sessionSettings, require_mfa: e.target.checked})}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label htmlFor="require-mfa" className="ml-2 block text-sm text-gray-900">
Require Multi-Factor Authentication
</label>
</div>
<div className="flex items-center">
<input
id="concurrent-sessions"
type="checkbox"
checked={sessionSettings.allow_concurrent_sessions}
onChange={(e) => setSessionSettings({...sessionSettings, allow_concurrent_sessions: e.target.checked})}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label htmlFor="concurrent-sessions" className="ml-2 block text-sm text-gray-900">
Allow Concurrent Sessions
</label>
</div>
<button
onClick={handleSave}
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Save Session Settings
</button>
</div>
);
};
const UsersSettings = ({ tenantConfig, onRefresh }) => { const UsersSettings = ({ tenantConfig, onRefresh }) => {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);

View File

@@ -0,0 +1,54 @@
/**
* Migration: Add session and role mapping configuration to tenants
* Adds session_timeout, require_mfa, allow_concurrent_sessions, and role_mappings fields
*/
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
// Add session configuration fields
await queryInterface.addColumn('tenants', 'session_timeout', {
type: Sequelize.INTEGER,
defaultValue: 480, // 8 hours in minutes
allowNull: false,
comment: 'Session timeout in minutes'
});
await queryInterface.addColumn('tenants', 'require_mfa', {
type: Sequelize.BOOLEAN,
defaultValue: false,
allowNull: false,
comment: 'Whether multi-factor authentication is required'
});
await queryInterface.addColumn('tenants', 'allow_concurrent_sessions', {
type: Sequelize.BOOLEAN,
defaultValue: true,
allowNull: false,
comment: 'Whether users can have multiple concurrent sessions'
});
await queryInterface.addColumn('tenants', 'role_mappings', {
type: Sequelize.JSONB,
allowNull: true,
comment: 'Mapping of external groups/attributes to system roles'
});
// Update auth_provider enum to include 'ad'
await queryInterface.sequelize.query(`
ALTER TYPE "enum_tenants_auth_provider" ADD VALUE 'ad';
`);
},
down: async (queryInterface, Sequelize) => {
// Remove the added columns
await queryInterface.removeColumn('tenants', 'session_timeout');
await queryInterface.removeColumn('tenants', 'require_mfa');
await queryInterface.removeColumn('tenants', 'allow_concurrent_sessions');
await queryInterface.removeColumn('tenants', 'role_mappings');
// Note: Removing enum values is complex in PostgreSQL and typically not done in production
// The 'ad' value will remain in the enum even after this rollback
}
};

View File

@@ -44,7 +44,7 @@ module.exports = (sequelize) => {
// Authentication Configuration // Authentication Configuration
auth_provider: { auth_provider: {
type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'), type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'ad', 'custom_sso'),
defaultValue: 'local', defaultValue: 'local',
comment: 'Primary authentication provider' comment: 'Primary authentication provider'
}, },
@@ -137,6 +137,32 @@ module.exports = (sequelize) => {
comment: 'Additional tenant metadata' comment: 'Additional tenant metadata'
}, },
// Session Configuration
session_timeout: {
type: DataTypes.INTEGER,
defaultValue: 480, // 8 hours in minutes
validate: {
min: 15, // Minimum 15 minutes
max: 1440 // Maximum 24 hours
},
comment: 'Session timeout in minutes'
},
require_mfa: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether multi-factor authentication is required'
},
allow_concurrent_sessions: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether users can have multiple concurrent sessions'
},
role_mappings: {
type: DataTypes.JSONB,
allowNull: true,
comment: 'Mapping of external groups/attributes to system roles'
},
created_at: { created_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
defaultValue: DataTypes.NOW defaultValue: DataTypes.NOW

View File

@@ -450,4 +450,266 @@ router.put('/users/:userId/status', authenticateToken, requirePermissions(['user
} }
}); });
/**
* GET /tenant/auth
* Get authentication configuration (auth admins or higher)
*/
router.get('/auth', authenticateToken, requirePermissions(['auth.view']), async (req, res) => {
try {
// Determine tenant from request
const tenantId = await multiAuth.determineTenant(req);
if (!tenantId) {
return res.status(400).json({
success: false,
message: 'Unable to determine tenant'
});
}
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
if (!tenant) {
return res.status(404).json({
success: false,
message: 'Tenant not found'
});
}
// Return auth configuration (excluding sensitive credentials)
const authConfig = {
auth_provider: tenant.auth_provider,
auth_config: tenant.auth_config ? {
...tenant.auth_config,
// Hide sensitive fields
client_secret: tenant.auth_config.client_secret ? '***HIDDEN***' : undefined,
bind_password: tenant.auth_config.bind_password ? '***HIDDEN***' : undefined,
service_password: tenant.auth_config.service_password ? '***HIDDEN***' : undefined,
certificate: tenant.auth_config.certificate ? '***HIDDEN***' : undefined
} : {},
session_timeout: tenant.session_timeout || 480,
require_mfa: tenant.require_mfa || false,
allow_concurrent_sessions: tenant.allow_concurrent_sessions !== false
};
console.log(`✅ Auth config retrieved for tenant "${tenantId}" by "${req.user.username}"`);
res.json({
success: true,
data: authConfig
});
} catch (error) {
console.error('Error retrieving auth config:', error);
res.status(500).json({
success: false,
message: 'Failed to retrieve authentication configuration'
});
}
});
/**
* PUT /tenant/auth
* Update authentication configuration (auth admins or higher)
*/
router.put('/auth', authenticateToken, requirePermissions(['auth.edit']), async (req, res) => {
try {
// Determine tenant from request
const tenantId = await multiAuth.determineTenant(req);
if (!tenantId) {
return res.status(400).json({
success: false,
message: 'Unable to determine tenant'
});
}
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
if (!tenant) {
return res.status(404).json({
success: false,
message: 'Tenant not found'
});
}
const {
auth_provider,
auth_config,
session_timeout,
require_mfa,
allow_concurrent_sessions,
role_mappings
} = req.body;
// Validate auth provider
const validProviders = ['local', 'saml', 'oauth', 'ldap', 'ad'];
if (auth_provider && !validProviders.includes(auth_provider)) {
return res.status(400).json({
success: false,
message: 'Invalid authentication provider'
});
}
// Validate session timeout
if (session_timeout && (session_timeout < 15 || session_timeout > 1440)) {
return res.status(400).json({
success: false,
message: 'Session timeout must be between 15 and 1440 minutes'
});
}
// Prepare update data
const updateData = {};
if (auth_provider) updateData.auth_provider = auth_provider;
if (auth_config) {
// Merge with existing config, preserving hidden sensitive fields
const existingConfig = tenant.auth_config || {};
updateData.auth_config = {
...existingConfig,
...auth_config,
// Restore hidden fields if they weren't changed
client_secret: auth_config.client_secret === '***HIDDEN***' ? existingConfig.client_secret : auth_config.client_secret,
bind_password: auth_config.bind_password === '***HIDDEN***' ? existingConfig.bind_password : auth_config.bind_password,
service_password: auth_config.service_password === '***HIDDEN***' ? existingConfig.service_password : auth_config.service_password,
certificate: auth_config.certificate === '***HIDDEN***' ? existingConfig.certificate : auth_config.certificate
};
}
if (session_timeout !== undefined) updateData.session_timeout = session_timeout;
if (require_mfa !== undefined) updateData.require_mfa = require_mfa;
if (allow_concurrent_sessions !== undefined) updateData.allow_concurrent_sessions = allow_concurrent_sessions;
if (role_mappings) updateData.role_mappings = role_mappings;
// Update tenant
await tenant.update(updateData);
console.log(`✅ Auth config updated for tenant "${tenantId}" by admin "${req.user.username}"`);
// Return updated config (with hidden sensitive fields)
const updatedConfig = {
auth_provider: tenant.auth_provider,
auth_config: tenant.auth_config ? {
...tenant.auth_config,
client_secret: tenant.auth_config.client_secret ? '***HIDDEN***' : undefined,
bind_password: tenant.auth_config.bind_password ? '***HIDDEN***' : undefined,
service_password: tenant.auth_config.service_password ? '***HIDDEN***' : undefined,
certificate: tenant.auth_config.certificate ? '***HIDDEN***' : undefined
} : {},
session_timeout: tenant.session_timeout,
require_mfa: tenant.require_mfa,
allow_concurrent_sessions: tenant.allow_concurrent_sessions,
role_mappings: tenant.role_mappings
};
res.json({
success: true,
message: 'Authentication configuration updated successfully',
data: updatedConfig
});
} catch (error) {
console.error('Error updating auth config:', error);
res.status(500).json({
success: false,
message: 'Failed to update authentication configuration'
});
}
});
/**
* POST /tenant/auth/test
* Test authentication configuration (auth admins or higher)
*/
router.post('/auth/test', authenticateToken, requirePermissions(['auth.edit']), async (req, res) => {
try {
// Determine tenant from request
const tenantId = await multiAuth.determineTenant(req);
if (!tenantId) {
return res.status(400).json({
success: false,
message: 'Unable to determine tenant'
});
}
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
if (!tenant) {
return res.status(404).json({
success: false,
message: 'Tenant not found'
});
}
const { test_username, test_password } = req.body;
// Simulate authentication test based on provider
const authProvider = tenant.auth_provider;
let testResult = {
success: false,
message: 'Authentication test not implemented for this provider',
details: {}
};
switch (authProvider) {
case 'local':
testResult = {
success: true,
message: 'Local authentication is always available',
details: { provider: 'local' }
};
break;
case 'saml':
// In real implementation, this would test SAML SSO endpoint
testResult = {
success: true,
message: 'SAML configuration appears valid (test connection would be performed in production)',
details: {
provider: 'saml',
sso_url: tenant.auth_config?.sso_url,
entity_id: tenant.auth_config?.entity_id
}
};
break;
case 'oauth':
// In real implementation, this would test OAuth endpoint
testResult = {
success: true,
message: 'OAuth configuration appears valid (test connection would be performed in production)',
details: {
provider: 'oauth',
oauth_provider: tenant.auth_config?.oauth_provider,
client_id: tenant.auth_config?.client_id
}
};
break;
case 'ldap':
case 'ad':
// In real implementation, this would test LDAP/AD connection
testResult = {
success: true,
message: `${authProvider.toUpperCase()} configuration appears valid (test connection would be performed in production)`,
details: {
provider: authProvider,
server: tenant.auth_config?.ldap_server || tenant.auth_config?.domain_controller
}
};
break;
}
console.log(`✅ Auth test performed for tenant "${tenantId}" by admin "${req.user.username}"`);
res.json({
success: true,
message: 'Authentication test completed',
data: testResult
});
} catch (error) {
console.error('Error testing auth config:', error);
res.status(500).json({
success: false,
message: 'Failed to test authentication configuration'
});
}
});
module.exports = router; module.exports = router;