From de220bb0403674041d061a5a3ef78e268c5afabb Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Sun, 14 Sep 2025 10:10:04 +0200 Subject: [PATCH] Fix jwt-token --- client/src/App.jsx | 2 + client/src/pages/Login.jsx | 8 +- client/src/pages/Register.jsx | 395 ++++++++++++++++++ .../20250914-add-allow-registration.js | 30 ++ server/models/Tenant.js | 5 + server/routes/auth.js | 2 +- server/routes/user.js | 164 +++++++- 7 files changed, 586 insertions(+), 20 deletions(-) create mode 100644 client/src/pages/Register.jsx create mode 100644 server/migrations/20250914-add-allow-registration.js diff --git a/client/src/App.jsx b/client/src/App.jsx index eaaa792..45c40b5 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -13,6 +13,7 @@ import Alerts from './pages/Alerts'; import Debug from './pages/Debug'; import Settings from './pages/Settings'; import Login from './pages/Login'; +import Register from './pages/Register'; import ProtectedRoute from './components/ProtectedRoute'; function App() { @@ -60,6 +61,7 @@ function App() { } /> + } /> diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index df49dcb..ff55df9 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Navigate } from 'react-router-dom'; +import { Navigate, Link } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; @@ -205,13 +205,13 @@ const Login = () => { )} {/* Registration link for local auth if enabled */} - {tenantConfig?.auth_provider === 'local' && tenantConfig?.local?.allow_registration && ( + {tenantConfig?.features?.registration && (

Don't have an account?{' '} - + Sign up - +

)} diff --git a/client/src/pages/Register.jsx b/client/src/pages/Register.jsx new file mode 100644 index 0000000..917ac81 --- /dev/null +++ b/client/src/pages/Register.jsx @@ -0,0 +1,395 @@ +import React, { useState, useEffect } from 'react'; +import { Navigate, Link } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; +import toast from 'react-hot-toast'; +import api from '../services/api'; + +const Register = () => { + const [formData, setFormData] = useState({ + username: '', + email: '', + password: '', + confirmPassword: '', + first_name: '', + last_name: '', + phone_number: '' + }); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [tenantConfig, setTenantConfig] = useState(null); + const [configLoading, setConfigLoading] = useState(true); + const [registering, setRegistering] = useState(false); + const { isAuthenticated } = useAuth(); + + // Fetch tenant configuration on mount + useEffect(() => { + const fetchTenantConfig = async () => { + try { + const response = await api.get('/auth/config'); + setTenantConfig(response.data.data); + + // Security check: If registration is not enabled, show error + if (!response.data.data?.features?.registration) { + toast.error('Registration is not enabled for this tenant'); + } + } catch (error) { + console.error('Failed to fetch tenant config:', error); + toast.error('Failed to load authentication configuration'); + } finally { + setConfigLoading(false); + } + }; + + fetchTenantConfig(); + }, []); + + if (isAuthenticated) { + return ; + } + + // Show loading while fetching config + if (configLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + // Block access if registration is not enabled + if (!tenantConfig?.features?.registration) { + return ( +
+
+
+ + + +
+

+ Registration Not Available +

+

+ Registration is not enabled for this tenant. Please contact your administrator. +

+
+ + Back to Login + +
+
+
+ ); + } + + // Block if not local authentication + if (tenantConfig?.provider !== 'local') { + return ( +
+
+
+ + + +
+

+ External Authentication +

+

+ This tenant uses {tenantConfig.provider.toUpperCase()} authentication. + Registration is handled through your organization's authentication system. +

+
+ + Go to Login + +
+
+
+ ); + } + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Validation + if (!formData.username || !formData.email || !formData.password) { + toast.error('Please fill in all required fields'); + return; + } + + if (formData.password !== formData.confirmPassword) { + toast.error('Passwords do not match'); + return; + } + + if (formData.password.length < 8) { + toast.error('Password must be at least 8 characters long'); + return; + } + + // Strong password validation + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/; + if (!passwordRegex.test(formData.password)) { + toast.error('Password must contain at least one lowercase letter, one uppercase letter, and one number'); + return; + } + + // Username validation + const usernameRegex = /^[a-zA-Z0-9._-]+$/; + if (!usernameRegex.test(formData.username)) { + toast.error('Username can only contain letters, numbers, dots, underscores, and hyphens'); + return; + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + toast.error('Please enter a valid email address'); + return; + } + + // Phone number validation (if provided) + if (formData.phone_number) { + const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/; + if (!phoneRegex.test(formData.phone_number.replace(/[\s\-\(\)]/g, ''))) { + toast.error('Please enter a valid phone number'); + return; + } + } + + setRegistering(true); + + try { + const { confirmPassword, ...registrationData } = formData; + const response = await api.post('/users/register', registrationData); + + if (response.data.success) { + toast.success('Registration successful! Please log in with your new account.'); + // Redirect to login page + window.location.href = '/login'; + } + } catch (error) { + console.error('Registration error:', error); + const errorMessage = error.response?.data?.message || 'Registration failed'; + toast.error(errorMessage); + } finally { + setRegistering(false); + } + }; + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + + return ( +
+
+
+
+ + + +
+

+ Create your account +

+

+ Register for {tenantConfig?.tenant_name || 'Drone Detection System'} +

+
+ +
+
+ {/* Username */} +
+ + +
+ + {/* Email */} +
+ + +
+ + {/* First Name */} +
+ + +
+ + {/* Last Name */} +
+ + +
+ + {/* Phone Number */} +
+ + +
+ + {/* Password */} +
+ +
+ + +
+
+ + {/* Confirm Password */} +
+ +
+ + +
+
+
+ +
+ +
+ +
+

+ Already have an account?{' '} + + Sign in + +

+
+
+
+
+ ); +}; + +export default Register; diff --git a/server/migrations/20250914-add-allow-registration.js b/server/migrations/20250914-add-allow-registration.js new file mode 100644 index 0000000..fd8acbc --- /dev/null +++ b/server/migrations/20250914-add-allow-registration.js @@ -0,0 +1,30 @@ +/** + * Migration: Add allow_registration field to tenants table + * This field controls whether self-registration is allowed for local auth tenants + */ + +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('tenants', 'allow_registration', { + type: Sequelize.BOOLEAN, + defaultValue: false, // Default to false for security + allowNull: false, + comment: 'Whether self-registration is allowed for local auth' + }); + + // For existing tenants, you might want to enable registration for specific tenants + // Uncomment the line below to enable registration for all existing tenants (NOT RECOMMENDED for production) + // await queryInterface.sequelize.query("UPDATE tenants SET allow_registration = true WHERE auth_provider = 'local'"); + + console.log('✅ Added allow_registration field to tenants table'); + console.log('⚠️ Registration is disabled by default for all tenants for security'); + console.log('💡 To enable registration for a tenant, update the allow_registration field to true'); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('tenants', 'allow_registration'); + console.log('✅ Removed allow_registration field from tenants table'); + } +}; diff --git a/server/models/Tenant.js b/server/models/Tenant.js index 70e4f87..2b43817 100644 --- a/server/models/Tenant.js +++ b/server/models/Tenant.js @@ -157,6 +157,11 @@ module.exports = (sequelize) => { defaultValue: true, comment: 'Whether users can have multiple concurrent sessions' }, + allow_registration: { + type: DataTypes.BOOLEAN, + defaultValue: false, // Default to false for security + comment: 'Whether self-registration is allowed for local auth' + }, role_mappings: { type: DataTypes.JSONB, allowNull: true, diff --git a/server/routes/auth.js b/server/routes/auth.js index 7b66757..1027d08 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -54,7 +54,7 @@ router.get('/config/:tenantId', async (req, res) => { features: { local_login: tenant.auth_provider === 'local', sso_login: ['saml', 'oauth', 'ldap'].includes(tenant.auth_provider), - registration: tenant.auth_provider === 'local' + registration: tenant.auth_provider === 'local' && tenant.allow_registration } }; diff --git a/server/routes/user.js b/server/routes/user.js index f7de313..76f3665 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -3,21 +3,65 @@ const router = express.Router(); const Joi = require('joi'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); +const rateLimit = require('express-rate-limit'); const { User, Tenant } = require('../models'); const { Op } = require('sequelize'); const { validateRequest } = require('../middleware/validation'); const { authenticateToken, requireRole } = require('../middleware/auth'); const MultiTenantAuth = require('../middleware/multi-tenant-auth'); -// Validation schemas +// Rate limiting for registration endpoint - EXTRA SECURITY +const registrationLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 3, // Limit each IP to 3 registration attempts per windowMs + message: { + success: false, + message: 'Too many registration attempts. Please try again later.' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +// Enhanced validation schema with stronger requirements const registerSchema = Joi.object({ - username: Joi.string().min(3).max(50).required(), - email: Joi.string().email().required(), - password: Joi.string().min(6).required(), - first_name: Joi.string().optional(), - last_name: Joi.string().optional(), - phone_number: Joi.string().optional(), - role: Joi.string().valid('admin', 'operator', 'viewer').default('viewer') + username: Joi.string() + .min(3) + .max(50) + .pattern(/^[a-zA-Z0-9._-]+$/) + .required() + .messages({ + 'string.pattern.base': 'Username can only contain letters, numbers, dots, underscores, and hyphens' + }), + email: Joi.string() + .email() + .required() + .max(255), + password: Joi.string() + .min(8) + .max(100) + .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) + .required() + .messages({ + 'string.pattern.base': 'Password must contain at least one lowercase letter, one uppercase letter, and one number' + }), + first_name: Joi.string() + .max(100) + .optional() + .allow(''), + last_name: Joi.string() + .max(100) + .optional() + .allow(''), + phone_number: Joi.string() + .pattern(/^[\+]?[1-9][\d]{0,15}$/) + .optional() + .allow('') + .messages({ + 'string.pattern.base': 'Please enter a valid phone number' + }), + role: Joi.string() + .valid('viewer') // Only allow viewer role for self-registration + .default('viewer') }); const loginSchema = Joi.object({ @@ -34,21 +78,111 @@ const updateProfileSchema = Joi.object({ timezone: Joi.string().optional() }); -// POST /api/users/register - Register new user -router.post('/register', validateRequest(registerSchema), async (req, res) => { +// POST /api/users/register - Register new user (ULTRA SECURE) +router.post('/register', registrationLimiter, validateRequest(registerSchema), async (req, res) => { try { + console.log('🔒 Registration attempt started'); + + // Step 1: Determine tenant context - CRITICAL SECURITY CHECK + const multiAuth = new MultiTenantAuth(); + const tenantId = await multiAuth.determineTenant(req); + console.log('🔍 Registration - Determined tenant:', tenantId); + + if (!tenantId) { + console.log('❌ Registration BLOCKED - No tenant determined'); + return res.status(400).json({ + success: false, + message: 'Unable to determine tenant context' + }); + } + + // Step 2: Get tenant from database - VERIFY TENANT EXISTS + const tenant = await Tenant.findOne({ where: { slug: tenantId } }); + if (!tenant) { + console.log('❌ Registration BLOCKED - Tenant not found:', tenantId); + return res.status(404).json({ + success: false, + message: 'Tenant not found' + }); + } + + // Step 3: TRIPLE CHECK - Tenant must be active + if (!tenant.is_active) { + console.log('❌ Registration BLOCKED - Tenant inactive:', tenantId); + return res.status(403).json({ + success: false, + message: 'Registration not available for this tenant' + }); + } + + // Step 4: CRITICAL SECURITY CHECK - Only local auth tenants allow registration + if (tenant.auth_provider !== 'local') { + console.log('❌ Registration BLOCKED - Non-local auth provider:', tenant.auth_provider); + return res.status(403).json({ + success: false, + message: 'Registration not available. This tenant uses external authentication.' + }); + } + + // Step 5: ULTIMATE SECURITY CHECK - Explicit registration permission + if (!tenant.allow_registration) { + console.log('❌ Registration BLOCKED - Registration disabled for tenant:', tenantId); + return res.status(403).json({ + success: false, + message: 'Registration is not enabled for this tenant. Please contact your administrator.' + }); + } + + // Step 6: Additional security - Check if registration is explicitly enabled in auth config + const authConfig = await multiAuth.getTenantAuthConfig(tenantId); + if (!authConfig.enabled) { + console.log('❌ Registration BLOCKED - Auth config disabled'); + return res.status(403).json({ + success: false, + message: 'Authentication is disabled for this tenant' + }); + } + + console.log('✅ Registration security checks passed for tenant:', tenantId); + + // Step 7: Process registration - User data validation const { password, ...userData } = req.body; - // Hash password + // Check if user already exists in this tenant + const existingUser = await User.findOne({ + where: { + [Op.or]: [ + { username: userData.username }, + { email: userData.email } + ], + tenant_id: tenant.id // Scope to specific tenant + } + }); + + if (existingUser) { + console.log('❌ Registration BLOCKED - User already exists in tenant'); + return res.status(409).json({ + success: false, + message: 'Username or email already exists' + }); + } + + // Step 8: Hash password with high security const saltRounds = 12; const password_hash = await bcrypt.hash(password, saltRounds); - // Create user + // Step 9: Create user with tenant association const user = await User.create({ ...userData, - password_hash + password_hash, + tenant_id: tenant.id, // CRITICAL: Associate with specific tenant + is_active: true, + created_at: new Date(), + updated_at: new Date() }); + console.log('✅ User registered successfully:', user.username, 'for tenant:', tenantId); + // Remove password hash from response const { password_hash: _, ...userResponse } = user.toJSON(); @@ -59,7 +193,7 @@ router.post('/register', validateRequest(registerSchema), async (req, res) => { }); } catch (error) { - console.error('Error registering user:', error); + console.error('❌ Registration error:', error); if (error.name === 'SequelizeUniqueConstraintError') { return res.status(409).json({ @@ -70,7 +204,7 @@ router.post('/register', validateRequest(registerSchema), async (req, res) => { res.status(500).json({ success: false, - message: 'Failed to register user', + message: 'Registration failed', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); }