Skip to content

Commit 6f59b7c

Browse files
committed
feat: Introduzione token di sessione per prevenire accessi multipli da parte dello stesso utente
1 parent b883ee8 commit 6f59b7c

2 files changed

Lines changed: 90 additions & 17 deletions

File tree

src/Auth.php

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,22 @@ public function attempt($username, $password, $force = false)
148148
$log['id_utente'] = $this->user->id;
149149
$status = 'success';
150150

151+
// Genera e salva il token di sessione
152+
$this->generateSessionToken($this->user->id);
153+
151154
// Salvataggio nella sessione
152155
$this->saveToSession();
153156
} elseif (
154-
$this->isAuthenticated()
155-
&& $this->password_check($password, $user['password'], $user['id'])
157+
$this->password_check($password, $user['password'], $user['id'])
156158
&& !empty($module)
157159
) {
158160
// Accesso completato
159161
$log['id_utente'] = $this->user->id;
160162
$status = 'success';
161163

164+
// Genera e salva il token di sessione
165+
$this->generateSessionToken($this->user->id);
166+
162167
// Salvataggio nella sessione
163168
$this->saveToSession();
164169
} else {
@@ -194,6 +199,10 @@ public function isAuthenticated()
194199
{
195200
// Controllo autenticazione normale
196201
if (!empty($this->user)) {
202+
// Verifica il token di sessione
203+
if (!$this->checkSessionToken()) {
204+
return false;
205+
}
197206
return true;
198207
}
199208

@@ -296,11 +305,11 @@ public function getFirstModule($first = null)
296305
if (!$this->isAdmin()) {
297306
$group = $this->getUser()['gruppo'];
298307

299-
$query .= ' AND `id` IN (SELECT `idmodule` FROM `zz_permissions` WHERE `idgruppo` = '.Group::where('nome', $group)->first()->id." AND `permessi` IN ('r', 'rw'))";
308+
$query .= ' AND `id` IN (SELECT `idmodule` FROM `zz_permissions` WHERE `idgruppo` = ' . Group::where('nome', $group)->first()->id . " AND `permessi` IN ('r', 'rw'))";
300309
}
301310

302311
$database = database();
303-
$results = $database->fetchArray($query." AND `options` != '' AND `options` != 'menu' AND `options` IS NOT NULL ORDER BY `order` ASC", $parameters);
312+
$results = $database->fetchArray($query . " AND `options` != '' AND `options` != 'menu' AND `options` IS NOT NULL ORDER BY `order` ASC", $parameters);
304313

305314
if (!empty($results)) {
306315
$module = null;
@@ -430,7 +439,7 @@ public static function getBruteTimeout()
430439

431440
$database = database();
432441

433-
$results = $database->fetchArray('SELECT TIME_TO_SEC(TIMEDIFF(DATE_ADD(created_at, INTERVAL '.self::$brute_options['timeout'].' SECOND), NOW())) AS diff FROM zz_logs WHERE ip = :ip AND stato = :state AND DATE_ADD(created_at, INTERVAL :timeout SECOND) >= NOW() ORDER BY created_at DESC LIMIT 1', [
442+
$results = $database->fetchArray('SELECT TIME_TO_SEC(TIMEDIFF(DATE_ADD(created_at, INTERVAL ' . self::$brute_options['timeout'] . ' SECOND), NOW())) AS diff FROM zz_logs WHERE ip = :ip AND stato = :state AND DATE_ADD(created_at, INTERVAL :timeout SECOND) >= NOW() ORDER BY created_at DESC LIMIT 1', [
434443
':ip' => get_client_ip(),
435444
':state' => self::getStatus()['failed']['code'],
436445
':timeout' => self::$brute_options['timeout'],
@@ -545,7 +554,7 @@ public function attemptOTPLogin($token, $otp_code)
545554
}
546555

547556
// Verifica token e OTP nel database
548-
$token_record = $database->fetchOne('SELECT * FROM `zz_otp_tokens` WHERE `token` = '.prepare($token).' AND `enabled` = 1');
557+
$token_record = $database->fetchOne('SELECT * FROM `zz_otp_tokens` WHERE `token` = ' . prepare($token) . ' AND `enabled` = 1');
549558

550559
if (empty($token_record)) {
551560
return [
@@ -647,13 +656,13 @@ public function attemptOTPLogin($token, $otp_code)
647656
}
648657

649658
// Pulisci l'OTP utilizzato
650-
$database->query('UPDATE `zz_otp_tokens` SET `last_otp` = "" WHERE `id` = '.prepare($token_record['id']));
659+
$database->query('UPDATE `zz_otp_tokens` SET `last_otp` = "" WHERE `id` = ' . prepare($token_record['id']));
651660

652661
// Pulisci le sessioni OTP
653-
unset($_SESSION['otp_last_sent_'.$token_record['id']]);
662+
unset($_SESSION['otp_last_sent_' . $token_record['id']]);
654663

655664
// Log del login
656-
$username = $utente ? $utente->username : 'token_'.$token_record['id'];
665+
$username = $utente ? $utente->username : 'token_' . $token_record['id'];
657666
$user_id = $utente ? $utente->id : null;
658667

659668
$database->insert('zz_logs', [
@@ -688,7 +697,7 @@ public function attemptTokenLogin($token)
688697
$database = database();
689698

690699
// Verifica token nel database
691-
$token_record = $database->fetchOne('SELECT * FROM `zz_otp_tokens` WHERE `token` = '.prepare($token).' AND `enabled` = 1');
700+
$token_record = $database->fetchOne('SELECT * FROM `zz_otp_tokens` WHERE `token` = ' . prepare($token) . ' AND `enabled` = 1');
692701

693702
if (empty($token_record)) {
694703
return [
@@ -783,7 +792,7 @@ public function attemptTokenLogin($token)
783792
}
784793

785794
// Log del login
786-
$username = $utente ? $utente->username : 'token_'.$token_record['id'];
795+
$username = $utente ? $utente->username : 'token_' . $token_record['id'];
787796
$user_id = $utente ? $utente->id : null;
788797

789798
$database->insert('zz_logs', [
@@ -937,14 +946,19 @@ protected function password_check($password, $hash, $user_id)
937946
*/
938947
protected function saveToSession()
939948
{
940-
if (session_status() == PHP_SESSION_ACTIVE && $this->isAuthenticated()) {
949+
if (session_status() == PHP_SESSION_ACTIVE && !empty($this->user)) {
941950
// Retrocompatibilità
942951
foreach ($this->user as $key => $value) {
943952
$_SESSION[$key] = $value;
944953
}
945954
$_SESSION['id_utente'] = $this->user->id;
946955

947-
$identifier = md5($_SESSION['id_utente'].$_SERVER['HTTP_USER_AGENT']);
956+
// Salva il token di autenticazione nella sessione
957+
if (!empty($this->user->session_token) && empty($_SESSION['auth_token'])) {
958+
$_SESSION['auth_token'] = $this->user->session_token;
959+
}
960+
961+
$identifier = md5($_SESSION['id_utente'] . $_SERVER['HTTP_USER_AGENT']);
948962
if ((empty($_SESSION['last_active']) || time() < $_SESSION['last_active'] + (60 * 60)) && (empty($_SESSION['identifier']) || $_SESSION['identifier'] == $identifier)) {
949963
$_SESSION['last_active'] = time();
950964
$_SESSION['identifier'] = $identifier;
@@ -962,7 +976,7 @@ protected function identifyUser($user_id)
962976
$database = database();
963977

964978
try {
965-
$results = $database->fetchArray('SELECT `id`, `idanagrafica`, `username`, (SELECT `title` FROM `zz_groups` LEFT JOIN `zz_groups_lang` ON `zz_groups`.`id`=`zz_groups_lang`.`id_record` AND `zz_groups_lang`.`id_lang`='.prepare(Models\Locale::getDefault()->id).' WHERE `zz_groups`.`id` = `zz_users`.`idgruppo`) AS gruppo FROM `zz_users` WHERE `id` = :user_id AND `enabled` = 1 LIMIT 1', [
979+
$results = $database->fetchArray('SELECT `id`, `idanagrafica`, `username`, `session_token`, (SELECT `title` FROM `zz_groups` LEFT JOIN `zz_groups_lang` ON `zz_groups`.`id`=`zz_groups_lang`.`id_record` AND `zz_groups_lang`.`id_lang`=' . prepare(Models\Locale::getDefault()->id) . ' WHERE `zz_groups`.`id` = `zz_users`.`idgruppo`) AS gruppo FROM `zz_users` WHERE `id` = :user_id AND `enabled` = 1 LIMIT 1', [
966980
':user_id' => $user_id,
967981
]);
968982

@@ -990,7 +1004,7 @@ protected function identifyByToken($token_record)
9901004
// Crea un utente virtuale per la sessione
9911005
$this->user = (object) [
9921006
'id' => 0,
993-
'username' => 'token_'.$token_record['id'],
1007+
'username' => 'token_' . $token_record['id'],
9941008
'nome' => 'Token Access',
9951009
'cognome' => '',
9961010
'email' => '',
@@ -1029,7 +1043,7 @@ protected function isTokenStillValid()
10291043
$database = database();
10301044

10311045
// Recupera il token dal database per verificare lo stato attuale
1032-
$token_record = $database->fetchOne('SELECT * FROM `zz_otp_tokens` WHERE `id` = '.prepare($this->token_user['token_id']).' AND `enabled` = 1');
1046+
$token_record = $database->fetchOne('SELECT * FROM `zz_otp_tokens` WHERE `id` = ' . prepare($this->token_user['token_id']) . ' AND `enabled` = 1');
10331047

10341048
if (empty($token_record)) {
10351049
// Token non trovato o disabilitato
@@ -1138,4 +1152,60 @@ private function isValidInternalUrl($url)
11381152

11391153
return false;
11401154
}
1155+
1156+
/**
1157+
* Genera un token di sessione sicuro e lo salva nel database.
1158+
* Invalida automaticamente le sessioni precedenti.
1159+
*
1160+
* @param int $user_id ID dell'utente
1161+
*/
1162+
protected function generateSessionToken($user_id)
1163+
{
1164+
// Genera un token sicuro di 64 caratteri esadecimali
1165+
$token = bin2hex(random_bytes(32));
1166+
1167+
$database = database();
1168+
1169+
// Salva il token nel database (invalida automaticamente le sessioni precedenti)
1170+
$database->update('zz_users', [
1171+
'session_token' => $token,
1172+
], [
1173+
'id' => $user_id,
1174+
]);
1175+
1176+
// Salva il token nella sessione
1177+
$_SESSION['auth_token'] = $token;
1178+
1179+
// Aggiorna anche l'oggetto user se già caricato
1180+
if (!empty($this->user) && $this->user->id == $user_id) {
1181+
$this->user->session_token = $token;
1182+
}
1183+
}
1184+
1185+
/**
1186+
* Verifica che il token di sessione corrisponda a quello nel database.
1187+
* Permette il login per utenti senza token (periodo di transizione).
1188+
*
1189+
* @return bool True se il token è valido o non presente, false altrimenti
1190+
*/
1191+
protected function checkSessionToken()
1192+
{
1193+
// Se l'utente non è caricato, non possiamo verificare
1194+
if (empty($this->user)) {
1195+
return false;
1196+
}
1197+
1198+
// Periodo di transizione: se l'utente non ha ancora un token nel DB, permetti l'accesso
1199+
if (empty($this->user->session_token)) {
1200+
return true;
1201+
}
1202+
1203+
// Se c'è un token nel DB ma non in sessione, invalida la sessione
1204+
if (empty($_SESSION['auth_token'])) {
1205+
return false;
1206+
}
1207+
1208+
// Confronta i token in modo sicuro contro timing attacks
1209+
return hash_equals($this->user->session_token, $_SESSION['auth_token']);
1210+
}
11411211
}

update/2_10.sql

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,7 @@ WHERE `name` IN ('Anagrafiche', 'Impianti', 'Gestione documentale');
197197

198198
-- Rimozione dei campi obsoleti dalla tabella zz_modules
199199
ALTER TABLE `zz_modules` DROP COLUMN `use_notes`;
200-
ALTER TABLE `zz_modules` DROP COLUMN `use_checklists`;
200+
ALTER TABLE `zz_modules` DROP COLUMN `use_checklists`;
201+
202+
-- Aggiunta colonna session_token per gestione sessione singola
203+
ALTER TABLE `zz_users` ADD `session_token` VARCHAR(64) NULL DEFAULT NULL AFTER `password`;

0 commit comments

Comments
 (0)