Inici de sessió amb 2FA: diferència entre les revisions
Cap resum de modificació |
|||
| Línia 39: | Línia 39: | ||
Un cop es respongui el formulari, obtindrem les dades del formulari i realitzarem una consulta per comprovar si el nom d'usuari realment existeix | Un cop es respongui el formulari, obtindrem les dades del formulari i realitzarem una consulta per comprovar si el nom d'usuari realment existeix | ||
// Recuperem les dades del formulari | // Recuperem les dades del formulari | ||
$nom_usuari = trim($_POST['nom_usuari']); | |||
$contrasenya = $_POST['contrasenya']; | |||
$recordar = isset($_POST['recordar']); // Opció per crear o no galetes | |||
// Recuperem les dades de l'usuari amb el nom d'usuari especificat | |||
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?"); | |||
$stmt->execute([$nom_usuari]); | |||
$usuari = $stmt->fetch(); | |||
// Verifiquem que l'usuari existeixi | |||
if ($usuari) { | |||
=== Comprovació d'usuari bloquejat === | === Comprovació d'usuari bloquejat === | ||
| Línia 59: | Línia 59: | ||
Si es cumpleixen els 2 requisits, el compte estarà bloquejat. | Si es cumpleixen els 2 requisits, el compte estarà bloquejat. | ||
// Comprovar si hi ha bloqueig | // Comprovar si hi ha bloqueig | ||
if ($usuari['intents_login'] >= $max_intents | |||
&& strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) { | |||
Per fer el càlcul de les dates fem servir la funció interna de PHP strtotime(), la qual retorna la data introduida en temps UNIX (nº de segons des de 1/1/1970). | Per fer el càlcul de les dates fem servir la funció interna de PHP strtotime(), la qual retorna la data introduida en temps UNIX (nº de segons des de 1/1/1970). | ||
| Línia 86: | Línia 86: | ||
// Calcular temps restant | // Calcular temps restant | ||
$ultim_intent_ts = strtotime($usuari['ultim_intent']); //Calculem el temps (l’hora) UNIX de l’últim intent | |||
$final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60); //Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig | |||
$segons_restants = $final_bloqueig_ts - time(); // Calculem els segons restants. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual | |||
$minuts_restants = floor($segons_restants / 60); // Convertim els segons restants a minuts | |||
$segons_restants = $segons_restants % 60; // Recuperem els segons restants després de calcular les hores | |||
$error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts_restants} minuts i {$segons_restants} segons."; | |||
Per tant el fluxe del codi seria el següent: | Per tant el fluxe del codi seria el següent: | ||
| Línia 105: | Línia 105: | ||
=== Comprovació de contrasenya === | === Comprovació de contrasenya === | ||
Un cop verificat que l'usuari no està bloquejat, comprovarem que la contrasenya sigui correcta amb la funció [[Funcions de la pràctica 5.3#verificar contrasenya|verificar_contrasenya]]. | Un cop verificat que l'usuari no està bloquejat, comprovarem que la contrasenya sigui correcta amb la funció [[Funcions de la pràctica 5.3#verificar contrasenya|verificar_contrasenya]]. | ||
if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) { | |||
En cas que la contrasenya sigui errònia, augmentarem en 1 els número d'intents d'autenticació, registrarem una acció a la taula activitat de nom "error-login" i registrarem un missatge d'error<pre> | En cas que la contrasenya sigui errònia, augmentarem en 1 els número d'intents d'autenticació, registrarem una acció a la taula activitat de nom "error-login" i registrarem un missatge d'error<pre> | ||
// Contrasenya incorrecta: incrementar intents | // Contrasenya incorrecta: incrementar intents | ||
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?"); | |||
$stmt->execute([$usuari['id']]); | |||
registrar_activitat($pdo, $usuari['id'], 'error-login'); | |||
$error = "Usuari o contrasenya incorrectes."; | |||
</pre> | </pre> | ||
| Línia 119: | Línia 119: | ||
Un cop la contrasenya ha estat verificada, comprovarem que l'usuari tingui el correu electrònic verificat. Si no és el cas, informarem que no pot iniciar sessió.<pre> | Un cop la contrasenya ha estat verificada, comprovarem que l'usuari tingui el correu electrònic verificat. Si no és el cas, informarem que no pot iniciar sessió.<pre> | ||
// Verificació d'email verificat | // Verificació d'email verificat | ||
if ($usuari['email_verificat'] !== 'si') { | |||
$error = "No pots usar 2FA perquè el teu correu no està verificat. Inicia sessió de forma normal i verifica'l."; | |||
</pre>Si en canvi té el correu verificat, reiniciarem els intents d'autenticació<pre> | </pre>Si en canvi té el correu verificat, reiniciarem els intents d'autenticació<pre> | ||
// Èxit Fase 1: Reset intents i generar codi | // Èxit Fase 1: Reset intents i generar codi | ||
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?"); | |||
$stmt->execute([$usuari['id']]); | |||
</pre> | </pre> | ||
| Línia 139: | Línia 139: | ||
<pre> | <pre> | ||
$_SESSION['mfa_temp_user'] = $usuari; // Ja que refresquem la pàgina, hem de recordar l'usuari a la sessió | $_SESSION['mfa_temp_user'] = $usuari; // Ja que refresquem la pàgina, hem de recordar l'usuari a la sessió | ||
$_SESSION['mfa_codi'] = $codi; // Ja que refresquem la pàgina, hem de recordar el codi enviat | |||
$_SESSION['mfa_recordar'] = $recordar; // Ja que refresquem la pàgina, hem de recordar si l'ususari es vol recordar a l'aplicació | |||
</pre>Seguidament, prepararem les capçaleres del correu: | </pre>Seguidament, prepararem les capçaleres del correu: | ||
| Línia 149: | Línia 149: | ||
<pre> | <pre> | ||
// Enviar mail | // Enviar mail | ||
$to = $usuari['email']; | |||
$subject = "Codi de seguretat 2FA"; | |||
$message = "Hola {$usuari['nom_complet']},\n\nEl teu codi és: $codi\n\nSalutacions, l'equip de gserrat.cat"; | |||
$headers = [ | |||
"From: no-reply@gserrat.cat", | |||
"MIME-Version: 1.0", | |||
"Content-Type: text/plain; charset=utf-8", | |||
"Date: " . date("r"), | |||
"X-Mailer: PHP/" . phpversion() | |||
]; | |||
</pre>A continuació, enviarem el correu amb la funció interna de PHP mail(). Aquesta funció espera rebre: | </pre>A continuació, enviarem el correu amb la funció interna de PHP mail(). Aquesta funció espera rebre: | ||
| Línia 168: | Línia 168: | ||
Per tant indicarem les variables anteriorment definides. En cas de les altres capçaleres, degut a que és una array amb diferents vectors, farem servir la funció interna de PHP implode(), la qual ajunta els elements d’una array com cadenes de text amb un separador indicat. | Per tant indicarem les variables anteriorment definides. En cas de les altres capçaleres, degut a que és una array amb diferents vectors, farem servir la funció interna de PHP implode(), la qual ajunta els elements d’una array com cadenes de text amb un separador indicat. | ||
// Enviem l'email fent servir la funció mail() configurada a PHP.ini | // Enviem l'email fent servir la funció mail() configurada a PHP.ini | ||
if (mail($to, $subject, $message, implode("\r\n", $headers))) { | |||
'''$pas = 2;''' | |||
} else { | |||
$error = "Error enviant el correu."; | |||
} | |||
És molt important configurar PHP.ini per indicar a la funció mail quin servidor de correu electrònic local fer servir per l’enviament (Postfix, msmtp, etc). Per més informació sobre com configurar un servidor per l’enviament de correus electrònics es pot consultar l’apartat “Configuració del VPS per l'enviament de correus electrònics amb PHP” | És molt important configurar PHP.ini per indicar a la funció mail quin servidor de correu electrònic local fer servir per l’enviament (Postfix, msmtp, etc). Per més informació sobre com configurar un servidor per l’enviament de correus electrònics es pot consultar l’apartat “Configuració del VPS per l'enviament de correus electrònics amb PHP” | ||
Revisió del 02:14, 12 gen 2026
Inici de sessió amb 2FA
L’usuari té la possibilitat de iniciar sessió mitjançant 2FA. El segon factor d’autenticació és el correu electrònic que ha registrat. El procés consisteix en enviar un codi de 8 dígits al correu indicat al registre i l’usuari ha d’entrar aquest codi en l’inici de sessió.
És molt important tenir en compte que per poder autenticar-se amb 2FA, cal haver verificat el correu electrònic abans.
Connexió a la BD i inici de sessió (PHP)
Sempre que es treballa amb sessions, el primer que hem de fer és iniciar-la abans d'escriure el codi HTML
session_start();
A més, hem de requerir el fitxer amb les funcions i una connexió a la BD. En aquest cas, si realitzem una autenticació errònia, haurem d'incrementar el número d'intents de login de la BD i per tant requerim la connexió d'escriptura.
require 'funcions.php'; require './connexioBD/connexioRW.php';
Comprovar si està autenticat
En cas que l'usuari està autenticat, el redirigirem directament a la seva pàgina privada. Per comprovar si l'usuari està autenticat farem servir la funció esta_autenticat.
// Redirigir si ja hi ha una sessió activa
if (esta_autenticat()) {
header('Location: privada.php');
exit;
}
Formulari d'inici de sessió
Aquest codi es divideix en 2 passos. El primer és mostrar el mateix formulari d’inici de sessió i el segon és mostrar el formulari per introduir el codi de verificació. Per determinar en quin pas està fem servir la variable “pas”
$pas = 1; // Pas 1: Login, Pas 2: Codi MFA
A l'HTML, a partir d'un condicional es decideix quin formulari mostrar
<?php if ($pas == 1): ?>
Codi amb el formulari d'inici de sessió<?php else: ?>
Codi amb el formulari 2FA
Quan s'entra a la pàgina la variable $pas té el valor 1 i per tant es mostra el formulari d'inici de sessió.
Al formulari es mostra dues entrades de text per introduir el nom d'usuari i la contrasenya. També es mostra una casella de selecció perque l'aplicatiu el "recordi"
El formulari es visualitzaria de la següent forma:

Comprovació de credencials i estat de bloqueig
Obtenció de les dades d'usuari del formulari i la BD
Un cop es respongui el formulari, obtindrem les dades del formulari i realitzarem una consulta per comprovar si el nom d'usuari realment existeix
// Recuperem les dades del formulari
$nom_usuari = trim($_POST['nom_usuari']);
$contrasenya = $_POST['contrasenya'];
$recordar = isset($_POST['recordar']); // Opció per crear o no galetes
// Recuperem les dades de l'usuari amb el nom d'usuari especificat
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?");
$stmt->execute([$nom_usuari]);
$usuari = $stmt->fetch();
// Verifiquem que l'usuari existeixi
if ($usuari) {
Comprovació d'usuari bloquejat
En cas que existeixi, primerament comprovarem si el compte està bloquejat. Per això comprovarem:
- Que el nombre d’intents d’inici de sessió que s’ha enregistrat a la BD sigui major o igual a $max_intents, en aquest cas 3 intents
- Si l’últim intent d’inici de sessió s’ha produit fa menys de $bloqueig_minuts, en aquest cas 2 minuts
Si es cumpleixen els 2 requisits, el compte estarà bloquejat.
// Comprovar si hi ha bloqueig
if ($usuari['intents_login'] >= $max_intents
&& strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) {
Per fer el càlcul de les dates fem servir la funció interna de PHP strtotime(), la qual retorna la data introduida en temps UNIX (nº de segons des de 1/1/1970).
strtotime($usuari['ultim_intent']) retorna el número de segons que han passat del 1/1/1970 a la data especificada (o dit d'una altra manera, quans segons han passat des de l’1/1/1970 fins el 10/1/2026 a les 15:00)
strtotime("-$bloqueig_minuts minutes") retorna el número de segons que han passat des de l’1/1/1970 fa $bloqueig_minuts minuts (o dit d'una altra manera, quans segons han passat des de l’1/1/1970 fa 2 minuts)
Així, tenim la mateixa “base temporal” amb la que fer càlculs
En cas que el compte estigui bloquejat, mostrarem a l’usuari el temps que queda per desbloquejar-lo.
Per això:
- Calculem el temps (l’hora) UNIX de l’últim intent
- Exemple (Exemple: 15:00 emmagatzemat en format UNIX)
- Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
- Agafem el temps de l’últim intent i li sumem X minuts en segons, en aquest cas 2 minuts per 60 segons
- Exemple: (Exemple: 15:02, emmagatzemat en format UNIX)
- Calculem els segons restants
- Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual
- Exemple: 15:02 - 15:01 = 00:01 (60 segons)
- Convertim els segons restants a minuts i segons
- 80 segons = 1 minut i 20 segons
- Mostrem els minuts i segons restants
// Calcular temps restant
$ultim_intent_ts = strtotime($usuari['ultim_intent']); //Calculem el temps (l’hora) UNIX de l’últim intent
$final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60); //Calculem el temps (l’hora) UNIX per que s’acabi el bloqueig
$segons_restants = $final_bloqueig_ts - time(); // Calculem els segons restants. Agafem el temps (hora) perquè s’acabi el bloqueig i restem a l’hora actual
$minuts_restants = floor($segons_restants / 60); // Convertim els segons restants a minuts
$segons_restants = $segons_restants % 60; // Recuperem els segons restants després de calcular les hores
$error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts_restants} minuts i {$segons_restants} segons.";
Per tant el fluxe del codi seria el següent:
- L’usuari s’equivoca 3 vegades
- Intenta accedir una 4ta, no pot (3 intents_login, 3 segons últim intent)
- Espera 2 minuts (3 intents_login, 2 minuts últim intent, pot accedir)
- Es torna a equivocar una 5na vegada (3 intents_login, 3 segons últim intent)
- Torna a esperar 2 minuts
- Accedeix correctament
Comprovació de contrasenya
Un cop verificat que l'usuari no està bloquejat, comprovarem que la contrasenya sigui correcta amb la funció verificar_contrasenya.
if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) {
En cas que la contrasenya sigui errònia, augmentarem en 1 els número d'intents d'autenticació, registrarem una acció a la taula activitat de nom "error-login" i registrarem un missatge d'error
// Contrasenya incorrecta: incrementar intents
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?");
$stmt->execute([$usuari['id']]);
registrar_activitat($pdo, $usuari['id'], 'error-login');
$error = "Usuari o contrasenya incorrectes.";
Enviament del correu electrònic amb el codi de verificació
Comprovar que el correu electrònic estigui verificat
Un cop la contrasenya ha estat verificada, comprovarem que l'usuari tingui el correu electrònic verificat. Si no és el cas, informarem que no pot iniciar sessió.
// Verificació d'email verificat
if ($usuari['email_verificat'] !== 'si') {
$error = "No pots usar 2FA perquè el teu correu no està verificat. Inicia sessió de forma normal i verifica'l.";
Si en canvi té el correu verificat, reiniciarem els intents d'autenticació
// Èxit Fase 1: Reset intents i generar codi $stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?"); $stmt->execute([$usuari['id']]);
Enviament del correu electrònic
Un cop reiniciat els intents d'autenticació, generarem un nombre de 8 dígits que serà el codi de verificació.
$codi = random_int(10000000, 99999999);
Seguidament, ja que al canviar de pas hem de refrescar la pàgina, hem de guardar els valors de l’usuari dins de la sessió per no perdre’ls
En concret, guardarem:
- El nom d’usuari
- El codi de verificació
- Si vol o no que l’aplicació recordi l’usuari (galetes)
$_SESSION['mfa_temp_user'] = $usuari; // Ja que refresquem la pàgina, hem de recordar l'usuari a la sessió $_SESSION['mfa_codi'] = $codi; // Ja que refresquem la pàgina, hem de recordar el codi enviat $_SESSION['mfa_recordar'] = $recordar; // Ja que refresquem la pàgina, hem de recordar si l'ususari es vol recordar a l'aplicació
Seguidament, prepararem les capçaleres del correu:
- Destinatari: l’email de l’usuari
- Assumpte: Codi de verificació
- Missatge: “El codi generat anteriorment”
- Altres capçaleres: per complir amb stàndards de seguretat de Nominalia. Si un correu no té unes bones capçaleres, pot ser interpretat com spam
// Enviar mail
$to = $usuari['email'];
$subject = "Codi de seguretat 2FA";
$message = "Hola {$usuari['nom_complet']},\n\nEl teu codi és: $codi\n\nSalutacions, l'equip de gserrat.cat";
$headers = [
"From: no-reply@gserrat.cat",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8",
"Date: " . date("r"),
"X-Mailer: PHP/" . phpversion()
];
A continuació, enviarem el correu amb la funció interna de PHP mail(). Aquesta funció espera rebre:
- Destinatari
- Assumpte
- Missatge
- Altres capçaleres
Per tant indicarem les variables anteriorment definides. En cas de les altres capçaleres, degut a que és una array amb diferents vectors, farem servir la funció interna de PHP implode(), la qual ajunta els elements d’una array com cadenes de text amb un separador indicat.
// Enviem l'email fent servir la funció mail() configurada a PHP.ini
if (mail($to, $subject, $message, implode("\r\n", $headers))) {
$pas = 2;
} else {
$error = "Error enviant el correu.";
}
És molt important configurar PHP.ini per indicar a la funció mail quin servidor de correu electrònic local fer servir per l’enviament (Postfix, msmtp, etc). Per més informació sobre com configurar un servidor per l’enviament de correus electrònics es pot consultar l’apartat “Configuració del VPS per l'enviament de correus electrònics amb PHP”
Un correu electrònic d'exemple es visualitzaria de la següent manera

Si el mail s’envia correctament sense cap error, passarem al pas 2 i per tant la condició de l’HTML serà diferent i mostrarà un formulari diferent, el d’introduir el codi de verificació
Formulari 2FA
Comprovació del codi 2FA i inici de sessió
Comprovació del codi 2FA
En el moment que enviem el formulari amb un codi de verificació, el recuperarem i tornarem a especificar el pas nº2, ja que si hi ha un error, l’usuari podrà tornar a intentar introduir el codi.
// --- FASE 2: VALIDACIÓ DEL CODI MFA ---
if (isset($_POST['codi_mfa'])) {
$codi_introduit = trim($_POST['codi_mfa']);
$pas = 2; // Mantinguem la vista del codi si hi ha error
El següent pas serà verificar el codi enviat pel formulari i el codi guardat a la sessió
if ($codi_introduit == $_SESSION['mfa_codi']) {
En cas que el codi no sigui correcte, mostrarem un missatge d'error. Com hem guardat el codi a la sessió, l'usuari podrà tornar a intentar a introduir el codi sense haver de sol·licitar-ne un de nou
} else {
$error = "Codi incorrecte."; }
Inici de sessió
En cas que el codi sigui correcte, iniciarem la sessió.
Primerament regenerarem la ID de sessió per evitar atacs de sessió fixada
session_regenerate_id(true);
Seguidament, a l'igual que en l'inici de sessió manual, crearem un vector de nom “usuari” dins de la sessió amb els següents vectors:
- ID de l’usuari dins la BD
- Nom d’usuari
- Nom complet
- Rol, per definir si és administrador o no i quin tipus
- Autenticat, per definir que està autenticat
$_SESSION['usuari'] = [ // Iniciem sessió a l'usuari indicant
'id' => $usuari['id'], // El seu ID a la BD
'nom_usuari' => $usuari['nom_usuari'], // El nom d'usuari
'nom_complet' => $usuari['nom_complet'], // El nom complet
'rol' => $usuari['rol'], // El seu rol (usuari, admin)
'autenticat' => true // Definim que està autenticat
];
En cas que l'usuari hagi vulgut que l'aplicació el recordi, ho podrem saber amb el valor del vector de la sessió "recordar", que desa el valor de la casella de selecció del formulari. En cas positiu, realitzarem el mateix procediment que amb l'inici de sessió normal
// Gestió de cookies "Recordar-me"
if ($_SESSION['mfa_recordar']) {
$token = bin2hex(random_bytes(16));
setcookie('recordar_id', $usuari['id'], time() + 30*24*60*60, '/');
setcookie('recordar_token', $token, time() + 30*24*60*60, '/');
$stmt = $pdo->prepare("UPDATE usuaris SET token_recordar = ? WHERE id = ?");
$stmt->execute([$token, $usuari['id']]);
}
Per últim, netejarem totes les dades temporals del 2FA a la sessió, registrarem l'acció "login-2fa" a la taula activitat i redirigirem l'usuari a la seva pàgina privada
// Neteja de dades temporals MFA
unset($_SESSION['mfa_temp_user'], $_SESSION['mfa_codi'], $_SESSION['mfa_recordar']);registrar_activitat($pdo, $usuari['id'], 'login-2fa'); header('Location: privada.php'); exit;
Mostra de missatges d'error o èxit
En cas que hagi succeït cap error, tant en la verificació de credencials com a la verificació del codi 2FA es mostrarà a la part superior del formulari
Un exemple d'un missatge d'error per introduir unes credencials errònies es visualitzaria de la següent forma

Un exemple d'un missatge d'error per introduir el codi de verificació incorrecte es visualitzaria de la següent forma

Codi complet
<?php
date_default_timezone_set('Europe/Madrid');
session_start();
require 'funcions.php';
require './connexioBD/connexioRW.php';
$error = '';
$pas = 1; // Pas 1: Login, Pas 2: Codi MFA
// Variables de bloqueig (igual que a login.php)
$max_intents = 3;
$bloqueig_minuts = 2;
// Si ja està autenticat, fora
if (esta_autenticat()) {
header('Location: privada.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// --- FASE 1: VALIDACIÓ DE CREDENCIALS ---
if (isset($_POST['nom_usuari']) && isset($_POST['contrasenya']) && !isset($_POST['codi_mfa'])) {
$nom_usuari = trim($_POST['nom_usuari']);
$contrasenya = $_POST['contrasenya'];
$recordar = isset($_POST['recordar']);
$stmt = $pdo->prepare("SELECT * FROM usuaris WHERE nom_usuari = ?");
$stmt->execute([$nom_usuari]);
$usuari = $stmt->fetch();
if ($usuari) {
// 1. Comprovar si l'usuari està bloquejat temporalment
if ($usuari['intents_login'] >= $max_intents
&& strtotime($usuari['ultim_intent']) > strtotime("-$bloqueig_minuts minutes")) {
$ultim_intent_ts = strtotime($usuari['ultim_intent']);
$final_bloqueig_ts = $ultim_intent_ts + ($bloqueig_minuts * 60);
$segons_restants = $final_bloqueig_ts - time();
$minuts = floor($segons_restants / 60);
$segons = $segons_restants % 60;
$error = "Compte bloquejat temporalment. Torna-ho a provar en {$minuts} min i {$segons} seg.";
} else {
// 2. Intentar validar contrasenya
if (verificar_contrasenya($contrasenya, $usuari['contrasenya'])) {
// Verificació d'email verificat
if ($usuari['email_verificat'] !== 'si') {
$error = "No pots usar 2FA perquè el teu correu no està verificat. Inicia sessió de forma normal i verifica'l.";
} else {
// Èxit Fase 1: Reset intents i generar codi
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = 0, ultim_intent = NULL WHERE id = ?");
$stmt->execute([$usuari['id']]);
$codi = random_int(10000000, 99999999);
$_SESSION['mfa_temp_user'] = $usuari; // Ja que refresquem la pàgina, hem de recordar l'usuari a la sessió
$_SESSION['mfa_codi'] = $codi; // Ja que refresquem la pàgina, hem de recordar el codi enviat
$_SESSION['mfa_recordar'] = $recordar; // Ja que refresquem la pàgina, hem de recordar si l'ususari es vol recordar a l'aplicació
// Enviar mail
$to = $usuari['email'];
$subject = "Codi de seguretat 2FA";
$message = "Hola {$usuari['nom_complet']},\n\nEl teu codi és: $codi\n\nSalutacions, l'equip de gserrat.cat";
$headers = [
"From: no-reply@gserrat.cat",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8",
"Date: " . date("r"),
"X-Mailer: PHP/" . phpversion()
];
// Enviem l'email fent servir la funció mail() configurada a PHP.ini
if (mail($to, $subject, $message, implode("\r\n", $headers))) {
$pas = 2;
} else {
$error = "Error enviant el correu.";
}
}
} else {
// Contrasenya incorrecta: incrementar intents
$stmt = $pdo->prepare("UPDATE usuaris SET intents_login = intents_login + 1, ultim_intent = NOW() WHERE id = ?");
$stmt->execute([$usuari['id']]);
registrar_activitat($pdo, $usuari['id'], 'error-login');
$error = "Usuari o contrasenya incorrectes.";
}
}
} else {
$error = "L'usuari no existeix.";
}
}
// --- FASE 2: VALIDACIÓ DEL CODI MFA ---
if (isset($_POST['codi_mfa'])) {
$codi_introduit = trim($_POST['codi_mfa']);
$pas = 2; // Mantinguem la vista del codi si hi ha error
if ($codi_introduit == $_SESSION['mfa_codi']) {
// ÈXIT TOTAL -> Loguegem a l'usuari
$usuari = $_SESSION['mfa_temp_user'];
session_regenerate_id(true);
$_SESSION['usuari'] = [
'id' => $usuari['id'],
'nom_usuari' => $usuari['nom_usuari'],
'nom_complet' => $usuari['nom_complet'],
'rol' => $usuari['rol'],
'autenticat' => true
];
// Gestió de cookies "Recordar-me"
if ($_SESSION['mfa_recordar']) {
$token = bin2hex(random_bytes(16));
setcookie('recordar_id', $usuari['id'], time() + 30*24*60*60, '/');
setcookie('recordar_token', $token, time() + 30*24*60*60, '/');
$stmt = $pdo->prepare("UPDATE usuaris SET token_recordar = ? WHERE id = ?");
$stmt->execute([$token, $usuari['id']]);
}
// Neteja de dades temporals MFA
unset($_SESSION['mfa_temp_user'], $_SESSION['mfa_codi'], $_SESSION['mfa_recordar']);
registrar_activitat($pdo, $usuari['id'], 'login-2fa');
header('Location: privada.php');
exit;
} else {
$error = "Codi incorrecte.";
}
}
}
?>
<html>
<head>
<meta lang="ca">
<meta charset="UTF-8">
<title>Seguretat 2FA</title>
<link rel="stylesheet" href="./css/mfa.css">
</head>
<body>
<div class="login-card">
<h2>Verificació 2FA</h2>
<?php if ($pas == 1): ?>
<p class="info-text">Introdueix les teves credencials per rebre el codi per correu.</p>
<?php if ($error): ?>
<div class="error-container">
<p style='color:red;'><?= $error ?></p>
</div>
<?php endif; ?>
<form method="post">
<div class="input-group">
<label>Nom d'usuari:</label>
<input type="text" name="nom_usuari" required>
</div>
<div class="input-group">
<label>Contrasenya:</label>
<input type="password" name="contrasenya" required>
</div>
<div class="checkbox-group">
<input type="checkbox" name="recordar" id="recordar">
<label for="recordar">Recordar-me</label>
</div>
<input type="submit" value="Enviar codi de seguretat" class="btn-2fa">
</form>
<?php else: ?>
<p class="info-text">T'hem enviat un codi de 8 dígits al teu correu.</p>
<form method="post">
<div class="input-group">
<label>Codi MFA:</label>
<input type="text" name="codi_mfa" placeholder="00000000" required autofocus>
</div>
<input type="submit" value="Verificar i Entrar" class="btn-2fa">
</form>
<?php endif; ?>
<div class="login-footer">
<a href="login.php" class="btn-outline">Tornar al login normal</a>
</div>
</div>
</body>
</html>