# 🔐 Web Sentinel - Système d'Authentification & Multi-Utilisateurs ENTERPRISE

*Architecture complète pour authentification sécurisée et gestion multi-tenant*

## 🎯 Vue d'ensemble

Ce système d'authentification combine sécurité moderne (JWT, hashing PBKDF2) avec une gestion multi-utilisateurs sophistiquée pour les entreprises. Il supportera l'écosystème FREE/PRO/ENTERPRISE avec permissions granulaires.

---

## 🗄️ Schéma de base de données étendu

### 👥 **Table Organizations - Gestion entreprises**
```sql
-- Table des organisations/entreprises
CREATE TABLE organizations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    domain VARCHAR(255) UNIQUE, -- exemple.com pour validation email automatique
    subscription_tier ENUM('ENTERPRISE') DEFAULT 'ENTERPRISE',
    stripe_customer_id VARCHAR(255) UNIQUE,
    max_users INT DEFAULT 10,
    max_domains INT DEFAULT 100,
    max_api_calls_per_month INT DEFAULT 10000,
    settings JSON, -- Préférences org (notifications, branding, etc.)
    logo_url VARCHAR(500),
    timezone VARCHAR(50) DEFAULT 'Europe/Paris',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_domain (domain),
    INDEX idx_stripe_customer (stripe_customer_id)
);
```

### 🔐 **Table Roles - Système de permissions**
```sql
-- Table des rôles et permissions
CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    organization_id INT NOT NULL,
    name VARCHAR(100) NOT NULL, -- 'admin', 'security_analyst', 'viewer', 'custom_role'
    description TEXT,
    permissions JSON NOT NULL, -- Array des permissions détaillées
    is_default BOOLEAN DEFAULT FALSE,
    is_system BOOLEAN DEFAULT FALSE, -- Rôles système non modifiables
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
    UNIQUE KEY unique_org_role (organization_id, name),
    INDEX idx_org_roles (organization_id, is_default)
);
```

### 👤 **Table Users étendue**
```sql
-- Extension table users pour multi-org
ALTER TABLE users ADD COLUMN current_organization_id INT NULL;
ALTER TABLE users ADD COLUMN password_reset_token VARCHAR(255);
ALTER TABLE users ADD COLUMN password_reset_expires TIMESTAMP NULL;
ALTER TABLE users ADD COLUMN failed_login_attempts INT DEFAULT 0;
ALTER TABLE users ADD COLUMN locked_until TIMESTAMP NULL;
ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(32);
ALTER TABLE users ADD COLUMN backup_codes JSON;
ALTER TABLE users ADD COLUMN profile_picture_url VARCHAR(500);
ALTER TABLE users ADD COLUMN preferences JSON;

ALTER TABLE users ADD FOREIGN KEY (current_organization_id) REFERENCES organizations(id) ON DELETE SET NULL;
ALTER TABLE users ADD INDEX idx_reset_token (password_reset_token);
ALTER TABLE users ADD INDEX idx_failed_attempts (failed_login_attempts, locked_until);
```

### 🤝 **Table User-Organizations - Liaison multi-tenant**
```sql
-- Table de liaison utilisateurs-organisations
CREATE TABLE user_organizations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    organization_id INT NOT NULL,
    role_id INT NOT NULL,
    status ENUM('active', 'pending', 'suspended', 'left') DEFAULT 'pending',
    invited_by INT, -- user_id de celui qui a invité
    invited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    joined_at TIMESTAMP NULL,
    last_activity_at TIMESTAMP NULL,
    permissions_override JSON, -- Permissions spéciales pour cet utilisateur
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE RESTRICT,
    FOREIGN KEY (invited_by) REFERENCES users(id) ON DELETE SET NULL,
    
    UNIQUE KEY unique_user_org (user_id, organization_id),
    INDEX idx_org_users (organization_id, status),
    INDEX idx_user_orgs (user_id, status),
    INDEX idx_activity (last_activity_at)
);
```

### 📧 **Table Invitations**
```sql
-- Table des invitations en attente
CREATE TABLE organization_invitations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    organization_id INT NOT NULL,
    email VARCHAR(255) NOT NULL,
    role_id INT NOT NULL,
    invited_by INT NOT NULL,
    invitation_token VARCHAR(255) UNIQUE NOT NULL,
    personal_message TEXT,
    expires_at TIMESTAMP NOT NULL,
    accepted_at TIMESTAMP NULL,
    declined_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
    FOREIGN KEY (invited_by) REFERENCES users(id) ON DELETE CASCADE,
    
    INDEX idx_token (invitation_token),
    INDEX idx_org_invitations (organization_id, accepted_at),
    INDEX idx_email_pending (email, expires_at)
);
```

### 📊 **Table Sessions & Audit**
```sql
-- Table des sessions utilisateur
CREATE TABLE user_sessions (
    id VARCHAR(255) PRIMARY KEY, -- UUID session
    user_id INT NOT NULL,
    organization_id INT,
    ip_address INET6,
    user_agent TEXT,
    device_fingerprint VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
    INDEX idx_user_sessions (user_id, is_active),
    INDEX idx_expires (expires_at),
    INDEX idx_activity (last_activity_at)
);

-- Table d'audit des actions utilisateur
CREATE TABLE audit_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    organization_id INT,
    action VARCHAR(100) NOT NULL, -- 'login', 'logout', 'scan_created', 'user_invited', etc.
    resource_type VARCHAR(50), -- 'user', 'scan', 'organization', etc.
    resource_id VARCHAR(50),
    old_values JSON,
    new_values JSON,
    ip_address INET6,
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE SET NULL,
    INDEX idx_user_audit (user_id, created_at),
    INDEX idx_org_audit (organization_id, created_at),
    INDEX idx_action_audit (action, created_at)
);
```

---

## 🔐 Système de permissions granulaires

### **Définition des permissions**
```python
# web_sentinel/auth/permissions.py
from enum import Enum
from typing import List, Dict, Set

class Permission(Enum):
    # === PERMISSIONS DE SCAN ===
    VIEW_SCANS = "view_scans"
    CREATE_SCANS = "create_scans"
    DELETE_SCANS = "delete_scans"
    DELETE_OWN_SCANS = "delete_own_scans"
    EXPORT_REPORTS = "export_reports"
    SCHEDULE_SCANS = "schedule_scans"
    
    # === PERMISSIONS DE GESTION UTILISATEURS ===
    VIEW_USERS = "view_users"
    INVITE_USERS = "invite_users"
    MANAGE_USERS = "manage_users"
    SUSPEND_USERS = "suspend_users"
    DELETE_USERS = "delete_users"
    
    # === PERMISSIONS DE RÔLES ===
    VIEW_ROLES = "view_roles"
    CREATE_ROLES = "create_roles"
    MODIFY_ROLES = "modify_roles"
    DELETE_ROLES = "delete_roles"
    ASSIGN_ROLES = "assign_roles"
    
    # === PERMISSIONS DE DOMAINES ===
    VIEW_DOMAINS = "view_domains"
    MANAGE_DOMAINS = "manage_domains"
    APPROVE_DOMAINS = "approve_domains"
    
    # === PERMISSIONS ADMINISTRATIVES ===
    MANAGE_ORGANIZATION = "manage_organization"
    MANAGE_BILLING = "manage_billing"
    VIEW_ANALYTICS = "view_analytics"
    MANAGE_INTEGRATIONS = "manage_integrations"
    VIEW_AUDIT_LOGS = "view_audit_logs"
    
    # === PERMISSIONS API ===
    API_ACCESS = "api_access"
    API_ADMIN = "api_admin"
    WEBHOOK_MANAGEMENT = "webhook_management"
    
    # === PERMISSIONS TECHNIQUES ===
    EXPORT_DATA = "export_data"
    IMPORT_DATA = "import_data"
    MANAGE_BACKUPS = "manage_backups"

class RoleTemplate:
    """Templates de rôles prédéfinis avec permissions optimisées"""
    
    SUPER_ADMIN = {
        "name": "Super Admin",
        "description": "Accès complet - Tous droits",
        "permissions": [p.value for p in Permission],
        "is_system": True
    }
    
    ADMIN = {
        "name": "Admin",
        "description": "Administrateur organisation",
        "permissions": [
            Permission.VIEW_SCANS.value, Permission.CREATE_SCANS.value, Permission.DELETE_SCANS.value,
            Permission.EXPORT_REPORTS.value, Permission.SCHEDULE_SCANS.value,
            Permission.VIEW_USERS.value, Permission.INVITE_USERS.value, Permission.MANAGE_USERS.value,
            Permission.SUSPEND_USERS.value, Permission.VIEW_ROLES.value, Permission.ASSIGN_ROLES.value,
            Permission.VIEW_DOMAINS.value, Permission.MANAGE_DOMAINS.value, Permission.APPROVE_DOMAINS.value,
            Permission.MANAGE_ORGANIZATION.value, Permission.MANAGE_BILLING.value,
            Permission.VIEW_ANALYTICS.value, Permission.MANAGE_INTEGRATIONS.value,
            Permission.VIEW_AUDIT_LOGS.value, Permission.API_ACCESS.value, Permission.WEBHOOK_MANAGEMENT.value
        ],
        "is_system": True
    }
    
    SECURITY_MANAGER = {
        "name": "Security Manager",
        "description": "Gestionnaire sécurité - Scans et utilisateurs",
        "permissions": [
            Permission.VIEW_SCANS.value, Permission.CREATE_SCANS.value, Permission.DELETE_OWN_SCANS.value,
            Permission.EXPORT_REPORTS.value, Permission.SCHEDULE_SCANS.value,
            Permission.VIEW_USERS.value, Permission.INVITE_USERS.value,
            Permission.VIEW_DOMAINS.value, Permission.MANAGE_DOMAINS.value,
            Permission.VIEW_ANALYTICS.value, Permission.API_ACCESS.value
        ],
        "is_system": True
    }
    
    SECURITY_ANALYST = {
        "name": "Security Analyst",
        "description": "Analyste sécurité - Exécution et analyse",
        "permissions": [
            Permission.VIEW_SCANS.value, Permission.CREATE_SCANS.value, Permission.DELETE_OWN_SCANS.value,
            Permission.EXPORT_REPORTS.value, Permission.SCHEDULE_SCANS.value,
            Permission.VIEW_DOMAINS.value, Permission.VIEW_ANALYTICS.value, Permission.API_ACCESS.value
        ],
        "is_system": True
    }
    
    VIEWER = {
        "name": "Viewer", 
        "description": "Consultation uniquement",
        "permissions": [
            Permission.VIEW_SCANS.value, Permission.EXPORT_REPORTS.value, Permission.VIEW_ANALYTICS.value
        ],
        "is_system": True
    }

def check_permission(user_permissions: List[str], required_permission: Permission) -> bool:
    """Vérification permission avec support hiérarchique"""
    return required_permission.value in user_permissions

def get_permission_hierarchy() -> Dict[Permission, List[Permission]]:
    """Hiérarchie des permissions (permission parent inclut enfants)"""
    return {
        Permission.MANAGE_USERS: [Permission.VIEW_USERS, Permission.INVITE_USERS],
        Permission.MANAGE_ORGANIZATION: [Permission.VIEW_ANALYTICS, Permission.MANAGE_INTEGRATIONS],
        Permission.DELETE_SCANS: [Permission.DELETE_OWN_SCANS, Permission.VIEW_SCANS],
        Permission.API_ADMIN: [Permission.API_ACCESS]
    }
```

---

## 🔒 Gestionnaire d'authentification sécurisé

### **Classe principale d'authentification**
```python
# web_sentinel/auth/auth_manager.py
import hashlib
import secrets
import jwt
import pyotp
import qrcode
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from io import BytesIO
import bcrypt

class AuthenticationManager:
    def __init__(self, db_manager, jwt_secret: str, session_timeout: int = 3600):
        self.db = db_manager
        self.jwt_secret = jwt_secret
        self.session_timeout = session_timeout
        self.max_failed_attempts = 5
        self.lockout_duration = timedelta(minutes=30)
    
    def hash_password(self, password: str) -> str:
        """Hashing sécurisé avec bcrypt + salt"""
        salt = bcrypt.gensalt(rounds=12)
        return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
    
    def verify_password(self, password: str, hashed: str) -> bool:
        """Vérification mot de passe"""
        return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
    
    def is_account_locked(self, user_id: int) -> bool:
        """Vérification si compte verrouillé"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                SELECT locked_until FROM users 
                WHERE id = %s AND locked_until > NOW()
            """, (user_id,))
            return cursor.fetchone() is not None
    
    def increment_failed_attempts(self, user_id: int) -> bool:
        """Incrémentation tentatives échouées"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute("""
                UPDATE users 
                SET failed_login_attempts = failed_login_attempts + 1,
                    locked_until = CASE 
                        WHEN failed_login_attempts + 1 >= %s 
                        THEN DATE_ADD(NOW(), INTERVAL %s MINUTE)
                        ELSE locked_until 
                    END
                WHERE id = %s
            """, (self.max_failed_attempts, self.lockout_duration.total_seconds() // 60, user_id))
            
            # Vérifier si compte maintenant verrouillé
            cursor.execute("SELECT failed_login_attempts FROM users WHERE id = %s", (user_id,))
            attempts = cursor.fetchone()[0]
            return attempts >= self.max_failed_attempts
    
    def reset_failed_attempts(self, user_id: int):
        """Reset compteur après connexion réussie"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE users 
                SET failed_login_attempts = 0, locked_until = NULL 
                WHERE id = %s
            """, (user_id,))
    
    def authenticate_user(self, email: str, password: str, 
                         ip_address: str = None, user_agent: str = None,
                         totp_code: str = None) -> Optional[Dict[str, Any]]:
        """Authentification complète avec 2FA optionnel"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # Récupérer utilisateur
            cursor.execute("""
                SELECT id, email, password_hash, two_factor_enabled, 
                       two_factor_secret, failed_login_attempts, locked_until,
                       current_organization_id
                FROM users 
                WHERE email = %s AND email_verified = TRUE
            """, (email,))
            
            user = cursor.fetchone()
            if not user:
                return None
            
            # Vérifier verrouillage
            if self.is_account_locked(user['id']):
                raise AuthenticationError("Account temporarily locked due to failed attempts")
            
            # Vérifier mot de passe
            if not self.verify_password(password, user['password_hash']):
                self.increment_failed_attempts(user['id'])
                return None
            
            # Vérifier 2FA si activé
            if user['two_factor_enabled']:
                if not totp_code:
                    raise TwoFactorRequiredError("2FA code required")
                
                if not self.verify_totp(user['two_factor_secret'], totp_code):
                    self.increment_failed_attempts(user['id'])
                    return None
            
            # Connexion réussie
            self.reset_failed_attempts(user['id'])
            
            # Créer session
            session_id = self.create_session(
                user['id'], user['current_organization_id'], 
                ip_address, user_agent
            )
            
            # Générer JWT
            token = self.generate_jwt_token(user['id'], session_id)
            
            # Log audit
            self.log_audit_event(user['id'], user['current_organization_id'], 
                               'login', ip_address=ip_address)
            
            return {
                'user_id': user['id'],
                'email': user['email'],
                'organization_id': user['current_organization_id'],
                'session_id': session_id,
                'access_token': token,
                'expires_in': self.session_timeout
            }
    
    def generate_jwt_token(self, user_id: int, session_id: str) -> str:
        """Génération JWT avec expiration"""
        payload = {
            'user_id': user_id,
            'session_id': session_id,
            'iat': datetime.utcnow(),
            'exp': datetime.utcnow() + timedelta(seconds=self.session_timeout)
        }
        return jwt.encode(payload, self.jwt_secret, algorithm='HS256')
    
    def verify_jwt_token(self, token: str) -> Optional[Dict[str, Any]]:
        """Vérification et décodage JWT"""
        try:
            payload = jwt.decode(token, self.jwt_secret, algorithms=['HS256'])
            
            # Vérifier que la session est active
            if self.is_session_active(payload['session_id']):
                return payload
            
        except jwt.ExpiredSignatureError:
            pass
        except jwt.InvalidTokenError:
            pass
        
        return None
    
    def create_session(self, user_id: int, organization_id: int = None,
                      ip_address: str = None, user_agent: str = None) -> str:
        """Création session avec fingerprinting"""
        session_id = secrets.token_urlsafe(32)
        device_fingerprint = self.generate_device_fingerprint(ip_address, user_agent)
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO user_sessions 
                (id, user_id, organization_id, ip_address, user_agent, 
                 device_fingerprint, expires_at)
                VALUES (%s, %s, %s, %s, %s, %s, %s)
            """, (
                session_id, user_id, organization_id,
                ip_address, user_agent, device_fingerprint,
                datetime.now() + timedelta(seconds=self.session_timeout)
            ))
        
        return session_id
    
    def is_session_active(self, session_id: str) -> bool:
        """Vérification validité session"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                SELECT id FROM user_sessions 
                WHERE id = %s AND is_active = TRUE AND expires_at > NOW()
            """, (session_id,))
            return cursor.fetchone() is not None
    
    def logout_user(self, session_id: str, user_id: int = None):
        """Déconnexion et invalidation session"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE user_sessions 
                SET is_active = FALSE 
                WHERE id = %s
            """, (session_id,))
            
            if user_id:
                self.log_audit_event(user_id, None, 'logout')

class TwoFactorManager:
    def __init__(self, auth_manager: AuthenticationManager):
        self.auth = auth_manager
    
    def setup_2fa(self, user_id: int) -> Dict[str, Any]:
        """Configuration 2FA pour utilisateur"""
        secret = pyotp.random_base32()
        
        with self.auth.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # Récupérer email pour QR code
            cursor.execute("SELECT email FROM users WHERE id = %s", (user_id,))
            user = cursor.fetchone()
            
            # Générer codes de récupération
            backup_codes = [secrets.token_hex(8) for _ in range(10)]
            backup_codes_hashed = [self.auth.hash_password(code) for code in backup_codes]
            
            # Sauvegarder secret temporaire (non activé)
            cursor.execute("""
                UPDATE users 
                SET two_factor_secret = %s, backup_codes = %s
                WHERE id = %s
            """, (secret, json.dumps(backup_codes_hashed), user_id))
        
        # Générer QR code
        totp = pyotp.TOTP(secret)
        provisioning_uri = totp.provisioning_uri(
            name=user['email'],
            issuer_name="Web Sentinel"
        )
        
        qr = qrcode.QRCode(version=1, box_size=10, border=5)
        qr.add_data(provisioning_uri)
        qr.make(fit=True)
        
        img = qr.make_image(fill_color="black", back_color="white")
        buffer = BytesIO()
        img.save(buffer, format='PNG')
        
        return {
            'secret': secret,
            'qr_code': buffer.getvalue(),
            'backup_codes': backup_codes,  # Montrer une seule fois
            'manual_entry_key': secret
        }
    
    def enable_2fa(self, user_id: int, verification_code: str) -> bool:
        """Activation 2FA après vérification"""
        with self.auth.db.get_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute("""
                SELECT two_factor_secret FROM users WHERE id = %s
            """, (user_id,))
            
            secret = cursor.fetchone()[0]
            if self.auth.verify_totp(secret, verification_code):
                cursor.execute("""
                    UPDATE users SET two_factor_enabled = TRUE WHERE id = %s
                """, (user_id,))
                return True
        
        return False
    
    def disable_2fa(self, user_id: int, password: str) -> bool:
        """Désactivation 2FA avec confirmation mot de passe"""
        with self.auth.db.get_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute("""
                SELECT password_hash FROM users WHERE id = %s
            """, (user_id,))
            
            if self.auth.verify_password(password, cursor.fetchone()[0]):
                cursor.execute("""
                    UPDATE users 
                    SET two_factor_enabled = FALSE, 
                        two_factor_secret = NULL,
                        backup_codes = NULL
                    WHERE id = %s
                """, (user_id,))
                return True
        
        return False
```

---

## 🏢 Gestionnaire d'organisations ENTERPRISE

### **Gestion multi-tenant avancée**
```python
# web_sentinel/auth/organization_manager.py
from typing import List, Dict, Optional
import json
from datetime import datetime, timedelta

class OrganizationManager:
    def __init__(self, db_manager, auth_manager):
        self.db = db_manager
        self.auth = auth_manager
    
    def create_organization(self, name: str, admin_user_id: int, 
                           domain: str = None, max_users: int = 50) -> int:
        """Création organisation avec admin et rôles par défaut"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            # Créer organisation
            cursor.execute("""
                INSERT INTO organizations (name, domain, max_users, max_domains, max_api_calls_per_month)
                VALUES (%s, %s, %s, %s, %s)
            """, (name, domain, max_users, max_users * 10, max_users * 1000))
            
            org_id = cursor.lastrowid
            
            # Créer rôles système
            role_ids = {}
            for role_template in [RoleTemplate.SUPER_ADMIN, RoleTemplate.ADMIN, 
                                 RoleTemplate.SECURITY_MANAGER, RoleTemplate.SECURITY_ANALYST, 
                                 RoleTemplate.VIEWER]:
                cursor.execute("""
                    INSERT INTO roles (organization_id, name, description, permissions, is_system, is_default)
                    VALUES (%s, %s, %s, %s, %s, %s)
                """, (
                    org_id,
                    role_template["name"],
                    role_template["description"],
                    json.dumps(role_template["permissions"]),
                    role_template.get("is_system", False),
                    role_template["name"] == "Security Analyst"
                ))
                
                role_ids[role_template["name"]] = cursor.lastrowid
            
            # Assigner admin principal
            cursor.execute("""
                INSERT INTO user_organizations (user_id, organization_id, role_id, status, joined_at)
                VALUES (%s, %s, %s, 'active', NOW())
            """, (admin_user_id, org_id, role_ids["Super Admin"]))
            
            # Mettre à jour organisation courante de l'admin
            cursor.execute("""
                UPDATE users SET current_organization_id = %s WHERE id = %s
            """, (org_id, admin_user_id))
            
            # Log création
            self.auth.log_audit_event(admin_user_id, org_id, 'organization_created', 
                                    new_values={'name': name, 'org_id': org_id})
            
            return org_id
    
    def invite_user_to_organization(self, org_id: int, email: str, role_id: int, 
                                   invited_by: int, personal_message: str = None) -> str:
        """Invitation utilisateur avec validation des limites"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            # Vérifier limites organisation
            cursor.execute("""
                SELECT COUNT(uo.id) as current_users, o.max_users
                FROM organizations o
                LEFT JOIN user_organizations uo ON o.id = uo.organization_id 
                    AND uo.status IN ('active', 'pending')
                WHERE o.id = %s
                GROUP BY o.max_users
            """, (org_id,))
            
            result = cursor.fetchone()
            if result and result[0] >= result[1]:
                raise OrganizationLimitError(f"User limit reached ({result[1]})")
            
            # Vérifier si invitation déjà envoyée
            cursor.execute("""
                SELECT id FROM organization_invitations 
                WHERE organization_id = %s AND email = %s 
                AND expires_at > NOW() AND accepted_at IS NULL
            """, (org_id, email))
            
            if cursor.fetchone():
                raise InvitationError("Active invitation already exists for this email")
            
            # Créer invitation
            token = secrets.token_urlsafe(32)
            expires_at = datetime.now() + timedelta(days=7)
            
            cursor.execute("""
                INSERT INTO organization_invitations 
                (organization_id, email, role_id, invited_by, invitation_token, 
                 personal_message, expires_at)
                VALUES (%s, %s, %s, %s, %s, %s, %s)
            """, (org_id, email, role_id, invited_by, token, personal_message, expires_at))
            
            # Log invitation
            self.auth.log_audit_event(invited_by, org_id, 'user_invited', 
                                    new_values={'email': email, 'role_id': role_id})
            
            return token
    
    def accept_invitation(self, token: str, user_id: int) -> bool:
        """Acceptation invitation par utilisateur"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # Récupérer invitation valide
            cursor.execute("""
                SELECT oi.*, u.email as user_email
                FROM organization_invitations oi
                JOIN users u ON u.id = %s
                WHERE oi.invitation_token = %s 
                AND oi.expires_at > NOW() 
                AND oi.accepted_at IS NULL
                AND oi.email = u.email
            """, (user_id, token))
            
            invitation = cursor.fetchone()
            if not invitation:
                return False
            
            # Ajouter utilisateur à l'organisation
            cursor.execute("""
                INSERT INTO user_organizations (user_id, organization_id, role_id, status, joined_at)
                VALUES (%s, %s, %s, 'active', NOW())
                ON DUPLICATE KEY UPDATE 
                role_id = VALUES(role_id), 
                status = 'active', 
                joined_at = NOW()
            """, (user_id, invitation['organization_id'], invitation['role_id']))
            
            # Marquer invitation comme acceptée
            cursor.execute("""
                UPDATE organization_invitations 
                SET accepted_at = NOW() 
                WHERE id = %s
            """, (invitation['id'],))
            
            # Si première organisation, la définir comme courante
            cursor.execute("SELECT current_organization_id FROM users WHERE id = %s", (user_id,))
            if not cursor.fetchone()[0]:
                cursor.execute("""
                    UPDATE users SET current_organization_id = %s WHERE id = %s
                """, (invitation['organization_id'], user_id))
            
            # Log acceptation
            self.auth.log_audit_event(user_id, invitation['organization_id'], 
                                    'invitation_accepted')
            
            return True
    
    def get_user_permissions(self, user_id: int, org_id: int) -> List[str]:
        """Récupération permissions utilisateur avec override"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            cursor.execute("""
                SELECT r.permissions, uo.permissions_override
                FROM user_organizations uo
                JOIN roles r ON uo.role_id = r.id
                WHERE uo.user_id = %s 
                AND uo.organization_id = %s 
                AND uo.status = 'active'
            """, (user_id, org_id))
            
            result = cursor.fetchone()
            if not result:
                return []
            
            # Permissions de base du rôle
            permissions = set(json.loads(result['permissions']))
            
            # Appliquer override si défini
            if result['permissions_override']:
                override = json.loads(result['permissions_override'])
                
                # Ajouter permissions supplémentaires
                if 'add' in override:
                    permissions.update(override['add'])
                
                # Retirer permissions spécifiques
                if 'remove' in override:
                    permissions.difference_update(override['remove'])
            
            return list(permissions)
    
    def switch_organization(self, user_id: int, org_id: int) -> bool:
        """Changement d'organisation active"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            # Vérifier appartenance
            cursor.execute("""
                SELECT id FROM user_organizations 
                WHERE user_id = %s AND organization_id = %s AND status = 'active'
            """, (user_id, org_id))
            
            if cursor.fetchone():
                cursor.execute("""
                    UPDATE users SET current_organization_id = %s WHERE id = %s
                """, (org_id, user_id))
                
                # Log changement
                self.auth.log_audit_event(user_id, org_id, 'organization_switched')
                return True
            
            return False
    
    def get_organization_users(self, org_id: int, include_pending: bool = True) -> List[Dict]:
        """Liste utilisateurs de l'organisation"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            status_filter = "('active', 'pending')" if include_pending else "('active')"
            
            cursor.execute(f"""
                SELECT 
                    u.id, u.email, u.created_at as user_created_at,
                    uo.status, uo.joined_at, uo.last_activity_at,
                    r.name as role_name, r.description as role_description,
                    COUNT(sr.id) as scan_count
                FROM user_organizations uo
                JOIN users u ON uo.user_id = u.id
                JOIN roles r ON uo.role_id = r.id
                LEFT JOIN scan_results sr ON u.id = sr.user_id 
                    AND sr.organization_id = uo.organization_id
                WHERE uo.organization_id = %s 
                AND uo.status IN {status_filter}
                GROUP BY u.id, uo.id, r.id
                ORDER BY uo.joined_at DESC
            """, (org_id,))
            
            return cursor.fetchall()

class OrganizationLimitError(Exception):
    pass

class InvitationError(Exception):
    pass
```

---

## 🌐 API REST sécurisée

### **Endpoints d'authentification**
```python
# web_sentinel/api/auth_endpoints.py
from flask import Blueprint, request, jsonify, g
from functools import wraps
import re

auth_bp = Blueprint('auth', __name__, url_prefix='/api/v1/auth')

def require_auth(f):
    """Décorateur d'authentification JWT"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not token or not token.startswith('Bearer '):
            return jsonify({'error': 'Authentication required'}), 401
        
        try:
            payload = g.auth_manager.verify_jwt_token(token[7:])
            if not payload:
                return jsonify({'error': 'Invalid or expired token'}), 401
            
            g.current_user_id = payload['user_id']
            g.current_session_id = payload['session_id']
            
            # Charger contexte utilisateur
            g.current_user = g.user_manager.get_user_context(payload['user_id'])
            
        except Exception as e:
            return jsonify({'error': 'Authentication failed'}), 401
        
        return f(*args, **kwargs)
    return decorated_function

@auth_bp.route('/login', methods=['POST'])
def login():
    """Authentification utilisateur"""
    data = request.get_json()
    
    if not data.get('email') or not data.get('password'):
        return jsonify({'error': 'Email and password required'}), 400
    
    # Validation email
    if not re.match(r'^[^@]+@[^@]+\.[^@]+$', data['email']):
        return jsonify({'error': 'Invalid email format'}), 400
    
    try:
        result = g.auth_manager.authenticate_user(
            email=data['email'],
            password=data['password'],
            ip_address=request.remote_addr,
            user_agent=request.headers.get('User-Agent'),
            totp_code=data.get('totp_code')
        )
        
        if result:
            return jsonify({
                'success': True,
                'access_token': result['access_token'],
                'expires_in': result['expires_in'],
                'user': {
                    'id': result['user_id'],
                    'email': data['email'],
                    'organization_id': result['organization_id']
                }
            })
        else:
            return jsonify({'error': 'Invalid credentials'}), 401
    
    except AuthenticationError as e:
        return jsonify({'error': str(e)}), 401
    except TwoFactorRequiredError:
        return jsonify({'error': '2FA required', 'requires_2fa': True}), 200

@auth_bp.route('/logout', methods=['POST'])
@require_auth
def logout():
    """Déconnexion utilisateur"""
    g.auth_manager.logout_user(g.current_session_id, g.current_user_id)
    return jsonify({'success': True})

@auth_bp.route('/register', methods=['POST'])
def register():
    """Inscription utilisateur"""
    data = request.get_json()
    
    required_fields = ['email', 'password']
    if not all(field in data for field in required_fields):
        return jsonify({'error': 'Missing required fields'}), 400
    
    # Validation mot de passe
    if len(data['password']) < 8:
        return jsonify({'error': 'Password must be at least 8 characters'}), 400
    
    try:
        user_id = g.user_manager.create_user(data['email'], data['password'])
        
        # Envoyer email de vérification
        g.email_manager.send_verification_email(data['email'])
        
        return jsonify({
            'success': True,
            'user_id': user_id,
            'message': 'Verification email sent'
        }), 201
        
    except UserExistsError:
        return jsonify({'error': 'Email already registered'}), 409

@auth_bp.route('/2fa/setup', methods=['POST'])
@require_auth
def setup_2fa():
    """Configuration 2FA"""
    result = g.two_factor_manager.setup_2fa(g.current_user_id)
    
    return jsonify({
        'secret': result['secret'],
        'qr_code_url': f"data:image/png;base64,{base64.b64encode(result['qr_code']).decode()}",
        'backup_codes': result['backup_codes'],
        'manual_entry_key': result['manual_entry_key']
    })

@auth_bp.route('/2fa/enable', methods=['POST'])
@require_auth
def enable_2fa():
    """Activation 2FA"""
    data = request.get_json()
    
    if not data.get('verification_code'):
        return jsonify({'error': 'Verification code required'}), 400
    
    if g.two_factor_manager.enable_2fa(g.current_user_id, data['verification_code']):
        return jsonify({'success': True, 'message': '2FA enabled'})
    else:
        return jsonify({'error': 'Invalid verification code'}), 400

@auth_bp.route('/organizations/switch', methods=['POST'])
@require_auth
def switch_organization():
    """Changement d'organisation"""
    data = request.get_json()
    org_id = data.get('organization_id')
    
    if not org_id:
        return jsonify({'error': 'Organization ID required'}), 400
    
    if g.org_manager.switch_organization(g.current_user_id, org_id):
        return jsonify({'success': True})
    else:
        return jsonify({'error': 'Access denied or invalid organization'}), 403
```

---

## 🎛️ Interface utilisateur intégrée

### **Composant d'authentification GUI**
```python
# web_sentinel/gui/auth_window.py
import tkinter as tk
from tkinter import ttk, messagebox
import qrcode
from PIL import Image, ImageTk

class AuthenticationWindow:
    def __init__(self, auth_manager, on_success_callback):
        self.auth_manager = auth_manager
        self.on_success = on_success_callback
        
        self.window = tk.Tk()
        self.window.title("🔐 Web Sentinel - Authentification")
        self.window.geometry("400x500")
        self.window.resizable(False, False)
        
        self.setup_ui()
    
    def setup_ui(self):
        """Interface de connexion avec onglets"""
        style = ttk.Style()
        style.theme_use('clam')
        
        # Notebook pour Login/Register
        self.notebook = ttk.Notebook(self.window)
        
        # Onglet Login
        self.login_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.login_frame, text="🔑 Connexion")
        self.setup_login_tab()
        
        # Onglet Register
        self.register_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.register_frame, text="📝 Inscription")
        self.setup_register_tab()
        
        self.notebook.pack(expand=True, fill='both', padx=20, pady=20)
    
    def setup_login_tab(self):
        """Onglet de connexion"""
        # Logo/Titre
        title_label = ttk.Label(self.login_frame, text="🛡️ Web Sentinel", 
                               font=("Arial", 18, "bold"))
        title_label.pack(pady=20)
        
        # Email
        ttk.Label(self.login_frame, text="Email:").pack(anchor="w", padx=50)
        self.email_var = tk.StringVar()
        email_entry = ttk.Entry(self.login_frame, textvariable=self.email_var, width=30)
        email_entry.pack(pady=5)
        
        # Mot de passe
        ttk.Label(self.login_frame, text="Mot de passe:").pack(anchor="w", padx=50)
        self.password_var = tk.StringVar()
        password_entry = ttk.Entry(self.login_frame, textvariable=self.password_var, 
                                  show="*", width=30)
        password_entry.pack(pady=5)
        
        # 2FA Code (masqué par défaut)
        self.totp_frame = ttk.Frame(self.login_frame)
        ttk.Label(self.totp_frame, text="Code 2FA:").pack(anchor="w")
        self.totp_var = tk.StringVar()
        ttk.Entry(self.totp_frame, textvariable=self.totp_var, width=30).pack(pady=5)
        
        # Remember me
        self.remember_var = tk.BooleanVar()
        ttk.Checkbutton(self.login_frame, text="Se souvenir de moi", 
                       variable=self.remember_var).pack(pady=10)
        
        # Boutons
        login_btn = ttk.Button(self.login_frame, text="🔑 Se connecter", 
                              command=self.handle_login)
        login_btn.pack(pady=10)
        
        forgot_btn = ttk.Button(self.login_frame, text="Mot de passe oublié?", 
                               command=self.show_password_reset)
        forgot_btn.pack()
        
        # Bind Enter key
        password_entry.bind('<Return>', lambda e: self.handle_login())
    
    def handle_login(self):
        """Traitement connexion"""
        email = self.email_var.get().strip()
        password = self.password_var.get()
        totp_code = self.totp_var.get().strip() or None
        
        if not email or not password:
            messagebox.showerror("Erreur", "Email et mot de passe requis")
            return
        
        try:
            result = self.auth_manager.authenticate_user(
                email=email,
                password=password,
                totp_code=totp_code,
                ip_address="127.0.0.1",  # GUI local
                user_agent="Web Sentinel Desktop"
            )
            
            if result:
                messagebox.showinfo("Succès", "Connexion réussie!")
                self.window.destroy()
                self.on_success(result)
            else:
                messagebox.showerror("Erreur", "Identifiants invalides")
                
        except TwoFactorRequiredError:
            # Afficher champ 2FA
            self.totp_frame.pack(pady=5)
            messagebox.showinfo("2FA", "Code d'authentification à deux facteurs requis")
            
        except AuthenticationError as e:
            messagebox.showerror("Erreur", str(e))

class OrganizationSelectorWindow:
    """Sélecteur d'organisation pour utilisateurs multi-org"""
    
    def __init__(self, parent, user_organizations, current_org_id, org_manager):
        self.parent = parent
        self.orgs = user_organizations
        self.current_org_id = current_org_id
        self.org_manager = org_manager
        self.selected_org = None
        
        self.window = tk.Toplevel(parent)
        self.window.title("🏢 Sélection Organisation")
        self.window.geometry("500x400")
        self.window.transient(parent)
        self.window.grab_set()
        
        self.setup_ui()
    
    def setup_ui(self):
        """Interface sélection organisation"""
        title = ttk.Label(self.window, text="Choisir votre organisation", 
                         font=("Arial", 14, "bold"))
        title.pack(pady=20)
        
        # Liste des organisations
        self.org_listbox = tk.Listbox(self.window, height=10)
        
        for org in self.orgs:
            status_icon = "🟢" if org['id'] == self.current_org_id else "⚪"
            self.org_listbox.insert(tk.END, 
                f"{status_icon} {org['name']} ({org['role_name']})")
        
        self.org_listbox.pack(expand=True, fill='both', padx=20, pady=10)
        
        # Boutons
        btn_frame = ttk.Frame(self.window)
        btn_frame.pack(pady=10)
        
        ttk.Button(btn_frame, text="✅ Sélectionner", 
                  command=self.select_organization).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="❌ Annuler", 
                  command=self.window.destroy).pack(side="left", padx=5)
    
    def select_organization(self):
        """Sélection et changement d'organisation"""
        selection = self.org_listbox.curselection()
        if not selection:
            messagebox.showwarning("Attention", "Veuillez sélectionner une organisation")
            return
        
        org_index = selection[0]
        selected_org = self.orgs[org_index]
        
        if selected_org['id'] != self.current_org_id:
            if self.org_manager.switch_organization(
                g.current_user_id, selected_org['id']
            ):
                messagebox.showinfo("Succès", 
                    f"Basculé vers {selected_org['name']}")
                self.window.destroy()
                # Recharger interface principale
                self.parent.reload_with_organization(selected_org['id'])
            else:
                messagebox.showerror("Erreur", "Impossible de changer d'organisation")
        else:
            self.window.destroy()
```

---

## 🔍 Audit et monitoring

### **Système d'audit complet**
```python
# web_sentinel/auth/audit_manager.py
class AuditManager:
    def __init__(self, db_manager):
        self.db = db_manager
    
    def log_event(self, user_id: int, organization_id: int, action: str,
                  resource_type: str = None, resource_id: str = None,
                  old_values: dict = None, new_values: dict = None,
                  ip_address: str = None, user_agent: str = None):
        """Enregistrement événement audit"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO audit_logs 
                (user_id, organization_id, action, resource_type, resource_id,
                 old_values, new_values, ip_address, user_agent)
                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
            """, (
                user_id, organization_id, action, resource_type, resource_id,
                json.dumps(old_values) if old_values else None,
                json.dumps(new_values) if new_values else None,
                ip_address, user_agent
            ))
    
    def get_organization_audit_trail(self, org_id: int, 
                                   start_date: datetime = None,
                                   end_date: datetime = None,
                                   action_filter: str = None) -> List[Dict]:
        """Récupération audit trail organisation"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            where_conditions = ["organization_id = %s"]
            params = [org_id]
            
            if start_date:
                where_conditions.append("created_at >= %s")
                params.append(start_date)
            
            if end_date:
                where_conditions.append("created_at <= %s")
                params.append(end_date)
            
            if action_filter:
                where_conditions.append("action LIKE %s")
                params.append(f"%{action_filter}%")
            
            query = f"""
                SELECT 
                    al.*, u.email as user_email
                FROM audit_logs al
                LEFT JOIN users u ON al.user_id = u.id
                WHERE {' AND '.join(where_conditions)}
                ORDER BY al.created_at DESC
                LIMIT 1000
            """
            
            cursor.execute(query, params)
            return cursor.fetchall()
```

---

## 🚀 Plan d'implémentation

### **Phase 1 (Semaines 1-2) : Base authentification**
1. ✅ Schéma BDD complet avec users, organizations, roles
2. ✅ AuthenticationManager avec JWT et 2FA
3. ✅ API endpoints login/logout/register
4. ✅ Interface GUI basique connexion

### **Phase 2 (Semaines 3-4) : Multi-tenant**
1. 🔄 OrganizationManager complet
2. 🔄 Système invitations email
3. 🔄 Permissions granulaires par rôle
4. 🔄 Interface gestion utilisateurs

### **Phase 3 (Mois 2) : Enterprise avancé**
1. 🔄 Dashboard analytics multi-org
2. 🔄 Audit trail complet
3. 🔄 API REST sécurisée
4. 🔄 Facturation dynamique ENTERPRISE

---

**Cette architecture d'authentification supportera parfaitement vos 170k€ ARR avec une sécurité enterprise-grade et une UX optimale !** 🔐