/*
Exemple pédagogique : vérification serveur d'un code confidentiel.
Ce fichier accompagne le clavier numérique aléatoire côté navigateur.
Il montre surtout une idée essentielle : la décision doit être prise côté serveur.
Limites volontaires de l'exemple :
- le code attendu est écrit en clair dans ce fichier ;
- les tentatives sont suivies en session, donc de manière simplifiée ;
- aucune base de données, aucun compte utilisateur, aucune vraie authentification forte
ne sont mis en place ici.
Ce fichier sert à comprendre le processus. Il ne doit pas être utilisé tel quel
pour protéger un accès réel.
*/
session_start();
header('Content-Type: application/json; charset=utf-8');
// Code attendu pour la démonstration.
// Dans un vrai projet, il ne serait pas stocké ainsi en clair dans un fichier.
$expectedPin = '1234';
// Paramètres simples de contrôle.
// Ils servent ici à montrer les principes de limitation et de temporisation.
$maxAttempts = 3;
$delayBetweenAttempts = 5; // secondes entre deux tentatives
$lockDuration = 60; // secondes de blocage après trop d'échecs
/*
Initialisation des informations conservées en session.
La session permet de garder un état entre plusieurs requêtes HTTP :
nombre d'essais, date de la dernière tentative, éventuel blocage temporaire.
Dans une application réelle, ces informations seraient souvent reliées à un compte,
une adresse IP, un appareil, ou à d'autres signaux de sécurité.
*/
if (!isset($_SESSION['pin_attempts'])) {
$_SESSION['pin_attempts'] = 0;
}
if (!isset($_SESSION['last_pin_attempt'])) {
$_SESSION['last_pin_attempt'] = 0;
}
if (!isset($_SESSION['pin_locked_until'])) {
$_SESSION['pin_locked_until'] = 0;
}
$now = time();
// Si un blocage est en cours, on refuse immédiatement la tentative.
if ($_SESSION['pin_locked_until'] > $now) {
echo json_encode([
'success' => false,
'status' => 'blocked',
'message' => 'Trop de tentatives. Accès temporairement bloqué.',
'remaining_seconds' => $_SESSION['pin_locked_until'] - $now,
'attempts' => $_SESSION['pin_attempts']
]);
exit;
}
// Si le blocage est terminé, on remet les compteurs à zéro.
if ($_SESSION['pin_locked_until'] > 0 && $_SESSION['pin_locked_until'] <= $now) {
$_SESSION['pin_locked_until'] = 0;
$_SESSION['pin_attempts'] = 0;
$_SESSION['last_pin_attempt'] = 0;
}
/*
Temporisation entre deux tentatives.
Cette étape évite d'enchaîner les essais trop rapidement.
Elle ne suffit pas à elle seule, mais elle montre que la sécurité se joue aussi
dans le rythme imposé aux tentatives.
*/
$elapsed = $now - $_SESSION['last_pin_attempt'];
if ($_SESSION['last_pin_attempt'] > 0 && $elapsed < $delayBetweenAttempts) {
echo json_encode([
'success' => false,
'status' => 'wait',
'message' => 'Veuillez patienter avant une nouvelle tentative.',
'remaining_seconds' => $delayBetweenAttempts - $elapsed,
'attempts' => $_SESSION['pin_attempts']
]);
exit;
}
// Lecture du corps JSON envoyé par fetch() depuis app.js.
$rawInput = file_get_contents('php://input');
$data = json_decode($rawInput, true);
// Vérification du format JSON.
// Le serveur ne doit jamais supposer que le client envoie des données propres.
if (!is_array($data)) {
echo json_encode([
'success' => false,
'status' => 'error',
'message' => 'Données invalides.'
]);
exit;
}
// Vérification de la présence du code.
if (!isset($data['pin'])) {
echo json_encode([
'success' => false,
'status' => 'error',
'message' => 'Aucun code reçu.'
]);
exit;
}
$pin = (string) $data['pin'];
/*
Vérification minimale du format attendu.
Ici, nous acceptons exactement 4 chiffres.
Cette validation est indispensable même si l'interface semble empêcher d'autres valeurs,
car une requête peut être fabriquée sans passer par l'interface.
*/
if (!preg_match('/^\d{4}$/', $pin)) {
echo json_encode([
'success' => false,
'status' => 'error',
'message' => 'Format de code invalide.'
]);
exit;
}
// À partir de ce point, on considère qu'une vraie tentative a été faite.
$_SESSION['last_pin_attempt'] = $now;
/*
Vérification du code.
hash_equals() est utilisé ici pour garder une bonne habitude de comparaison
entre chaînes sensibles. Cela ne transforme pas l'exemple en système complet,
mais cela évite une comparaison trop naïve.
*/
if (hash_equals($expectedPin, $pin)) {
$_SESSION['pin_attempts'] = 0;
$_SESSION['last_pin_attempt'] = 0;
$_SESSION['pin_locked_until'] = 0;
echo json_encode([
'success' => true,
'status' => 'accepted',
// En production, on évite de renvoyer le code reçu dans le message.
'message' => 'Code accepté.'
]);
exit;
}
// Échec : on incrémente le compteur de tentatives.
$_SESSION['pin_attempts']++;
// Si le maximum est atteint, on active un blocage temporaire.
if ($_SESSION['pin_attempts'] >= $maxAttempts) {
$_SESSION['pin_locked_until'] = $now + $lockDuration;
echo json_encode([
'success' => false,
'status' => 'blocked',
'message' => 'Trop de tentatives. Accès temporairement bloqué.',
'remaining_seconds' => $lockDuration,
'attempts' => $_SESSION['pin_attempts']
]);
exit;
}
// Sinon, on renvoie simplement le refus.
echo json_encode([
'success' => false,
'status' => 'refused',
// Message volontairement générique : on ne renvoie pas le code reçu.
'message' => 'Code refusé.',
'attempts' => $_SESSION['pin_attempts'],
'remaining_attempts' => max(0, $maxAttempts - $_SESSION['pin_attempts'])
]);