Créa-blog

#100JoursPourCoder
Projet Créa-code

Ressources pour développeur web

Jour 18 – Valider un compte utilisateur par email en PHP

Temps de lecture : 14 minutes
Accueil Projets Jour 18 – Valider un compte utilisateur par email en PHP

Aujourd’hui, pour le jour 18 de #100JoursPourCoder, nous allons enrichir notre système d’inscription avec une validation par email. Cela signifie qu’un utilisateur ne pourra pas se connecter tant qu’il n’aura pas cliqué sur un lien reçu dans sa boîte mail. Ce lien contient un token de vérification unique, temporaire (valide 24h), et sécurisé. Ce mécanisme est essentiel pour s’assurer que l’adresse email fournie est bien réelle et appartenant au visiteur.

Nous allons voir ensemble, pas à pas, comment modifier notre contrôleur d’inscription, notre modèle User.php, puis comment envoyer un mail avec PHPMailer (installé au jour 17), générer un token, et créer une route VerifyController capable de gérer cette validation.

Chaque action, chaque ligne de code, sera expliquée clairement pour que tout le monde puisse suivre, même si vous débutez avec PHP, l’architecture MVC ou l’envoi d’emails.

Générer un token de validation unique lors de l’inscription

Dans le code actuel de notre notre site web Créa-code, dans le contrôleur  SigninController, le champ verification_token a pour valeur null lors de la création du compte. Nous allons modifier cela.

Tout d’abord, dans la méthode register() de SigninController, juste après le hachage du mot de passe, ajoutons la génération d’un token de vérification.

Un token, c’est une longue chaîne unique, difficile à deviner, qui va servir d’identifiant temporaire. On peut utiliser la fonction bin2hex(random_bytes(32)) qui nous donne 64 caractères sécurisés.

Voici ce qu’on ajoute :

$verification_token = bin2hex(random_bytes(32));
$token_expiry = date('Y-m-d H:i:s', strtotime('+1 day')); // valide 24h

On a donc notre token et sa date d’expiration. On va maintenant les inclure dans la création de l’utilisateur, dans la méthode create() du fichier app/Models/User.php. Modifions cet appel :

$userModel->create([
    'email' => $email,
    'password' => $hashedPassword,
    'pseudo' => $pseudo,
    ...
    'is_active' => 0,
    'status' => 'actif',
    'verification_token' => $verification_token,
    'reset_token' => null,
    'reset_token_expiry' => $token_expiry,
]);

Ce qu’on fait ici, c’est très simple :

  • On indique que l’utilisateur n’est pas encore actif (is_active = 0)
  • On enregistre le token de validation dans verification_token
  • Et on utilise reset_token_expiry pour y stocker la date de péremption du lien (oui, ce champ est utilisé pour la réinitialisation du mot de passe, mais on l’utilise aussi ici pour éviter d’ajouter une colonne de plus).

Envoyer le mail de validation avec PHPMailer

Maintenant que notre utilisateur est enregistrer dans la base de données avec un token unique, il faut lui envoyer un mail contenant un lien pour vérifier son adresse mail. Pour cela, nous allons utiliser PHPMailer.

Dans votre SigninController, juste après l’appel à $userModel->create(), insérons le code suivant :

require_once '../vendor/autoload.php'; 
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);

Puis, juste après la création de la classe :

try {
    
    $mail->CharSet = 'UTF-8';
    $mail->isSMTP();
    
    $mail->Host = 'smtp.example.com'; // à adapter
    $mail->SMTPAuth = true;
    $mail->Username = 'votre_email@example.com'; // à adapter
    $mail->Password = 'votre_mot_de_passe'; // à adapter
    $mail->SMTPSecure = 'tls';
    $mail->Port = 587;
    $mail->setFrom('monmail@gmail.com', 'Créa-code');

    $mail->addAddress($email, $pseudo);

    $mail->isHTML(true);
    $mail->Subject = 'Validez votre compte sur Créa-code';
    $mail->Body = "
        <h1>Bienvenue sur Créa-code !</h1>
        <p>Merci de vous être inscrit. Pour activer votre compte, veuillez cliquer sur ce lien :</p>
        <p><a href='https://code.crea-troyes.fr/" . BASE_URL . "verify?token=$verification_token'>Activer mon compte</a></p>
        <p>Ce lien est valable 24 heures.</p>
    ";

    $mail->send();
} catch (Exception $e) {
    if (DEVELOPMENT) {
        echo "Erreur lors de l'envoi du mail : {$mail->ErrorInfo}";
        exit;
    }
}

// Création de la session utilisateur
$_SESSION['user'] = [
    'pseudo' => $pseudo,
    'email' => $email,
    'role' => 'user',
    'is_active' => 0
];

Ce code envoie un mail avec un lien de la forme :

https://code.crea-troyes.fr/verify?token=xyz123

L’utilisateur n’a plus qu’à cliquer dessus pour activer son compte.

Créer le contrôleur VerifyController pour activer le compte

Maintenant que nous avons envoyé un lien contenant un token sécurisé, nous devons créer un contrôleur qui va traiter cette URL. Ce contrôleur vérifiera si le token reçu est valide, non expiré, et non déjà utilisé.

Nous allons créer un fichier VerifyController.php dans le dossier controllers.

Voici le contenu de base de ce fichier, que nous allons commenter ligne par ligne.

app/Controllers/VerifyController.php :

<?php

namespace App\Controllers;

use App\Models\User;
use App\Core\Controller;
use App\Core\View;
use App\Core\Database;

class VerifyController extends Controller
{
    public function index()
    {

        if (isset($_SESSION['user']) && $_SESSION['is_active'] == 1) {
            header('Location: '.BASE_URL.'dashboard');
            exit;
        }

        $title = "Vérification de votre email - Créa-code";

        // On vérifie que le token est bien présent dans l'URL
        if (!isset($_GET['token']) || empty($_GET['token'])) {
            $errors =["Lien de validation invalide."];
            View::render('login', compact('title', 'errors'));
            return;
        }

        $token = htmlspecialchars($_GET['token']);

        // On charge le modèle User
        $userModel = new User(Database::getInstance());

        // On récupère l'utilisateur correspondant à ce token
        $user = $userModel->findByTokenVerif($token);

        // Si aucun utilisateur trouvé, ou déjà activé, ou token expiré
        if (!$user || $user['is_active'] == 1 || strtotime($user['reset_token_expiry']) < time()) {
            $errors = ["Ce lien de validation est invalide ou expiré."];
            View::render('login', compact('title', 'errors'));
            return;
        }

        // Si tout est OK, on active le compte
        $userModel->activateAccount($user['id']);

        $errors = ["Votre compte a bien été activé. Vous pouvez vous connecter."];
        View::render('login', compact('title', 'errors'));
        return;

    }
}

Prenons quelques instants pour expliquer tout ces lignes de code.

Étape 1 : Vérification du token dans l’URL

// On vérifie que le token est bien présent dans l'URL
if (!isset($_GET['token']) || empty($_GET['token'])) {
    $errors =["Lien de validation invalide."];
    View::render('login', compact('title', 'errors'));
    return;
}

On s’assure que l’utilisateur est bien arrivé ici avec un paramètre ?token=.... Si ce n’est pas le cas, on le redirige vers la page de connexion avec un message d’erreur.

Étape 2 : Sécurisation du token

$token = htmlspecialchars($_GET['token']);

On sécurise le contenu du token avec htmlspecialchars() pour éviter d’éventuelles injections.

Étape 3 : Chargement du modèle User

$userModel = new User(Database::getInstance());

On inclut notre modèle User.php, puis on crée une instance.

Formation web et informatique - Alban Guillier - Formateur

Des formations informatique pour tous !

Débutant ou curieux ? Apprenez le développement web, le référencement, le webmarketing, la bureautique, à maîtriser vos appareils Apple et bien plus encore…

Formateur indépendant, professionnel du web depuis 2006, je vous accompagne pas à pas et en cours particulier, que vous soyez débutant ou que vous souhaitiez progresser. En visio, à votre rythme, et toujours avec pédagogie.

Découvrez mes formations Qui suis-je ?

Étape 4 : Recherche de l’utilisateur avec ce token

$user = $userModel->findByTokenVerif($token);

On suppose que le modèle User.php contient une méthode findByToken() qui retourne un utilisateur selon son verification_token. On codera cette méthode dans un instant.

Étape 5 : Vérification de la validité du token

// Si aucun utilisateur trouvé, ou déjà activé, ou token expiré
if (!$user || $user['is_active'] == 1 || strtotime($user['reset_token_expiry']) < time()) {
    $errors = ["Ce lien de validation est invalide ou expiré."];
    View::render('login', compact('title', 'errors'));
    return;
}

On vérifie plusieurs choses :

  • Si aucun utilisateur ne correspond au token : alors c’est un faux lien
  • Si le compte est déjà activé (is_active == 1) : alors ce lien est inutile
  • Si la date d’expiration est dépassée : alors ce lien est expiré

Étape 6 : Activation du compte

$userModel->activateAccount($user['id']);

On appelle une méthode activateAccount() que nous allons coder dans User.php. Elle doit mettre is_active à 1, et supprimer le token.

Étape 7 : Message de succès et redirection

$errors = ["Votre compte a bien été activé. Vous pouvez vous connecter."];
View::render('login', compact('title', 'errors'));
return;

Le compte est maintenant actif. L’utilisateur est redirigé vers la page de connexion, avec un message de confirmation.

On utilise la variable $errors alors qu’il ne s’agit pas d’une erreur. Cela peut paraître paradoxal mais de cette façon que nous pouvons transmettre et afficher un message dans la vue login.php :

<h1>Connexion</h1>

<?php if (!empty($errors)) : ?>
    <div style="color:red;">
        <?php foreach ($errors as $error) : ?>
            <p><?= htmlspecialchars($error) ?></p>
        <?php endforeach; ?>
    </div>
<?php endif; ?>

<form id="login-form" action="<?= BASE_URL ?>login/post" method="post">
    <label for="email">Email :</label><br>
    <input type="email" name="email" id="email" required>
    <div id="email-error" class="error-message"></div>

    <label for="password">Mot de passe :</label><br>
    <input type="password" name="password" id="password" required>
    <div id="password-error" class="error-message"></div>

    <button type="submit">Se connecter</button>
</form>

<p>Pas encore inscrit ? <a href="<?= BASE_URL ?>signin">Créer un compte</a></p>

<p><a href="<?= BASE_URL ?>forgot">Mot de passe oublié ?</a></p>

<script src="<?= BASE_URL ?>assets/js/login-form-validation.js" defer></script> 

Méthodes à ajouter dans models/User.php

Nous devons maintenant ajouter deux méthodes à notre modèle utilisateur app/Models/User.php :

  • findByTokenVerif($token) : pour retrouver un utilisateur via son token
  • activateAccount($id) : pour mettre à jour la valeur de is_active, supprimer le token et nettoyer les colonnes

Méthode findByToken($token)

public function findByTokenVerif($token)
{
    $sql = "SELECT * FROM users WHERE verification_token = :token LIMIT 1";
    $stmt = $this->db->prepare($sql);
    $stmt->bindValue(':token', $token, PDO::PARAM_STR);
    $stmt->execute();

    return $stmt->fetch(PDO::FETCH_ASSOC);
}

Ce code prépare une requête SQL pour rechercher un utilisateur dont le verification_token correspond au paramètre. On ne prend qu’un seul résultat.

Méthode activateAccount($id)

public function activateAccount($id)
{
    $sql = "UPDATE users SET is_active = 1, verification_token = NULL, reset_token_expiry = NULL WHERE id = :id";
    $stmt = $this->db->prepare($sql);
    $stmt->bindValue(':id', $id, PDO::PARAM_INT);
    return $stmt->execute();
}

Ici, on met is_active à 1, on supprime le token et sa date d’expiration pour qu’il ne soit plus utilisable.

Définir la route dans le routeur

Pour que l’URL https://code.crea-troyes.fr/verify?token=xyz123 fonctionne, il faut que notre routeur reconnaisse le mot verify.

// Vérification de l'email
$router->get('/verify', 'VerifyController@index');

Cela permet d’exécuter VerifyController@index.

Résultat final

Quand l’utilisateur s’inscrit :

  1. Un token est généré et stocké en base de données
  2. Un mail est envoyé avec un lien contenant ce token
  3. L’utilisateur clique, est redirigé vers le contrôleur VerifyController
  4. Si le token est valide et non expiré, le compte est activé
  5. Sinon, il est redirigé vers la page de login avec un message d’erreur

Mise à jour du menu du haut dans le fichier header.php

Il va maintenant falloir mettre à jour la vue du menu du haut pour qu’elle affiche un menu correspondant à la situation du visiteur. Inutile de le considérer comme connecté si son adresse mail n’est pas validée (si is_active = 0).

Dans le fichier app/Views/partials/header.php :

<header>
    <h1><a href="<?= BASE_URL ?>">Créa-code</a></h1>
    <nav>
        <a href="<?= BASE_URL ?>">Accueil</a>

        <?php if (isset($_SESSION['user']) && $_SESSION['user']['is_active'] == 1): ?>
            <a href="<?= BASE_URL ?>dashboard">Dashboard</a>
            <a href="<?= BASE_URL ?>logout">Déconnexion</a>
        <?php elseif (isset($_SESSION['user']) && $_SESSION['user']['is_active'] == 0): ?>
            <a href="<?= BASE_URL ?>login">Connexion</a>
        <?php else: ?>
            <a href="<?= BASE_URL ?>signin">Inscription</a>
            <a href="<?= BASE_URL ?>login">Connexion</a>
        <?php endif; ?>
    </nav>
</header>

Redirection au niveau des contrôleurs

Pour éviter qu’un utilisateur se connecte à son Dashboard alors que son adresse n’est pas vérifiée, il va nous falloir tester la valeur de is_active.

Dans le fichier app/Controllers/DashboardController.php :

<?php

namespace App\Controllers;

use App\Core\Controller;
use App\Core\View;

class DashboardController extends Controller
{
    public function index()
    {
        if (!isset($_SESSION['user'])) {
            header('Location: '.BASE_URL.'login');
            exit;
        }

        if ($_SESSION['user']['is_active'] == 0) {
            $_SESSION['login_errors'] = ["Votre compte n'est pas encore activé. Veuillez vérifier votre email."];
            header('Location: '.BASE_URL.'login');
            exit;
        }

        $title = "Dashboard - Créa-code";
        $desc = "Dashboard - Créa-code";

        View::render('dashboard/dashboard', compact('title', 'desc'));

    }
}

Idem pour le fichier app/Controller/LoginController.php :

<?php
namespace App\Controllers;

use App\Core\Controller;
use App\Core\View;
use App\Models\User;
use App\Core\Database;


class LoginController extends Controller
{
    public function index(): void
    {

        if (isset($_SESSION['user']) && $_SESSION['user']['is_active'] == 1) {
            header('Location: '.BASE_URL.'dashboard');
            exit;
        }

        $title = "Login - Créa-code";
        $desc = "Login - Créa-code";
        $errors = $_SESSION['login_errors'] ?? [];
        unset($_SESSION['login_errors']);
        
        View::render('login', compact('title', 'desc', 'errors'));

    }

    public function login()
    {

        if (isset($_SESSION['user'])) {
            header('Location: '.BASE_URL.'dashboard');
            exit;
        }
        
        $errors = [];

        // Vérifier si le formulaire est bien soumis
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $email = trim($_POST['email'] ?? '');
            $password = trim($_POST['password'] ?? '');

            // Vérification des champs requis
            if (empty($email) || empty($password)) {
                $errors[] = "Tous les champs sont obligatoires.";
            }

            // Vérification du format de l'email
            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                $errors[] = "L'adresse email est invalide.";
            }

            // Si aucune erreur, on passe à la suite
            if (empty($errors)) {
                $userModel = new User(Database::getInstance());
                $user = $userModel->findByEmail($email);

                if ($user && password_verify($password, $user['password'])) {
                    // Connexion réussie
                    $_SESSION['user'] = [
                        'id' => $user['id'],
                        'email' => $user['email'],
                        'pseudo' => $user['pseudo'],
                        'role' => $user['role'],
                        'is_active' => $user['is_active']
                    ];
                    header('Location: '.BASE_URL.'dashboard');
                    exit;
                } else {
                    $errors[] = "Identifiants incorrects.";
                }
            }
        }

        if (!empty($errors)) {
            $_SESSION['login_errors'] = $errors;
            header('Location: ' . BASE_URL . 'login');
            exit;
        }

    }

}

Nous venons de mettre en place une fonctionnalité indispensable pour tout site moderne : l’activation d’un compte par lien de confirmation email. C’est une étape essentielle pour garantir la sécurité, vérifier la bonne adresse du visiteur, et éviter les inscriptions frauduleuses.

Grâce à ce mécanisme, nous assurons que seuls les utilisateurs qui ont confirmé leur adresse mail peuvent accéder aux fonctionnalités réservées. Cela renforce la fiabilité de notre plateforme, sécurise les accès, et améliore l’expérience utilisateur.

Ce système d’activation de compte par email est aujourd’hui incontournable. Il protège votre application, et rassure vos membres.

La suite, demain …

Live on Twitch