• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Itération 8 : gestion de la sécurité

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Le but de cette itération est d'offrir aux visiteurs la possibilité de s'identifier afin d'être reconnus par l'application.

Contexte métier

Nous souhaitons ajouter à notre mini-CMS une fonctionnalité d'ajout de commentaires à un article. Cependant, cet ajout ne doit être possible que pour les utilisateurs enregistrés dans l'application. Tout commentaire sera associé à son auteur, qui est nécessairement un utilisateur enregistré.

Afin de pouvoir réaliser cette fonctionnalité, il est nécessaire de sécuriser notre application web. Pour cela, nous allons tirer parti des possibilités du framework Silex, reprises de celles de son grand frère Symfony.

Symfony et la sécurité

La sécurisation est un besoin récurrent des applications web. Comme tous les frameworks majeurs, Symfony dispose de fonctionnalités avancées dans ce domaine. Ce paragraphe en fait un bref résumé. Pour plus de détails sur la sécurité avec Symfony/Silex, consultez les rubriques associées des documentations de Symfony et de Silex.

Symfony envisage la sécurité comme un processus en deux étapes :

  1. L'authentification. Durant cette étape, l'utilisateur s'identifie auprès de l'application. Celle-ci tente ensuite de le reconnaître.

  2. L'autorisation. Ici, l'application détermine si l'utilisateur reconnu a accès à la ressource qu'il demande.

Authentification et autorisation avec Symfony

Symfony permet de sécuriser les ressources d'une application web en définissant un pare-feu(firewall). Lorsqu'un utilisateur fait une requête à une URL qui est protégée par un pare-feu, le système de sécurité de Symfony est activé. Le rôle du pare-feu est de déterminer si un utilisateur doit ou ne doit pas être authentifié (selon sa configuration, un pare-feu peut autoriser ou non les utilisateurs anonymes), et s'il doit l'être, de retourner une réponse à l'utilisateur afin d'entamer le processus d'authentification. Cette authentification peut prendre différentes formes : saisie d'un couple login/mot de passe dans un formulaire web (la plus courante), certificat, etc.

Accès d'un utilisateur anonyme à l'application
Accès d'un utilisateur anonyme à l'application

L'autorisation se base sur l'attribution de rôles aux utilisateurs reconnus. Dans la configuration du pare-feu, on peut soumettre l'accès à certaines ressources à la possession par l'utilisateur du rôle associé.

Refus d'accès à une ressource nécessitant le rôle d'administrateur
Refus d'accès à une ressource nécessitant le rôle d'administrateur

Gestion des mots de passe

La forme la plus courante d'authentification, celle que nous allons adopter, consiste à attribuer à chaque utilisateur un login et un mot de passe. Ces identifiants (credentials) permettent à l'application de reconnaître l'utilisateur.

Comme l'actualité nous le rappelle souvent, la gestion des mots de passe revêt une importante critique. Il est essentiel qu'une application web stocke et manipule ses mots de passe sous forme cryptée et non directement "en clair". Les mots de passe de nos utilisateurs seront donc stockés après application d'un algorithme de hachage cryptographique. Par exemple, le mot Baptiste donne le résultat 04ce34a463c52d41c4d0c04c9afd0abe après application de l'algorithme de hachage MD5. Le résultat d'une opération de hachage est appelée empreinte.

Hachage de différents textes (source : Wikipedia)
Hachage de différents textes (source : Wikipedia)

Les algorithmes de hachage ont la particularité d'être unidirectionnels : il n'existe pas de moyen de revenir de l'empreinte obtenue au mot de passe en clair initial. Le mot de passe saisi par un utilisateur sera immédiatement haché avec le même algorithme, puis comparé avec la valeur cryptée stockée : si les deux résultats sont identiques, c'est que l'utilisateur a saisi le bon mot de passe.

Même si nos mots de passe sont stockés sous forme hachée, le risque subsiste qu'un individu mal intentionné arrive à s'introduire dans la base de données pour dérober ces mots de passe cryptés. Il pourrait ensuite utiliser une solution de type force brute pour hacher un très grand nombre de mots de passe et comparer le résultat avec les mots de passe volés jusqu'à trouver une correspondance.

Le salage est une solution pour limiter ce risque. Cette technique consiste à ajouter plusieurs caractères au mot de passe juste avant de le hacher. Le résultat du hachage est différent de celui obtenu avec le mot de passe seul. Cela permet de protéger les mots de passe contre les attaques de type dictionnaire (force brute avec utilisation d'une liste de mots de passe potentiels). Les données ajoutés au mot de passe sont appelées salt. Pour plus de sécurité, le salt doit être différent pour chaque mot de passe.

Pour plus de détails concernant les bonnes pratiques de cryptage des mots de passe, consultez cet article.

Sécurisation de l'application

Base de données

La structure de notre base de données doit évoluer afin de refléter les nouveaux besoins métier : un commentaire est maintenant associé à un utilisateur de l'application. Voici le nouveau script SQLdb/structure.sql qui permet de créer la base.

drop table if exists t_comment;
drop table if exists t_user;
drop table if exists t_article;

create table t_article (
    art_id integer not null primary key auto_increment,
    art_title varchar(100) not null,
    art_content varchar(2000) not null
) engine=innodb character set utf8 collate utf8_unicode_ci;

create table t_user (
    usr_id integer not null primary key auto_increment,
    usr_name varchar(50) not null,
    usr_password varchar(88) not null,
    usr_salt varchar(23) not null,
    usr_role varchar(50) not null 
) engine=innodb character set utf8 collate utf8_unicode_ci;

create table t_comment (
    com_id integer not null primary key auto_increment,
    com_content varchar(500) not null,
    art_id integer not null,
    usr_id integer not null,
    constraint fk_com_art foreign key(art_id) references t_article(art_id),
    constraint fk_com_usr foreign key(usr_id) references t_user(usr_id)
) engine=innodb character set utf8 collate utf8_unicode_ci;

Les utilisateurs sont stockés dans la tablet_user. Voici la description de ses champs.

  • usr_id est l'identifiant de l'utilisateur.

  • usr_name est le nom de l'utilisateur. Il sera utilisé commé login. 

  • usr_password est son mot de passé, stockée sous forme hachée.

  • usr_salt est le salage utilisé pour hacher le mot de passe. Il est stocké en clair.

  • usr_role est le rôle attribué à l'utilisateur. Il sera utilisé lors de la phase d'autorisation.

On observe également que la tablet_comment contient maintenant une clé étrangère vers la tablet_user , afin de matérialiser le lien entre un commentaire et son auteur.

Les données de test sont également mises à jour. Voici le scriptdb/content.sql  associé.

insert into t_article values
(1, 'First article', 'Hi there! This is the very first article.');
insert into t_article values
(2, 'Lorem ipsum', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut hendrerit mauris ac porttitor accumsan. Nunc vitae pulvinar odio, auctor interdum dolor. Aenean sodales dui quis metus iaculis, hendrerit vulputate lorem vestibulum. Suspendisse pulvinar, purus at euismod semper, nulla orci pulvinar massa, ac placerat nisi urna eu tellus. Fusce dapibus rutrum diam et dictum. Sed tellus ipsum, ullamcorper at consectetur vitae, gravida vel sem. Vestibulum pellentesque tortor et elit posuere vulputate. Sed et volutpat nunc. Praesent nec accumsan nisi, in hendrerit nibh. In ipsum mi, fermentum et eleifend eget, eleifend vitae libero. Phasellus in magna tempor diam consequat posuere eu eget urna. Fusce varius nulla dolor, vel semper dui accumsan vitae. Sed eget risus neque.');
insert into t_article values
(3, 'Lorem ipsum in french', "J’en dis autant de ceux qui, par mollesse d’esprit, c’est-à-dire par la crainte de la peine et de la douleur, manquent aux devoirs de la vie. Et il est très facile de rendre raison de ce que j’avance. Car, lorsque nous sommes tout à fait libres, et que rien ne nous empêche de faire ce qui peut nous donner le plus de plaisir, nous pouvons nous livrer entièrement à la volupté et chasser toute sorte de douleur ; mais, dans les temps destinés aux devoirs de la société ou à la nécessité des affaires, souvent il faut faire divorce avec la volupté, et ne se point refuser à la peine. La règle que suit en cela un homme sage, c’est de renoncer à de légères voluptés pour en avoir de plus grandes, et de savoir supporter des douleurs légères pour en éviter de plus fâcheuses.");

/* raw password is 'john' */
insert into t_user values
(1, 'JohnDoe', '$2y$13$F9v8pl5u5WMrCorP9MLyJeyIsOLj.0/xqKd/hqa5440kyeB7FQ8te', 'YcM=A$nsYzkyeDVjEUa7W9K', 'ROLE_USER');
/* raw password is 'jane' */
insert into t_user values
(2, 'JaneDoe', '$2y$13$qOvvtnceX.TjmiFn4c4vFe.hYlIVXHSPHfInEG21D99QZ6/LM70xa', 'dhMTBkzwDKxnD;4KNs,4ENy', 'ROLE_USER');

insert into t_comment values
(1, 'Great! Keep up the good work.', 1, 1);
insert into t_comment values
(2, "Thank you, I'll try my best.", 1, 2);

Ce jeu de données insère dans la base les utilisateurs 'JohnDoe' (mot de passe : 'john') et 'JaneDoe' (mot de passe : 'jane'). Pour chaque utilisateur, un salage a été généré aléatoirement puis l'algorithme de hachage par défaut de Symfony a été utilisé pour générer le mot de passe crypté. Celui-ci est stocké dans la base. On attribue à tous les utilisateurs le rôle ROLE_USER (rôle par défaut pour Symfony).

Exécutezstructure.sql puiscontent.sql afin de mettre à jour votre base de données.

Composant Symfony

Afin d'exploiter les fonctionnalités offertes par Symfony, nous devons récupérer le composant nécessaire. Il suffit pour cela de le déclarer dans le fichier de dépendancescomposer.json. Ce composant se nomme security et il regroupe les services de gestion de la sécurité. 

"require": {
    ...
    "symfony/security": "~2.8|3.0.*"
}
...

Une fois ce fichier modifié, on utilise Composer pour télécharger ce composant.

composer update

Partie Modèle

La première étape de notre travail dans cette partie est de modéliser un utilisateur de l'application sous la forme d'une classeUser située dans l'espace de nomsMicroCMS\Domain. Voici son code source. Créez cette classe dans le fichier sourcesrc/Domain/User.php.

<?php

namespace MicroCMS\Domain;

use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface
{
    /**
     * User id.
     *
     * @var integer
     */
    private $id;

    /**
     * User name.
     *
     * @var string
     */
    private $username;

    /**
     * User password.
     *
     * @var string
     */
    private $password;

    /**
     * Salt that was originally used to encode the password.
     *
     * @var string
     */
    private $salt;

    /**
     * Role.
     * Values : ROLE_USER or ROLE_ADMIN.
     *
     * @var string
     */
    private $role;

    public function getId() {
        return $this->id;
    }

    public function setId($id) {
        $this->id = $id;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getUsername() {
        return $this->username;
    }

    public function setUsername($username) {
        $this->username = $username;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getPassword() {
        return $this->password;
    }

    public function setPassword($password) {
        $this->password = $password;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getSalt()
    {
        return $this->salt;
    }

    public function setSalt($salt)
    {
        $this->salt = $salt;
        return $this;
    }

    public function getRole()
    {
        return $this->role;
    }

    public function setRole($role) {
        $this->role = $role;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getRoles()
    {
        return array($this->getRole());
    }

    /**
     * @inheritDoc
     */
    public function eraseCredentials() {
        // Nothing to do here
    }
}

On constate une différence importante avec les classes métierArticle etComment : la classeUser implémente l'interface Symfony UserInterface et définit les méthodes présentes dans cette interface (ces méthodes sont identifiées par des@inheritDoc dans les commentaires de la classeUser). Ces méthodes sont indispensables pour que l'utilisateur puisse être authentifié et autorisé par Symfony.

L'auteur d'un commentaire est maintenant un utilisateur enregistré. On modélise cela par une association entre les classes du domaineComment etUser.

Diagramme de classes UML du domaine
Diagramme de classes UML du domaine

La classeComment subit un changement mineur du point de vue du code source, mais important pour son utilisation : sa propriété$author n'est plus une chaîne de caractères, mais une instance de la classeUser. On met à jour le commentaire et le mutateur associés pour refléter cette évolution.

<?php

namespace MicroCMS\Domain;

class Comment 
{
    // ...

    /**
     * Comment author.
     *
     * @var \MicroCMS\Domain\User
     */
    private $author;

    // ...
    
    public function setAuthor(User $author) {
        $this->author = $author;
        return $this;
    }
    
    // ...

Nous devons également créer dans le fichiersrc/DAO/UserDAO.php la classeUserDAO qui gère l'accès aux utilisateurs.

<?php

namespace MicroCMS\DAO;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use MicroCMS\Domain\User;

class UserDAO extends DAO implements UserProviderInterface
{
    /**
     * Returns a user matching the supplied id.
     *
     * @param integer $id The user id.
     *
     * @return \MicroCMS\Domain\User|throws an exception if no matching user is found
     */
    public function find($id) {
        $sql = "select * from t_user where usr_id=?";
        $row = $this->getDb()->fetchAssoc($sql, array($id));

        if ($row)
            return $this->buildDomainObject($row);
        else
            throw new \Exception("No user matching id " . $id);
    }

    /**
     * {@inheritDoc}
     */
    public function loadUserByUsername($username)
    {
        $sql = "select * from t_user where usr_name=?";
        $row = $this->getDb()->fetchAssoc($sql, array($username));

        if ($row)
            return $this->buildDomainObject($row);
        else
            throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
    }

    /**
     * {@inheritDoc}
     */
    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if (!$this->supportsClass($class)) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $class));
        }
        return $this->loadUserByUsername($user->getUsername());
    }

    /**
     * {@inheritDoc}
     */
    public function supportsClass($class)
    {
        return 'MicroCMS\Domain\User' === $class;
    }

    /**
     * Creates a User object based on a DB row.
     *
     * @param array $row The DB row containing User data.
     * @return \MicroCMS\Domain\User
     */
    protected function buildDomainObject(array $row) {
        $user = new User();
        $user->setId($row['usr_id']);
        $user->setUsername($row['usr_name']);
        $user->setPassword($row['usr_password']);
        $user->setSalt($row['usr_salt']);
        $user->setRole($row['usr_role']);
        return $user;
    }
}

Cette classe reprend la structure de nos classes DAO existantes et implémente l'interface Symfony UserProviderInterface. Cette interface contient les méthodes nécessaires pour qu'une classe puisse être utilisée comme fournisseur de données utilisateur par le composant de gestion de la sécurité de Symfony au cours du processus d'authentification.

La classeCommentDAO est mise à jour : elle dépend maintenant de la classeUserDAO pour construire un objetComment complet à partir d'un résultat de requête SQL.

<?php

namespace MicroCMS\DAO;

use MicroCMS\Domain\Comment;

class CommentDAO extends DAO 
{
    /**
     * @var \MicroCMS\DAO\ArticleDAO
     */
    private $articleDAO;

    /**
     * @var \MicroCMS\DAO\UserDAO
     */
    private $userDAO;

    public function setArticleDAO(ArticleDAO $articleDAO) {
        $this->articleDAO = $articleDAO;
    }

    public function setUserDAO(UserDAO $userDAO) {
        $this->userDAO = $userDAO;
    }

    /**
     * Return a list of all comments for an article, sorted by date (most recent last).
     *
     * @param integer $articleId The article id.
     *
     * @return array A list of all comments for the article.
     */
    public function findAllByArticle($articleId) {
        // The associated article is retrieved only once
        $article = $this->articleDAO->find($articleId);

        // art_id is not selected by the SQL query
        // The article won't be retrieved during domain objet construction
        $sql = "select com_id, com_content, usr_id from t_comment where art_id=? order by com_id";
        $result = $this->getDb()->fetchAll($sql, array($articleId));

        // Convert query result to an array of domain objects
        $comments = array();
        foreach ($result as $row) {
            $comId = $row['com_id'];
            $comment = $this->buildDomainObject($row);
            // The associated article is defined for the constructed comment
            $comment->setArticle($article);
            $comments[$comId] = $comment;
        }
        return $comments;
    }

    /**
     * Creates an Comment object based on a DB row.
     *
     * @param array $row The DB row containing Comment data.
     * @return \MicroCMS\Domain\Comment
     */
    protected function buildDomainObject(array $row) {
        $comment = new Comment();
        $comment->setId($row['com_id']);
        $comment->setContent($row['com_content']);

        if (array_key_exists('art_id', $row)) {
            // Find and set the associated article
            $articleId = $row['art_id'];
            $article = $this->articleDAO->find($articleId);
            $comment->setArticle($article);
        }
        if (array_key_exists('usr_id', $row)) {
            // Find and set the associated author
            $userId = $row['usr_id'];
            $user = $this->userDAO->find($userId);
            $comment->setAuthor($user);
        }
        
        return $comment;
    }
}

Partie Contrôleur

Le fichier de configuration de l'application Silexapp/app.php est modifié pour intégrer les nouveaux services.

<?php

use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;

// Register global error and exception handlers
ErrorHandler::register();
ExceptionHandler::register();

// Register service providers
$app->register(new Silex\Provider\DoctrineServiceProvider());
$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__.'/../views',
));
$app->register(new Silex\Provider\AssetServiceProvider(), array(
    'assets.version' => 'v1'
));
$app->register(new Silex\Provider\SessionServiceProvider());
$app->register(new Silex\Provider\SecurityServiceProvider(), array(
    'security.firewalls' => array(
        'secured' => array(
            'pattern' => '^/',
            'anonymous' => true,
            'logout' => true,
            'form' => array('login_path' => '/login', 'check_path' => '/login_check'),
            'users' => function () use ($app) {
                return new MicroCMS\DAO\UserDAO($app['db']);
            },
        ),
    ),
));

// Register services
$app['dao.article'] = function ($app) {
    return new MicroCMS\DAO\ArticleDAO($app['db']);
};
$app['dao.user'] = function ($app) {
    return new MicroCMS\DAO\UserDAO($app['db']);
};
$app['dao.comment'] = function ($app) {
    $commentDAO = new MicroCMS\DAO\CommentDAO($app['db']);
    $commentDAO->setArticleDAO($app['dao.article']);
    $commentDAO->setUserDAO($app['dao.user']);
    return $commentDAO;
};

Il est important de bien comprendre les paramètres utilisés pour la définition du pare-feu (firewall) associé au fournisseur de servicesSecurityServiceProvider :

  • pattern définit la partie sécurisée de l'application sous la forme d'une expression rationnelle. Ici, la valeur^/ indique que le pare-feu sécurise l'intégralité de l'application ;

  • anonymous précise qu'un utilisateur non authentifié peut tout de même accéder à la partie sécurisée. Il est nécessaire pour que les visiteurs anonymes puissent continuer à consulter les articles du CMS ;

  • logout indique qu'il est possible pour les utilisateurs authentifiés de se déconnecter de l'application ;

  • form permet d'utiliser un formulaire comme méthode d'authentification.

  • login_path définit le chemin vers le formulaire etcheck_path le chemin d'authentification ;

  • users définit le fournisseur de données utilisateur, autrement dit la source de données qui permet d'accéder aux utilisateurs de l'application. Ici, il s'agit logiquement d'une instance de la classeUserDAO créée précédemment.

Il faut aussi ajouter une route pour afficher le formulaire d'authentification dans le fichierapp/routes.php.

<?php

use Symfony\Component\HttpFoundation\Request;

// ...

// Login form
$app->get('/login', function(Request $request) use ($app) {
    return $app['twig']->render('login.html.twig', array(
        'error'         => $app['security.last_error']($request),
        'last_username' => $app['session']->get('_security.last_username'),
    ));
})->bind('login');

Le contrôleur associé utilise la classe SymfonyRequest pour afficher la vuelogin.html.twig en lui passant en paramètres l'éventuelle dernière erreur de sécurité (par exemple un utilisateur non reconnu) et le dernier nom d'utilisateur utilisé.

Partie Vue

Dans la partie Vue, il faut tout d'abord créer la vuelogin.html.twig associée à la route d'authentification.

{% extends 'layout.html.twig' %}

{% block title %}User authentication{% endblock %}

{% block content %}
<h2 class="text-center">{{ block('title') }}</h2>
{% if error %}
<div class="alert alert-danger">
    <strong>Login failed!</strong> {{ error }}
</div>
{% endif %}
<div class="well">
    <form class="form-signin form-horizontal" role="form" action="{{ path('login_check') }}" method="post">
        <div class="form-group">
            <div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
            <input type="text" name="_username" value="{{ last_username }}" class="form-control" placeholder="Enter your username" required autofocus>
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
                <input type="password" name="_password" class="form-control" placeholder="Enter your password" required>
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-6 col-sm-offset-3 col-md-4 col-md-offset-4">
                <button type="submit" class="btn btn-default btn-primary"><span class="glyphicon glyphicon-log-in"></span> Login</button>
            </div>
        </div>
    </form>
</div>
{% endblock %}

Comme toutes nos vues, elle hérite delayout.html.twig afin d'intégrer les éléments d'interface communs (barre de navigation, pied de page, etc.). Elle définit un formulaire (balise<form>) contenant les champs_username et_password pour saisir le login et le mot de passe de l'utilisateur.

L'action associée à ce formulaire utilise la fonctionpath fournie par le composanttwig-bridge pour récupérer le chemin d'authentification défini lors du paramétrage du pare-feu. Le nom de ce chemin provient de la valeur du paramètrecheck_path : les/ sont remplacés par des_ et leinitial est supprimé.

Ensuite, on modifie la vuearticle.html.twig pour obtenir un affichage adapté à la présence d'un utilisateur connecté.

{% extends "layout.html.twig" %}

{% block title %}{{ article.title }}{% endblock %}

{% block content %}
<p>
    <h2>{{ article.title }}</h2>
    <p>{{ article.content }}</p>

    <h3>Comments</h3>
    {% for comment in comments %}
        <strong>{{ comment.author.username }}</strong> said : {{ comment.content }}<br>
    {% else %}
        No comments yet.
    {% endfor %}

    <h3>Add a comment</h3>
    {% if is_granted('IS_AUTHENTICATED_FULLY') %}
        Soon!
    {% else %}
        <a href="{{ path('login') }} ">Log in</a> to add comments.
    {% endif %}
</p>
{% endblock %}

Le nom de l'auteur du commentaire est maintenant accessible via la variable Twigcomment.author.username et non pluscomment.author. La ligne if is_granted('IS_AUTHENTICATED_FULLY') permet de vérifier si la vue est affichée pour un utilisateur connecté. Si c'est le cas, il faudrait lui offrir la possibilité d'ajouter un commentaire. Ce sera l'objet d'une prochaine itération. Sinon, on précise au visiteur anonyme qu'il doit se connecter pour pouvoir commenter l'article.

Enfin, on modifie la partie commune à toutes les vues (fichierlayout.html.twig) afin d'ajouter à la barre de navigation un menu déroulant associé à l'éventuel utilisateur authentifié.

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="{{ asset('/lib/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('/css/microcms.css') }}" rel="stylesheet">
    <title>MicroCMS - {% block title %}{% endblock %}</title>
</head>
<body>
    <div class="container">
        <nav class="navbar navbar-default navbar-fixed-top navbar-inverse" role="navigation">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-target">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="{{ path('home') }}">MicroCMS</a>
                </div>
                <div class="collapse navbar-collapse" id="navbar-collapse-target">
                    <ul class="nav navbar-nav navbar-right">
                        {% if is_granted('IS_AUTHENTICATED_FULLY') %}
                            <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                <span class="glyphicon glyphicon-user"></span> Welcome, {{ app.user.username }} <b class="caret"></b></a>
                                <ul class="dropdown-menu">
                                    <li><a href="{{ path('logout') }}">Log out</a></li>
                                </ul>
                            </li>
                        {% else %}
                            <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                                <span class="glyphicon glyphicon-user"></span> Not connected <b class="caret"></b></a>
                                <ul class="dropdown-menu">
                                    <li><a href="{{ path('login') }}">Log in</a></li>
                                </ul>
                            </li>
                        {% endif %}
                    </ul>
                </div>
            </div><!-- /.container -->
        </nav>
        <div id="content">{% block content %}{% endblock %}</div>
        <footer class="footer">
            <a href="https://github.com/bpesquet/OC-MicroCMS">MicroCMS</a> is a minimalistic CMS built as a showcase for modern PHP development.
        </footer>
    </div>
    
    <!-- jQuery -->
    <script src="{{ asset('/lib/jquery/jquery.min.js') }}"></script>
    <!-- JavaScript Boostrap plugin -->
    <script src="{{ asset('/lib/bootstrap/js/bootstrap.min.js') }}"></script>
</body>
</html>

Si l'utilisateur est authentifié, le menu affiche son nom (accessible via la variable Twigapp.user.username) et lui permet de se déconnecter (lien verspath('logout')). Sinon, il affiche un message et un lien vers le formulaire d'authentification (lien verspath('login')).

A la fin du fichier, on ajoute liens vers jQuery et le plugin JavaScript de Bootstrap afin de faire fonctionner le menu déroulant.

Résultat obtenu

La sécurisation de notre application est terminée. Ouvrez l'URL http://microcms pour afficher la page d'accueil de l'application. Elle doit maintenant disposer d'un menu déroulant en haut à droite. Lorsqu'aucun utilisateur ne s'est authentifié, ce menu comporte une entrée invitant l'utilisateur à le faire.‌

Le formulaire d'authentification permet à l'utilisateur de saisir son nom et son mot de passe. Les éventuelles erreurs d'authentification sont affichées.

Lorsque l'authentification réussit, le menu déroulant de la barre de navigation est mis à jour, ainsi que l'affichage d'un article.

Le code source associé à cette itération est disponible sur une branche du dépôt GitHub

Bilan

Cette itération nous a permis de sécuriser notre application web en exploitant les possibilités offertes par Symfony (et donc Silex). On constate combien l'intégration d'un framework, même si elle nécessite un travail initial d'adaptation, facilite l'ajout de fonctionnalités complexes à une application web.

La prochaine itération permettra aux utilisateurs authentifiés d'interagir avec l'application.

Exemple de certificat de réussite
Exemple de certificat de réussite