/** * Multi-Tenant Authentication Routes * Handles authentication for different providers and tenants */ const express = require('express'); const router = express.Router(); const passport = require('passport'); const session = require('express-session'); const { Tenant } = require('../models'); const MultiTenantAuth = require('../middleware/multi-tenant-auth'); const SAMLAuth = require('../middleware/saml-auth'); const OAuthAuth = require('../middleware/oauth-auth'); const LDAPAuth = require('../middleware/ldap-auth'); // Initialize multi-tenant auth const multiAuth = new MultiTenantAuth(); // Session middleware for OAuth state management router.use(session({ secret: process.env.SESSION_SECRET || 'your-session-secret', resave: false, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000 } // 10 minutes })); // Initialize passport router.use(passport.initialize()); router.use(passport.session()); passport.serializeUser((user, done) => done(null, user)); passport.deserializeUser((obj, done) => done(null, obj)); /** * GET /auth/config/:tenantId * Get authentication configuration for a tenant */ router.get('/config/:tenantId', async (req, res) => { try { const { tenantId } = req.params; const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Return MINIMAL public auth configuration (no internal settings exposed) const publicConfig = { provider: tenant.auth_provider, enabled: tenant.is_active, features: { local_login: tenant.auth_provider === 'local', sso_login: ['saml', 'oauth', 'ldap'].includes(tenant.auth_provider), // Only show registration as enabled if ALL server-side checks would pass registration: ( tenant.auth_provider === 'local' && tenant.is_active && tenant.allow_registration ) } }; // Add provider-specific public config (URLs only - no secrets) if (tenant.auth_provider === 'saml') { publicConfig.saml = { login_url: `/auth/saml/${tenantId}/login`, metadata_url: `/auth/saml/${tenantId}/metadata` }; } else if (tenant.auth_provider === 'oauth') { publicConfig.oauth = { login_url: `/auth/oauth/${tenantId}/login` }; } // Add security notice for developers publicConfig._security_notice = "This config is for UI display only. All security validations occur server-side."; res.json({ success: true, data: publicConfig }); } catch (error) { console.error('Error fetching auth config:', error); res.status(500).json({ success: false, message: 'Failed to fetch authentication configuration' }); } }); /** * GET /auth/config * Get authentication configuration for current tenant (auto-detected) */ router.get('/config', async (req, res) => { try { // Auto-determine tenant from request const tenantId = await multiAuth.determineTenant(req); if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant from request' }); } const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Return public auth configuration (no secrets) const publicConfig = { tenant_id: tenant.slug, tenant_name: tenant.name, auth_provider: tenant.auth_provider, branding: tenant.branding }; // Add provider-specific configuration if (tenant.auth_provider === 'local') { const authConfig = tenant.auth_config || {}; publicConfig.local = { allow_registration: authConfig.allow_registration || false, require_email_verification: authConfig.require_email_verification || false }; } else if (tenant.auth_provider === 'saml') { publicConfig.saml = { login_url: `/auth/saml/${tenantId}/login`, metadata_url: `/auth/saml/${tenantId}/metadata` }; } else if (tenant.auth_provider === 'oauth') { publicConfig.oauth = { login_url: `/auth/oauth/${tenantId}/login` }; } else if (tenant.auth_provider === 'ldap') { publicConfig.ldap = { // LDAP uses same form as local but with different backend }; } res.json({ success: true, data: publicConfig }); } catch (error) { console.error('Error fetching auth config:', error); res.status(500).json({ success: false, message: 'Failed to fetch authentication configuration' }); } }); /** * POST /auth/login * Universal login endpoint that routes to appropriate provider */ router.post('/login', async (req, res, next) => { try { // Determine tenant const tenantId = await multiAuth.determineTenant(req); const authConfig = await multiAuth.getTenantAuthConfig(tenantId); req.tenant = { id: tenantId, authConfig }; // Route based on authentication provider switch (authConfig.type) { case 'local': return require('../routes/user').loginLocal(req, res, next); case 'ldap': const ldapAuth = new LDAPAuth(); return ldapAuth.authenticate(req, res, next); case 'saml': case 'oauth': return res.status(400).json({ success: false, message: `Please use SSO login for ${authConfig.type} authentication`, redirect_url: `/auth/${authConfig.type}/${tenantId}/login` }); default: return res.status(400).json({ success: false, message: 'Authentication provider not configured' }); } } catch (error) { console.error('Login error:', error); res.status(500).json({ success: false, message: 'Login failed' }); } }); /** * POST /auth/register * Universal registration endpoint that routes to appropriate provider */ router.post('/register', async (req, res, next) => { try { // Determine tenant const tenantId = await multiAuth.determineTenant(req); const authConfig = await multiAuth.getTenantAuthConfig(tenantId); req.tenant = { id: tenantId, authConfig }; // Only local authentication supports registration if (authConfig.type !== 'local') { return res.status(400).json({ success: false, message: `Registration not supported for ${authConfig.type} authentication` }); } // Route to local registration handler return require('../routes/user').registerLocal(req, res, next); } catch (error) { console.error('Registration error:', error); res.status(500).json({ success: false, message: 'Registration failed' }); } }); /** * POST /auth/local * Local authentication endpoint with tenant isolation */ router.post('/local', async (req, res, next) => { try { // Determine tenant const tenantId = await multiAuth.determineTenant(req); const authConfig = await multiAuth.getTenantAuthConfig(tenantId); // Verify tenant supports local authentication if (authConfig.type !== 'local') { return res.status(400).json({ success: false, message: `This tenant uses ${authConfig.type} authentication. Please use the appropriate login method.`, auth_provider: authConfig.type }); } // Perform local authentication with tenant context const bcrypt = require('bcryptjs'); const { User, Tenant } = require('../models'); const { Op } = require('sequelize'); const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ success: false, message: 'Username and password are required' }); } // Find tenant const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Find user by username or email within this tenant const user = await User.findOne({ where: { [Op.or]: [ { username: username }, { email: username } ], is_active: true, tenant_id: tenant.id } }); if (!user || !await bcrypt.compare(password, user.password_hash)) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Update last login await user.update({ last_login: new Date() }); // Generate JWT token with tenant information const token = multiAuth.generateJWTToken(user, tenantId); // Remove password hash from response const { password_hash: _, ...userResponse } = user.toJSON(); res.json({ success: true, data: { user: userResponse, token, expires_in: '24h', tenant: { id: tenant.slug, name: tenant.name } }, message: 'Login successful' }); } catch (error) { console.error('Local login error:', error); res.status(500).json({ success: false, message: 'Local login failed' }); } }); /** * SAML Authentication Routes */ // GET /auth/saml/:tenantId/login - Initiate SAML login router.get('/saml/:tenantId/login', async (req, res, next) => { try { const tenantId = req.params.tenantId; req.tenant = { id: tenantId, authConfig: await multiAuth.getTenantAuthConfig(tenantId) }; const samlAuth = new SAMLAuth(); return samlAuth.authenticate(req, res, next); } catch (error) { console.error('SAML login error:', error); res.redirect(`/login?error=saml_error&tenant=${req.params.tenantId}`); } }); // POST /auth/saml/:tenantId/callback - SAML callback router.post('/saml/:tenantId/callback', async (req, res, next) => { const samlAuth = new SAMLAuth(); return samlAuth.handleCallback(req, res, next); }); // GET /auth/saml/:tenantId/metadata - SAML metadata router.get('/saml/:tenantId/metadata', async (req, res) => { try { const { tenantId } = req.params; const authConfig = await multiAuth.getTenantAuthConfig(tenantId); if (authConfig.type !== 'saml') { return res.status(404).json({ message: 'SAML not configured for this tenant' }); } const samlAuth = new SAMLAuth(); const metadata = samlAuth.generateMetadata(tenantId, authConfig.config); res.set('Content-Type', 'application/xml'); res.send(metadata); } catch (error) { console.error('SAML metadata error:', error); res.status(500).json({ message: 'Failed to generate SAML metadata' }); } }); /** * OAuth Authentication Routes */ // GET /auth/oauth/:tenantId/login - Initiate OAuth login router.get('/oauth/:tenantId/login', async (req, res, next) => { try { const tenantId = req.params.tenantId; req.tenant = { id: tenantId, authConfig: await multiAuth.getTenantAuthConfig(tenantId) }; const oauthAuth = new OAuthAuth(); return oauthAuth.authenticate(req, res, next); } catch (error) { console.error('OAuth login error:', error); res.redirect(`/login?error=oauth_error&tenant=${req.params.tenantId}`); } }); // GET /auth/oauth/:tenantId/callback - OAuth callback router.get('/oauth/:tenantId/callback', async (req, res, next) => { const oauthAuth = new OAuthAuth(); return oauthAuth.handleCallback(req, res, next); }); /** * Logout endpoint for all providers */ router.post('/logout', async (req, res) => { try { const tenantId = await multiAuth.determineTenant(req); const authConfig = await multiAuth.getTenantAuthConfig(tenantId); // Clear local session req.logout((err) => { if (err) console.error('Logout error:', err); }); // Provider-specific logout if (authConfig.type === 'saml' && authConfig.config.logout_url) { return res.json({ success: true, logout_url: authConfig.config.logout_url, message: 'Please complete logout with your identity provider' }); } res.json({ success: true, message: 'Logged out successfully' }); } catch (error) { console.error('Logout error:', error); res.status(500).json({ success: false, message: 'Logout failed' }); } }); /** * Test authentication configuration */ router.post('/test/:tenantId', async (req, res) => { try { const { tenantId } = req.params; const authConfig = await multiAuth.getTenantAuthConfig(tenantId); let testResult = { success: false, message: 'Unknown provider' }; switch (authConfig.type) { case 'ldap': const ldapAuth = new LDAPAuth(); try { await ldapAuth.testConnection(authConfig.config); testResult = { success: true, message: 'LDAP connection successful' }; } catch (error) { testResult = { success: false, message: error.message }; } break; case 'local': testResult = { success: true, message: 'Local authentication ready' }; break; case 'saml': // Test SAML configuration validity const requiredSamlFields = ['sso_url', 'certificate', 'issuer']; const missingSamlFields = requiredSamlFields.filter(field => !authConfig.config[field]); if (missingSamlFields.length > 0) { testResult = { success: false, message: `Missing SAML configuration: ${missingSamlFields.join(', ')}` }; } else { testResult = { success: true, message: 'SAML configuration valid' }; } break; case 'oauth': // Test OAuth configuration validity const requiredOAuthFields = ['client_id', 'client_secret', 'authorization_url', 'token_url']; const missingOAuthFields = requiredOAuthFields.filter(field => !authConfig.config[field]); if (missingOAuthFields.length > 0) { testResult = { success: false, message: `Missing OAuth configuration: ${missingOAuthFields.join(', ')}` }; } else { testResult = { success: true, message: 'OAuth configuration valid' }; } break; } res.json(testResult); } catch (error) { console.error('Auth test error:', error); res.status(500).json({ success: false, message: 'Authentication test failed' }); } }); /** * POST /auth/refresh * Refresh JWT token */ router.post('/refresh', async (req, res) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ success: false, message: 'Authorization token required' }); } const token = authHeader.substring(7); const jwt = require('jsonwebtoken'); // Verify current token (even if expired, we want to check if it's valid) let decoded; try { decoded = jwt.verify(token, process.env.JWT_SECRET, { ignoreExpiration: true }); } catch (error) { return res.status(401).json({ success: false, message: 'Invalid token' }); } // Find user to ensure they still exist and are active const { User } = require('../models'); const user = await User.findOne({ where: { id: decoded.userId, is_active: true } }); if (!user) { return res.status(401).json({ success: false, message: 'User not found or inactive' }); } // Generate new token const newToken = jwt.sign( { userId: user.id, username: user.username, role: user.role, tenantId: decoded.tenantId, provider: user.auth_provider }, process.env.JWT_SECRET, { expiresIn: '24h' } ); res.json({ success: true, data: { token: newToken, expires_in: '24h' }, message: 'Token refreshed successfully' }); } catch (error) { console.error('Token refresh error:', error); res.status(500).json({ success: false, message: 'Token refresh failed' }); } }); /** * POST /auth/logout * Logout (mainly for client-side token removal) */ router.post('/logout', (req, res) => { res.json({ success: true, message: 'Logged out successfully' }); }); /** * GET /auth/me * Get current user information */ router.get('/me', async (req, res) => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ success: false, message: 'Authorization token required' }); } const token = authHeader.substring(7); const jwt = require('jsonwebtoken'); let decoded; try { decoded = jwt.verify(token, process.env.JWT_SECRET); } catch (error) { return res.status(401).json({ success: false, message: 'Invalid or expired token' }); } // Find user const { User } = require('../models'); const user = await User.findOne({ where: { id: decoded.userId, is_active: true }, attributes: { exclude: ['password_hash'] } }); if (!user) { return res.status(401).json({ success: false, message: 'User not found or inactive' }); } res.json({ success: true, data: { user }, message: 'User information retrieved successfully' }); } catch (error) { console.error('Get user info error:', error); res.status(500).json({ success: false, message: 'Failed to retrieve user information' }); } }); module.exports = router;