feat: Implémentation authentification NIST SP 800-63B v3.0.8
- Ajout du service PasswordSecurityService conforme NIST SP 800-63B - Vérification des mots de passe contre la base Have I Been Pwned - Validation : minimum 8 caractères, maximum 64 caractères - Pas d'exigences de composition obligatoires (conforme NIST) - Intégration dans LoginController et UserController - Génération de mots de passe sécurisés non compromis 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
210
api/src/Controllers/PasswordController.php
Normal file
210
api/src/Controllers/PasswordController.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/PasswordSecurityService.php';
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
|
||||
use Request;
|
||||
use Response;
|
||||
use LogService;
|
||||
use App\Services\PasswordSecurityService;
|
||||
|
||||
/**
|
||||
* Contrôleur pour la gestion de la sécurité des mots de passe
|
||||
* Fournit des endpoints pour vérifier la force et la compromission des mots de passe
|
||||
*/
|
||||
class PasswordController {
|
||||
|
||||
/**
|
||||
* Vérifie la force d'un mot de passe et s'il a été compromis
|
||||
* Endpoint utilisable sans authentification pour le formulaire d'inscription
|
||||
*
|
||||
* POST /api/password/check
|
||||
*/
|
||||
public function checkStrength(): void {
|
||||
try {
|
||||
$data = Request::getJson();
|
||||
|
||||
if (!isset($data['password']) || empty($data['password'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Mot de passe requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $data['password'];
|
||||
$checkCompromised = $data['check_compromised'] ?? true;
|
||||
|
||||
// Validation du mot de passe
|
||||
$validation = PasswordSecurityService::validatePassword($password, $checkCompromised);
|
||||
|
||||
// Estimation de la force
|
||||
$strength = PasswordSecurityService::estimatePasswordStrength($password);
|
||||
|
||||
// Vérification spécifique de compromission si demandée
|
||||
$compromisedInfo = null;
|
||||
if ($checkCompromised) {
|
||||
$compromisedCheck = PasswordSecurityService::checkPasswordCompromised($password);
|
||||
if ($compromisedCheck['compromised']) {
|
||||
$compromisedInfo = [
|
||||
'compromised' => true,
|
||||
'occurrences' => $compromisedCheck['occurrences'],
|
||||
'message' => sprintf(
|
||||
'Ce mot de passe a été trouvé %s fois dans des fuites de données',
|
||||
number_format($compromisedCheck['occurrences'], 0, ',', ' ')
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'valid' => $validation['valid'],
|
||||
'errors' => $validation['errors'],
|
||||
'warnings' => $validation['warnings'],
|
||||
'strength' => $strength,
|
||||
'compromised' => $compromisedInfo
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
LogService::log('Erreur lors de la vérification du mot de passe', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la vérification du mot de passe'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un mot de passe sécurisé aléatoire
|
||||
* Endpoint nécessitant une authentification
|
||||
*
|
||||
* GET /api/password/generate
|
||||
*/
|
||||
public function generate(): void {
|
||||
try {
|
||||
// Vérifier l'authentification
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Authentification requise'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les paramètres optionnels
|
||||
$length = isset($_GET['length']) ? intval($_GET['length']) : 14;
|
||||
$length = max(12, min(20, $length)); // Limiter entre 12 et 20
|
||||
|
||||
// Générer un mot de passe non compromis
|
||||
$password = PasswordSecurityService::generateSecurePassword($length);
|
||||
|
||||
if ($password === null) {
|
||||
// En cas d'échec, utiliser la méthode classique
|
||||
$password = $this->generateFallbackPassword($length);
|
||||
}
|
||||
|
||||
// Calculer la force du mot de passe généré
|
||||
$strength = PasswordSecurityService::estimatePasswordStrength($password);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'password' => $password,
|
||||
'length' => strlen($password),
|
||||
'strength' => $strength
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
LogService::log('Erreur lors de la génération du mot de passe', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la génération du mot de passe'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie uniquement si un mot de passe est compromis
|
||||
* Endpoint rapide pour vérification en temps réel
|
||||
*
|
||||
* POST /api/password/compromised
|
||||
*/
|
||||
public function checkCompromised(): void {
|
||||
try {
|
||||
$data = Request::getJson();
|
||||
|
||||
if (!isset($data['password']) || empty($data['password'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Mot de passe requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$password = $data['password'];
|
||||
|
||||
// Vérification de compromission
|
||||
$compromisedCheck = PasswordSecurityService::checkPasswordCompromised($password);
|
||||
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'compromised' => $compromisedCheck['compromised'],
|
||||
'occurrences' => $compromisedCheck['occurrences']
|
||||
];
|
||||
|
||||
if ($compromisedCheck['compromised']) {
|
||||
$response['message'] = sprintf(
|
||||
'Ce mot de passe a été trouvé %s fois dans des fuites de données',
|
||||
number_format($compromisedCheck['occurrences'], 0, ',', ' ')
|
||||
);
|
||||
$response['recommendation'] = 'Il est fortement recommandé de choisir un autre mot de passe';
|
||||
}
|
||||
|
||||
if ($compromisedCheck['error']) {
|
||||
$response['warning'] = 'Impossible de vérifier complètement le mot de passe';
|
||||
}
|
||||
|
||||
Response::json($response);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
LogService::log('Erreur lors de la vérification de compromission', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la vérification'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un mot de passe de secours si le service principal échoue
|
||||
*
|
||||
* @param int $length Longueur du mot de passe
|
||||
* @return string Le mot de passe généré
|
||||
*/
|
||||
private function generateFallbackPassword(int $length): string {
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
$password = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$password .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user