Authentification MySQL avec Silex, le micro-framework PHP

La documentation officielle de Silex fournit un tas d’exemples bien utiles pour démarrer un projet. Toutefois, on ne trouve pas d’exemple complet d’authentification MySQL. Voici donc les quelques étapes nécessaires à la mise en place de celle-ci.

Introduction

Ici, nous allons sécuriser l’ensemble du front-office, cela signifie que pour accéder à n’importe quelle url, il faudra avoir le rôle ROLE_USER, qui correspond à une personne connectée.

Pré-requis

1. DoctrineServiceProvider

$app->register(new Silex\Provider\DoctrineServiceProvider(), array(
    'db.options' => array(
        'driver' => 'pdo_mysql',
        'dbhost' => 'localhost',
        'dbname' => 'mydbname',
        'user' => 'root',
        'password' => '',
    ),
));

2. SessionServiceProvider

$app->register(new Silex\Provider\SessionServiceProvider());

3. Namespace App

Nous allons avoir besoin d’une classe spécifique pour la connexion, nous allons donc déclarer le namespace App, que vous pouvez renommer à votre convenance.

Modification du fichier composer.json :

"autoload": {
    "psr-0": {
        "App": "app/src/" // chemin vers les classes spécifiques à votre application
    }
}

Mise en place du provider SecurityServiceProvider

Ce provider nécessite de mettre à jour les dépendances dans votre fichier composer.json, ajoutez-y ceci :

"require": {
    "symfony/security": "2.1.*"
}

Enregistrement du provider :

$app->register(new Silex\Provider\SecurityServiceProvider(), array(
    'security.firewalls' => array(
        'foo' => array('pattern' => '^/foo'), // Exemple d'une url accessible en mode non connecté
        'default' => array(
            'pattern' => '^.*$',
            'anonymous' => true, // Indispensable car la zone de login se trouve dans la zone sécurisée (tout le front-office)
            'form' => array('login_path' => '/', 'check_path' => 'connexion'),
            'logout' => array('logout_path' => '/deconnexion'), // url à appeler pour se déconnecter
            'users' => $app->share(function() use ($app) {
                // La classe App\User\UserProvider est spécifique à notre application et est décrite plus bas
                return new App\User\UserProvider($app['db']);
            }),
        ),
    ),
    'security.access_rules' => array(
        // ROLE_USER est défini arbitrairement, vous pouvez le remplacer par le nom que vous voulez
        array('^/.+$', 'ROLE_USER'),
        array('^/foo$', ''), // Cette url est accessible en mode non connecté
    )
));

Classe App\User\UserProvider

<?php
// app/src/App/User/UserProvider.php
namespace App\User;
 
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\DBAL\Connection;
 
class UserProvider implements UserProviderInterface
{
    private $conn;
 
    public function __construct(Connection $conn)
    {
        $this->conn = $conn;
    }
 
    public function loadUserByUsername($username)
    {
        $stmt = $this->conn->executeQuery('SELECT * FROM users WHERE username = ?', array(strtolower($username)));
        if (!$user = $stmt->fetch()) {
            throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
        }
 
        return new User($user['username'], $user['password'], explode(',', $user['roles']), true, true, true, true);
    }
 
    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
 
        return $this->loadUserByUsername($user->getUsername());
    }
 
    public function supportsClass($class)
    {
        return $class === 'Symfony\Component\Security\Core\User\User';
    }
}

Structure de la table `users`

CREATE TABLE `users` (
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(100) NOT NULL DEFAULT '',
  `password` VARCHAR(255) NOT NULL DEFAULT '',
  `roles` VARCHAR(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Création d’un utilisateur pour test

-- Le mot de passe est : password
INSERT INTO `users` (`username`, `password`, `roles`) VALUES ('johann', 'BFEQkknI/c+Nd7BaG7AaiyTfUFby/pkMHy3UsYqKqDcmvHoPRX/ame9TnVuOV2GrBH0JK9g4koW+CgTYI9mK+w==', 'ROLE_USER');

Le mot de passe a été généré avec le code ci-dessous :

<?php
 
echo $app['security.encoder.digest']->encodePassword('password', '');
// BFEQkknI/c+Nd7BaG7AaiyTfUFby/pkMHy3UsYqKqDcmvHoPRX/ame9TnVuOV2GrBH0JK9g4koW+CgTYI9mK+w==

Exemple d’utilisation avec Twig

Ajouter la dépéndance ci-dessous dans le fichier composer.json :

"require": {
    "symfony/twig-bridge": "2.1.*"
}
use Symfony\Component\HttpFoundation\Request;
 
$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__ . '/views',
));
 
$app->get('/', function(Request $request) use ($app) {
    return $app['twig']->render('index.twig', array(
        'error' => $app['security.last_error']($request),
        'last_username' => $app['session']->get('_security.last_username'),
    ));
});
{% if is_granted('ROLE_USER') %}
    <p>Bienvenue {{ app.security.token.user.username }} !</p>
    <p><a href="{{ path('deconnexion') }}">Déconnexion</a></p>
{% else %}
    <form action="{{ path('connexion') }}" method="post">
        <p><label for="username">Nom d'utilisateur : </label><input id="username" type="text" name="_username" value="{{ last_username }}"></p>
        <p><label for="password">Mot de passe : </label><input id="password" type="password" name="_password"></p>
        <p><input type="submit" value="Connexion"></p>
        {% if error %}
            <div class="error">{{ error }}</div>
        {% endif %}
    </form>
{% endif %}

Conclusion

Ceci n’est qu’un exemple simple qui pourrait être amélioré en utilisant un ORM et notre propre classe User mais cela fera peut-être l’objet d’un autre article :)

24 réponses à “Authentification MySQL avec Silex, le micro-framework PHP”

  1. Maksim dit :

    Great article. But are you sure about dependencies ? I have
    ReflectionException: Class Request does not exist
    And I have no I idea which class php can’t find.

    • Johann Reinke dit :

      Request class comes from Symfony\Component\HttpFoundation\Request
      You should add this line in top of your file :
      use Symfony\Component\HttpFoundation\Request;

  2. Maksim dit :

    Thanks, It helps. But now I have error with Twig it can’t recognize is_granted(). Is this additional extention?

  3. Maksim dit :

    ‘Identifier « security.authentication_providers » is not defined. Very wired … I’m already have ‘users’ => $app->share(function() use ($app)
    {
    // Specific class App\User\UserProvider is described below
    return new App\User\UserProvider($app['db']);
    })

  4. GromNaN dit :

    Great article. Having it in the cookbook for Silex documentation would make it accessible to everybody.

  5. Antonio dit :

    Nice article. However, I keep getting an annoying error saying:

    Twig_Error_Syntax: The function « path » does not exist in « index.twig » at line 3

    Any idea? I have looked around the web but no one seems to have had the same problem.

  6. Gerrit dit :

    Thanks for the great article !

  7. Marcin Kłeczek dit :

    Thank you, very good article. Good start to « full feature » authentication.

  8. René de Kat dit :

    Hi,

    Thanks for writing this tutorial. I am very new to Silex/Symfony and coming from Zend Framework I am in sort of a transition phase.

    I tried to integrate this into a simple project, but I keep getting: Bad credentials.

    Would anyone have a look at the example code here: https://github.com/9livesdevelopment/silex-mongoauth-skeleton

    I’ve commented all MySQL code, and hardcoded some username and password in. MySQL will be replaced with Mongo in my project.

    Thanks in advance for anyone who can shed some light on this.

    • René de Kat dit :

      Problem solved. Everything works like a charm now.

      The problem was I tested without encrypted passwords first and after that an encrypted version of an empty string which is no good.

      All works perfectly now with an encrypted string > 0 chars.

      • Dwi dit :

        Hi,

        I have the same issue with error ‘Bad credentials’, I also debug the user object in the App\User\UserProvider.php is correctly fetched from the database. I understand that this is not the only part of user authentication, and it looks like that error message came from:

        vendor/symfony/security/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php

        Where should I go to debug the underlying problem?

        Thanks

  9. Stephan dit :

    I’m getting a fatal error:

    Fatal error: Class ‘Doctrine\DBAL\Configuration’ not found in (..) vendor/silex/silex/src/Silex/Provider/DoctrineServiceProvider.php

    How can I fix this?

  10. Jean Christophe dit :

    Bonjour,
    Juste une question … Faut il écrire la fonction qui correspond à la route du check_path ?
    Quelque chose du genre
    $app->post(‘/connexion’…
    Par avance meri

    • Pingax dit :

      Non, check_path est définie automatiquement par Silex.

      Extrait de la doc : « The admin_login_check route is automatically defined by Silex and its name is derived from the check_path value (all / are replaced with _ and the leading / is stripped). »

      Très bon article, que je bookmark de ce pas !

  11. James dit :

    Thanks for the article! This utilises PHP for the config – I’m using YAML. Is there any way to link the users to the right class using YAML instead of PHP?

  12. switcherdav dit :

    Alors là un grand merci pour ton article.

    newbie sur ce framework, j’avais juste oublié d’ajouter /index.php dans mon URL pour que ça fonctionne (vu que je ne me suis pas encore occupé du htaccess)

    Reste maintenant à décortiquer le code pour être capable de le reproduire …

  13. DK dit :

    Very simplified version…
    One question-
    I am new to silex…What are the considerations if we want to just use Rest api based authentication tokens for server and client side…

  14. CT dit :

    Thank you for providing this simple tutorial allowing me to create my skeleton for several projects.

    That being said, I kept having the « bad credentials » error and none of the exposed solutions did work for me.

    But, after looking at the source code, I saw that the password validity was checked with the « password_verify() » function of php. So, instead of creating the password with « $app['security.encoder.digest']->encodePassword(‘password’,  »); », I created it with « password_hash(‘password’,PASSWORD_BCRYPT) » and it worked. But I’m not happy with it, I would rather create the password with the « encodePassword » of the Symfony’s security encoder.

    Any suggestion ?

  15. Nas dit :

    Je suis constamment redirigé sur login meme quand la connexion est bonne?!

    Comment rediriger ailleurs ?

    Merci

  16. ashtoneason dit :

    What’s up to every body, it’s my first go to see of this weblog; this web site consists of amazing and actually fine data in favor of readers.

Laisser un commentaire

* Champs requis

Categories