-
Authentication Settings
-
- Authentication provider configuration will be available here.
-
+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 (
+
+ );
+ }
+
+ const authProvider = tenantConfig?.auth_provider || 'local';
+
+ return (
+
+ {/* Current Authentication Provider */}
+
+
+
+
+
Authentication Provider
+
+ Current authentication method for this tenant
+
+
+
+
+ {authProvider.toUpperCase()}
+
+
+
+
+
+ {/* Provider Description */}
+
+
+ {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.'}
+
+
+
+
+
+ {/* Authentication Configuration */}
+ {editing && (
+
setEditing(false)}
+ saving={saving}
+ />
+ )}
+
+ {/* User Role Mappings */}
+
+
+
Role Mappings
+
+ Configure how external users are assigned roles in your system
+
+
+
+ {authProvider === 'local' ? (
+
+ Role assignments for local users are managed in the Users tab.
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Session Settings */}
+
+
+
Session Settings
+
+ Configure session timeout and security settings
+
+
+
+
+
+
+
+
+ );
+};
+
+// Auth Provider Configuration Component
+const AuthProviderConfig = ({ provider, config, onSave, onCancel, saving }) => {
+ const [formData, setFormData] = useState(config || {});
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSave(formData);
+ };
+
+ return (
+
+
+
+ {provider.toUpperCase()} Configuration
+
+
+
+
+
+ );
+};
+
+// SAML Configuration
+const SAMLConfig = ({ formData, setFormData }) => (
+
+
+
+ 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"
+ />
+
+
+
+ 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"
+ />
+
+
+
+
);
+// OAuth Configuration
+const OAuthConfig = ({ formData, setFormData }) => (
+
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+ 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"
+ />
+
+
+);
+
+// LDAP Configuration
+const LDAPConfig = ({ formData, setFormData }) => (
+
+);
+
+// Active Directory Configuration
+const ADConfig = ({ formData, setFormData }) => (
+
+);
+
+// Role Mapping Configuration
+const RoleMappingConfig = ({ provider, config }) => (
+
+
+ Configure how {provider.toUpperCase()} groups/attributes map to system roles:
+
+
+
+ {['admin', 'user_admin', 'security_admin', 'branding_admin', 'operator', 'viewer'].map(role => (
+
+
+ {role}
+
+ {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)'}
+
+
+
+
+ ))}
+
+
+);
+
+// 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 (
+
+
+
+
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"
+ />
+
Users will be logged out after this period of inactivity
+
+
+
+ setSessionSettings({...sessionSettings, require_mfa: e.target.checked})}
+ className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
+ />
+
+
+
+
+ setSessionSettings({...sessionSettings, allow_concurrent_sessions: e.target.checked})}
+ className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
+ />
+
+
+
+
+
+ );
+};
+
const UsersSettings = ({ tenantConfig, onRefresh }) => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
diff --git a/server/migrations/20250913-add-auth-session-config.js b/server/migrations/20250913-add-auth-session-config.js
new file mode 100644
index 0000000..a1391e4
--- /dev/null
+++ b/server/migrations/20250913-add-auth-session-config.js
@@ -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
+ }
+};
diff --git a/server/models/Tenant.js b/server/models/Tenant.js
index f97dc2f..70e4f87 100644
--- a/server/models/Tenant.js
+++ b/server/models/Tenant.js
@@ -44,7 +44,7 @@ module.exports = (sequelize) => {
// Authentication Configuration
auth_provider: {
- type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'),
+ type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'ad', 'custom_sso'),
defaultValue: 'local',
comment: 'Primary authentication provider'
},
@@ -137,6 +137,32 @@ module.exports = (sequelize) => {
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: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
diff --git a/server/routes/tenant.js b/server/routes/tenant.js
index 6e9543e..90a1e68 100644
--- a/server/routes/tenant.js
+++ b/server/routes/tenant.js
@@ -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;