Verificació de correu electrònic

De Wiki personal d'en Guillem Serrat

Verificació del correu electrònic

L’aplicació compta amb un procés de verificació de correu electrònic. Aquest procés consta d’un enviament d’un codi de 8 dígits al correu especificat, i l’usuari ha d’introduir aquest codi dins del formulari de verificació.

És important remarcar que cal tenir el correu electrònic verificat per poder iniciar sessió amb 2FA.

Inicialització de la sessió i connexió a la BD

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, com hem de modificar dades de l'usuari i per tant actualitzar registres de la BD farem servir la connexió d'escriptura.

require 'funcions.php';
require './connexioBD/connexioRW.php';

Requerir autenticació i no tenir el correu verificat

Per accedir a aquesta pàgina, és imperatiu que l'usuari estigui autenticat, en cas contrari no hi pot accedir.

Per verificar que l'usuari està autenticat, farem servir la funció requerir_autenticacio

// Requerir autenticació per accedir a la pàgina
requerir_autenticacio();

A més, l'usuari únicament podrà accedir a la pàgina únicament si no té el correu verificat, en cas que el tingui, el retornarem a la seva pàgina privada.

Per això, mitjançant la ID de la sessió, obtindrem el mail (per enviar el correu) i l'estat de verificació. En cas que l'estat de verificació sigui "si", retornarem l'usuari a la seva pàgina privada, evitant que es torni a enviar un correu de verificació i tornar haver de passar per aquest procés

// ID de l'usuari a partir de la sessió
$usuari_id = $_SESSION['usuari']['id'];

// Obtenir l'email i l'estat de l'email de l'usuari
$stmt = $pdo->prepare("SELECT email, email_verificat FROM usuaris WHERE id = ?");
$stmt->execute([$usuari_id]);
$usuari = $stmt->fetch();

// Si la consulta no retorna res, significa que no existeix cap usuari amb aquella ID
if (!$usuari) {
    header('Location: login.php');
    exit;
}

// Si a la BD està especificat que l'usuari ja ha verificat l'email, no és necessair que ho torni a fer
if ($usuari["email_verificat"] == "si"){
    header('Location: privada.php'); // Redirigim directament a la pàgina privada
    exit;
}

Enviament del codi de verificació

Generació del codi

Generarem un codi de verificació, però únicament si no existeix ja un. Així, si l’usuari refresca la pàgina, no tornarà a enviar el codi de nou, sinó que l’enviat anteriorment encara serà vàlid.

Tot i això, sempre que l’usuari surti d’aquesta pàgina sense haver especificat el codi de verificació, aquest serà esborrat.

Aquest codi es desarà a la sessió de l’usuari, per en cas que refresqui la pàgina i serà un nombre enter de 8 dígits

// Generar y enviar un codi de verificació sempre que no hi hagi un a la sessió
if (!isset($_SESSION['codi_verificacio'])) {
    // Generem un codi de 8 xifres aleatori
    $_SESSION['codi_verificacio'] = random_int(10000000, 99999999); // Desem el codi a la sessió

Enviament del correu

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
// Enviament del correu
$to = $usuari['email'];  // Destinatari: l'email de l'usuari registrat a la BD
$subject = "Codi de verificació de correu electrònic"; // Assumpte del correu

// Cos del missatge
$message = "Hola {$usuari['nom_complet']},\n\nEl teu codi és: " . $_SESSION['codi_verificacio'] .  "\n\nSalutacions, l'equip de gserrat.cat";

// Capçaleres genèriques per evitar ser categoritzat com a spam o correu sospitós
$headers = [
    "From: no-reply@gserrat.cat",
    "Reply-To: 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.

En cas que hi hagi un error, desarem un missatge a la variable $errorMail

// Enviem l'email fent servir la funció mail() configurada a PHP.ini i amb els paràmetres definits anteriorment
if (mail($to, $subject, $message, implode("\r\n", $headers))) {
} else {
    // En cas que hi hagi cap problema al enviar el correu, l'usuari veurà un error
    $errorMail = "Error al enviar el correu.";
    // Com a administrador d'aplicacions web, hauriem de consultar /var/log/msmtp.log (definit a la configuració del VPS)
}

É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”

El correu dins de la safata d'entrada de qualsevol usuari, es visualitzaria de la següent manera

Formulari de verificació del correu electrònic

Un cop s'ha enviat el codi de verificació al correu electrònic de l'usuari, es mostrarà un formulari per introduir el codi de verificació. És important remarcar que si es produeix qualsevol error en l'enviament, no es mostrarà el formulari i únicament es visualitzarà l'error

<?php if (empty($errorMail)): // Sempre que no hi hagi cap error a l'hora d'enviar el mail, mostrarem el formulari per introduir el codi?>

[Contingut del formulari]

<?php endif ?>

Aquest formulari informa del correu electrònic al que s'ha enviat el codi i una entrada numèrica per introduir-lo.

El formulari en qüestió es visualitza de la següent manera:

Verificació del codi

Un cop s'introdueixi el codi dins del formulari, en cas que sigui el mateix que s'ha generat, actualitzarem la BD i definirem el camp de verificació d'email a "si", per acabar redirigint l'usuari a la seva pàgina privada. En cas que no sigui correcte, definirem un error per mostrar-lo a l'usuari.

En cas que hagi sigut un error, com hem desat el codi a la sessió i només es genera un de nou quan no existeix cap, tot i tenir un error i refrescar la pàgina, l’usuari podrà introduir el mateix codi generat al principi i no es generarà un codi nou cada cop que s’equivoqui.

// Processar formulari del codi de verificació
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['codi_verificacio'])) { // Quan rebem el codi de verificació
    $codi = trim($_POST['codi_verificacio']); // El desem a una variable

    if ($codi == $_SESSION['codi_verificacio']) { // I comprovem que el codi enviat pel formulari sigui el mateix que l'emmagatzemat en sessió
        // Codi correcte, actualitzar BD
        $stmt = $pdo->prepare("UPDATE usuaris SET email_verificat = 'si' WHERE id = ?");
        $stmt->execute([$usuari_id]);

        unset($_SESSION['codi_verificacio']); // Eliminem el codi de la sessió
        header('Location: privada.php'); // Redirigim a la pàgina privada
        exit;
    } else {
        $errorCodi = "El codi introduït és incorrecte.";
    }
}

Mostra de missatges d'error o èxit

En cas que l'usuari completi el procés de verificació correctament, no es mostrarà cap missatge, sinó que directament es redirigirà a la seva pàgina privada.

En canvi, si hi ha qualsevol error, en la part superior del formulari s'imprimirà el missatge informat el codi és incorrecte

<?php if ($errorMail): // En cas que hi hagi un error es mostrarà?>
    <div class="alert alert-error"><?= htmlspecialchars($errorMail) ?></div>
<?php endif; ?>

<?php if ($errorCodi): // En cas que hi hagi un error es mostrarà?>
    <div class="alert alert-error"><?= htmlspecialchars($errorCodi) ?></div>
<?php endif; ?>

Codi complet

<?php
session_start();
require 'funcions.php';
require './connexioBD/connexioRW.php';

// Per accedir a la pàgina requerim que l'usuari estigui autenticat
requerir_autenticacio();

// Inicialitzem les variables dels missatges
$errorMail = '';
$errorCodi = '';

// ID de l'usuari a partir de la sessió
$usuari_id = $_SESSION['usuari']['id'];

// Obtenir l'email i l'estat de l'email de l'usuari
$stmt = $pdo->prepare("SELECT nom_complet email, email_verificat FROM usuaris WHERE id = ?");
$stmt->execute([$usuari_id]);
$usuari = $stmt->fetch();

// Si la consulta no retorna res, significa que no existeix cap usuari amb aquella ID
if (!$usuari) {
    header('Location: login.php');
    exit;
}

// Si a la BD està especificat que l'usuari ja ha verificat l'email, no és necessair que ho torni a fer
if ($usuari["email_verificat"] == "si"){
    header('Location: privada.php'); // Redirigim directament a la pàgina privada
    exit;
}

// Generar y enviar un codi de verificació sempre que no hi hagi un a la sessió
if (!isset($_SESSION['codi_verificacio'])) {
    // Generem un codi de 8 xifres aleatori
    $_SESSION['codi_verificacio'] = random_int(10000000, 99999999); // Desem el codi a la sessió

    // Enviament del correu
    $to = $usuari['email'];  // Destinatari: l'email de l'usuari registrat a la BD
    $subject = "Codi de verificació de correu electrònic"; // Assumpte del correu
    
    // Cos del missatge
    $message = "Hola {$usuari['nom_complet']},\n\nEl teu codi és: " . $_SESSION['codi_verificacio'] .  "\n\nSalutacions, l'equip de gserrat.cat";

    // Capçaleres genèriques per evitar ser categoritzat com a spam o correu sospitós
    $headers = [
        "From: no-reply@gserrat.cat",
        "Reply-To: 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 i amb els paràmentres definits anteriorment
    if (mail($to, $subject, $message, implode("\r\n", $headers))) {
    } else {
        // En cas que hi hagi cap problema al enviar el correu, l'usuari veurà un error
        $errorMail = "Error al enviar el correu.";
        // Com a administrador d'aplicacions web, hauriem de consultar /var/log/msmtp.log (definit a la configuració del VPS)
    }
}

// Processar formulari del codi de verificació
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['codi_verificacio'])) { // Quan rebem el codi de verificació
    $codi = trim($_POST['codi_verificacio']); // El desem a una variable

    if ($codi == $_SESSION['codi_verificacio']) { // I comprovem que el codi enviat pel formulari sigui el mateix que l'emmagatzemat en sessió
        // Codi correcte, actualitzar BD
        $stmt = $pdo->prepare("UPDATE usuaris SET email_verificat = 'si' WHERE id = ?");
        $stmt->execute([$usuari_id]);

        unset($_SESSION['codi_verificacio']); // Eliminem el codi de la sessió
        header('Location: privada.php'); // Redirigim a la pàgina privada
        exit;
    } else {
        $errorCodi = "El codi introduït és incorrecte.";
    }
}

?>

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <title>Verificació de correu</title>
    <link rel="stylesheet" href="./css/verificaCorreu.css"> 
</head>
<body>
    <div class="verify-container">
        <header class="verify-header">
            <h2>Verifica el teu correu</h2>
        </header>

        <?php if ($errorMail): // En cas que hi hagi un error es mostrarà?>
            <div class="alert alert-error"><?= htmlspecialchars($errorMail) ?></div>
        <?php endif; ?>

        <?php if ($errorCodi): // En cas que hi hagi un error es mostrarà?>
            <div class="alert alert-error"><?= htmlspecialchars($errorCodi) ?></div>
        <?php endif; ?>

        <?php if (empty($errorMail)): // Sempre que no hi hagi cap error a l'hora d'enviar el mail, mostrarem el formulari per introduir el codi?>

        <div class="verify-card">
            <div class="info-box">
                <p>Hem enviat un codi de verificació a:</p>
                <strong><?php echo htmlspecialchars($usuari['email']) ?></strong>
                <p class="spam-warn">Recorda revisar la carpeta d'spam si no el trobes.</p>
            </div>

            <form method="post">
                <div class="input-group">
                    <label for="codi_verificacio">Codi de verificació</label>
                    <input type="text" name="codi_verificacio" id="codi_verificacio" 
                           placeholder="00000000" required autofocus>
                </div>
                
                <button type="submit" class="btn-verify">Verificar correu</button>
            </form>

            <div class="verify-footer">
                <a href="privada.php">← Tornar a la zona privada</a>
            </div>
        </div>
        <?php endif; ?>
    </div>
</body>
</html>