Authentification de Symfony2

conseils Symfony2

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
Pendant le travail sur mon dernier projet j'ai été confronté au développement d'un composant d'authentification. Vu l'absence de la documentation, la tâche n'était pas facile. C'est pourquoi j'ai décidé de partager cette expérience avec vous, l'histoire de vous éviter les "me**es" et "fu**s".

Dans un premier temps on expliquera brièvement le fonctionnement de l'authentification sous Symfony2. Ensuite on passera au développement de notre composant.

Le flux d'authentification de l'utilisateur est très bien expliqué (et dessiné) sur le site du Symfony2. Juste au titre d'un bref rappel, Symfony2 emploie la notion des firewalls qui interceptent la requête et adaptent les actions définies dans le fichier app/config/security.yml (chemin par défaut).

Développement d'un Authentication Provider
On va commencer par personnaliser le fichier security.yml. On va y renseigner le fournisseur d'accès, le chemin vers le fichier de configuration de nos classes d'authentification ainsi que les firewalls et les rôles des utilisateurs qui permettent d'accéder aux ressources.


security:
encoders:
User\ProfilesBundle\Entity\Users: plaintext

## Le chemin vers le fichier de configuration
factories:
- "%kernel.root_dir%/../src/Frontend/FrontBundle/Resources/config/security_factories.xml"

role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

## La classe qui gère les utilisateurs
providers:
frontend:
entity: { class: User\ProfilesBundle\Entity\Users, property: login}

## Les firewalls
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false

login:
pattern: ^/connexion$
anonymous: ~

## auth : true => indique qu'il faut utiliser la classe d'authentification qu'on va bientôt créer; la classe aura l'identifiant "auth"
## Attention : le chemin vers check_path doit correspondre au pattern défini
frontend:
pattern: ^/mon_compte/.*
anonymous: false
# security: true
auth: true
provider: frontend
form_login:
check_path: /mon_compte/do
login_path: /connexion
username_parameter: username
password_parameter: password
post_only: true
default_target_path: /mon_compte/
access_control:
- { path: ^/mon_compte, roles: ROLE_ADMIN }
- { path: ^/do, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/connexion, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /.*, roles: IS_AUTHENTICATED_ANONYMOUSLY }


Maintenant on va créer des classes d'authentification. A cause des raisons que je ne vais pas expliquer dans cet article, on va les développer dans le répertoire vendor/Security. L'injection de dépendance sera exécutée dans le bundle qui s'appelle Frontend. L'article ne va pas présenter le développement d'un bundle sous Symfony2. On parcourra rapidement le développement d'une extension qui va s'occuper de charger le fichier de configuration pour le dependency injection :


namespace Frontend\FrontBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;

class FrontendFrontExtension extends Extension
{

public function load(array $configs, ContainerBuilder $container)
{
// we load configuration file with our authentication services
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
}


}

Cette extension n'est pas compliquée. La seule méthode présente, load(), charge le fichier contenant les services attachés à ce bundle.

Qu'est-ce que contient ce fameux fichier services.xml ? Son contenu n'est pas non plus très sophistiqué. Il stocke les éléments dont on aura besoin pour notre système d'authentification. Il se présente ainsi :


<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="security.form.listener">Security\AuthenticationFormListener</parameter>
<parameter key="security.user.provider.entity.class">Security\AuthenticationEntity</parameter>
</parameters>
<services>
<service id="security.authentication.provider.auth" class="Security\AuthenticationProvider" public="false">
<argument />
</service>
<service id="security.authentication.listener.auth" class="Security\AuthenticationListener" public="false">
<argument type="service" id="security.context"/>
<argument type="service" id="security.authentication.manager" />
<argument />
<argument type="service" id="logger" on-invalid="null" />
</service>
<service id="security.authentication.factory.auth" class="Frontend\FrontBundle\DependencyInjection\Security\Factory\AuthFactory" public="false">
<tag name="security.listener.factory" />
</service>
<service id="security.authentication.listener.form" class="%security.form.listener%"parent="security.authentication.listener.abstract" abstract="true" />
</services>
</container>

Le fichier contient la déclaration composants du mécanisme d'authentification :
- un fournisseur (provider) avec l'identifiant "security.authentication.provider.auth". Il prend comme paramètre la classe qui va contenir les informations sur les utilisateurs
- un écouteur (listener) qui prend comme paramètres : le contexte de sécurité (security.context), le gestionnaire d'authentication (security.authentication.manager), la clé du firewall (argument avec le commentaire Provier-shared Key), le loggeur (logger)
- un écouteur (security.authentication.listener.form) pour gérer la requête d'authentification envoyé depuis le formulaire de connexion précisé dans security.yml sous la clé check_path.
- un fournisseur pour l'entité (user.provider.entity.class).

A ce stade-là on doit aussi préciser le contenu du fichier security_factories.xml :

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="security.authentication.factory.auth" class="Frontend\FrontBundle\DependencyInjection\Security\Factory\AuthFactory" public="false">
<tag name="security.listener.factory" />
</service>
</services>
</container>

Le fichier contient la déclaration d'un seul service : la fabrique (factory) qui initialise tout le mécanisme d'authentification.

Maintenant on va passer au développement des classes : factory, provider, listener et authentication form listener.

Factory

namespace Frontend\FrontBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory ;

class AuthFactory extends AbstractFactory
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.auth.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.'.$id))
->replaceArgument(0, new Reference($userProvider));

$listenerId = 'security.authentication.listener.auth.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.'.$id))
->replaceArgument(2, $id); // replace third argument by firewall's id

return array($providerId, $listenerId, $defaultEntryPoint);
}

public function getPosition()
{
return 'pre_auth';
}

public function getKey()
{
return 'auth';
}

public function addConfiguration(NodeDefinition $node)
{
}

protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
return 'security.authentication.provider.auth';
}

protected function getListenerId()
{
return 'security.authentication.listener.auth';
}

}

Ce qui nous intéresse le plus, ce sont les méthodes create() et getKey(). La première conçoit le listener et remplace la balise <argument /> par l'identifiant du firewall. Cela s'effectue à travers la méthode replaceArgument(2, $id). La fonction getKey() sert à définir la clé qui sera utilisée dans le fichier security.yml. Comme cela a été indiqué au début de l'article, on utilisera pour cela la clé "auth". D'autres méthodes retournent les identifiants définis dans services.xml .

Authentication listener


use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Security\AuthenticationToken;
use Security\AuthenticationProvider;
use Symfony\Component\HttpFoundation\Response;

class AuthenticationListener implements ListenerInterface
{

protected $securityContext;
protected $authenticationManager;
protected $providerKey;
private $logger;

/**
* Constructor for listener. The parameters are defined in security.xml.
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, $providerKey, LoggerInterface $logger = null)
{
// DEBUG echo 'AuthenticationListener : __construct
';
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->providerKey = $providerKey;
$this->logger = $logger;
}

/**
* Handles login request.
* @access public
* @param GetResponseEvent $event Handled event
*/
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$session = $request->getSession();
$securityToken = $this->securityContext->getToken(); //var_dump($securityToken);//die();
if($securityToken instanceof AuthenticationToken && $securityToken->isAuthenticated())
{
try
{
$token = $this->authenticationManager->authenticate($securityToken);
// set new AuthenticationToken
$this->securityContext->setToken($token);
}
catch(BadCredentialsException $e)
{
$this->securityContext->setToken(null);
$this->logger->debug(sprintf('Authentication failed for user: %s. Catched exception : %s',
$securityToken->getUser(), $e->getMessage()));
}
}
}

}

La classe est composée de :
- un constructeur qui prend comme paramètres les services définis dans notre fichier services.xml.
- une méthode qui intercepte la requête est execute le code avant l'affichage de la page (handle()). C'est dedans où se déroule l'authentification de l'utilisateur via la méthode authenticate().

Authentication provider

namespace Security;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Security\AuthenticationToken;
use Security\SaltCellar;

class AuthenticationProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $salt;

public function __construct(UserProviderInterface $userProvider)
{
// DEBUG echo 'AuthenticationProvider : __construct
';
$this->userProvider = $userProvider;
// It's only an example, you can salt it better
$this->salt = $_SERVER['REMOTE_ADDR'].'salT';
}

/**
* Authenticates AuthenticationToken user.
* @access public
* @param TokenInterface $token Token of logged user.
* @return TokenInterface if authentication was done correctly. Otherwise, it throwns an exception.
*/
public function authenticate(TokenInterface $token)
{
// DEBUG echo 'AuthenticationProvider : authenticate
';
$result = false;
// regenerate session
if(time()%2 == 0)
{
session_regenerate_id(false);
}
$userLogin = $token->getUsername();
if(!preg_match('/^([A-Za-z0-9\-_])+$/i', $userLogin))
{
$userLogin = '';
}
$userRow = $this->userProvider->loadUserByUsername($token->getUsername());
if(isset($userRow))
{
// First login (from login form)
if($token->firstLogin)
{
$regDate = strtotime($userRow->getRegisteredDate());
$passSalt = sha1($this->salt.$token->getCredentials());
$result = (bool)($userRow->getLogin() == $token->getUsername() && $userRow->getPassword() == $passSalt);
}
else
{
// check fingerprinting
$attributes = $token->getAttributes();
$result = $userRow->checkFingerprinting($attributes['fingerprinting']);
if($result)
{
$result = (bool)($userRow->getPassword() == $token->getCredentials());
}
}
// DEBUG echo $passSalt.'
'.$userRow->getPassword();die();

if($result)
{
if($userRow->getUserState() != $userRow->getActiveState())
{
$token->tokenErrors = array('type' => 'notActivated', 'message' => "Le compte n'a pas été activé.");
throw new BadCredentialsException("Account wasn't activated.");
}
// DEBUG echo 'AuthenticationProvider : authenticate : TRUE
';
if($token->firstLogin)
{
$this->setFirstLogin($userRow->getIdUs());
}
$userRow->makeFingerprinting();
$securityToken = new AuthenticationToken($userRow->getLogin(), $userRow->getPassword(), $token->getProviderKey(),$userRow->getRoles());
$securityToken->setAttributes($userRow->getAttributes());
return $securityToken;
}
}
$token->tokenErrors = array('type' => 'invalidData', 'message' => "Les données de connexion ne sont pas correctes.");
throw new BadCredentialsException('Incorrect login data.');
}

/**
* Checks if this is the provider for AuthenticationToken.
* @access public
* @param TokenInterface $token User token.
* @return boolean True if it is the provider for AuthenticationToken, false otherwise.
*/
public function supports(TokenInterface $token)
{
// DEBUG echo 'AuthenticationProvider : supports()
';
return $token instanceof AuthenticationToken;
}

/**
* Notifies that user is connected and updates field in the database.
* @access public
* @param int $id Id of user to update.
* @return void
*/
public function setFirstLogin($id)
{
$this->userProvider->getRepository()->updateLastLogin($id);
}

public function getProviderKey()
{
return $this->providerKey;
}

}

La méthode clé de cette classe est authenticate() qui sert à authentifier l'internaute. Elle est appelée au début de notre block try{} catch{} du listener de l'authentification. La logique de cette fonction comporte principalement les comparaisons entre les différents getters, appartenant soit au token de l'utilisateur, soit au fournisseur des données (user provider). S'il s'agit de la première connexion de l'internaute, on appele aussi la méthode qui met à jour la date de la dernière visite de cet utilisateur.

User Token

namespace Security;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class AuthenticationToken extends AbstractToken implements TokenInterface
{

public $firstLogin = false;
private $credentials;
private $providerKey;
private $user;
public $tokenErrors = array('type' => '', 'message' => '');

/**
* Constructor.
*
* @param string $user The username (like a nickname, email address, etc.)
* @param string $credentials This usually is the password of the user
* @param string $providerKey The provider key
* @param array $roles An array of roles
*
* @throws \InvalidArgumentException
*/
public function __construct($user, $credentials, $providerKey, array $roles = array())
{
parent::__construct($roles);

if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}

$this->setUser($user);
$this->credentials = $credentials;
$this->providerKey = $providerKey;

parent::setAuthenticated(count($roles) > 0);
}

public function setAuthenticated($isAuthenticated)
{
if ($isAuthenticated) {
throw new \LogicException('Cannot set this token to trusted after instantiation.');
}

parent::setAuthenticated(false);
}

public function getCredentials()
{
return $this->credentials;
}

public function eraseCredentials()
{
return '';
}

public function serialize()
{
return serialize(array($this->credentials, $this->providerKey, parent::serialize()));
}

public function unserialize($str)
{
list($this->credentials, $this->providerKey, $parentStr) = unserialize($str);
parent::unserialize($parentStr);
}

public function getProviderKey()
{
return $this->providerKey;
}

}

Cette classe d'user token ne contient rien de plus qu'une classe native (UsernamePasswordToken par exemple). La seule différence consiste en présence de la variable $firstLogin qui est utilisée par AuthenticationProvider pour déterminer la méthode d'authentification.

User provider

namespace User\ProfilesBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class UsersRepository extends EntityRepository implements UserProviderInterface
{
/**
* Loads user by his login.
* @access public
* @param string $login User's login.
* @return User\ProfilesBundle\Entity\User
*/
public function loadUserByUsername($username)
{
return $this->findOneBy(array('login' => $username));
}

/**
* Updates last login informations.
* @access public
* @param int $id User's id.
* @return void
*/
public function updateLastLogin($id)
{
$date = new \DateTime();
$query = $this->getEntityManager()
->createQuery("UPDATE UserProfilesBundle:Users u SET
u.lastLogin = :lastLogin
WHERE u.id_us = :id_us")
->setParameter("id_us", (int)$id)
->setParameter("lastLogin", $date->format('Y-m-d H:i:s'));
$query->execute();
}

/**
* Do nothing. They are here to keep the comptability with UserProviderInterface.
*/
function refreshUser(UserInterface $user)
{
}

public function supportsClass($class)
{
return true;
}
}

Cette classe représente seulement les méthodes utilisées dans les classes précedentes. On y retrouve donc la méthode du chargement de l'utilisateur et celle qui met à jour la dernière connexion de l'internaute.

Authentication form listener

namespace Security;

use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;

class AuthenticationFormListener extends AbstractAuthenticationListener
{

private $csrfProvider;

/**
* {@inheritdoc}
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, array $options = array(), AuthenticationSuccessHandlerInterface $successHandler = null, AuthenticationFailureHandlerInterface $failureHandler = null, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'intention' => 'authenticate',
'post_only' => true,
), $options), $successHandler, $failureHandler, $logger, $dispatcher);

$this->csrfProvider = $csrfProvider;
}

/**
* {@inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
// Detect which authentication method is supported
if($this->options['post_only'] && 'post' !== strtolower($request->getMethod()))
{
if(null !== $this->logger)
{
$this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod()));
}
return null;
}

if(null !== $this->csrfProvider)
{
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);

if(false === $this->csrfProvider->isCsrfTokenValid($this->options['intention'], $csrfToken))
{
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}

// Clean form data with XSS filter
$data = $this->prepareData(array('username' => trim($request->get($this->options['username_parameter'], null, true)),
'password' => $request->get($this->options['password_parameter'], null, true)));

// Initialize new token
$token = new AuthenticationToken($data['login'], $data['password'], $this->providerKey);
$token->firstLogin = true;
return $this->authenticationManager->authenticate($token);
}

/**
* Handle form submit for AuthenticationToken (makes login, password).
* @access private
* @param array $data Form data.
* @return array Prepared data.
*/
private function prepareData($data)
{
$filter = new FilterXss(array('xss' => 'STRICT'));
return array('login' => $filter->filterXss($data['username']), 'password' => $filter->filterXss($data['password']));
}

}

Cette méthode remplace la classe Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener, utilisée par défaut pour toutes les requêtes d'authentification via la clé form_login du security.yml . Notre nouvelle classe reprend la fonctionnalité de celle développée par Fabien Potencier. Elle a été seulement enrichie par un filtrage des données et une précision qu'il s'agit de la première connexion.

User entity provider

namespace User\ProfilesBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\True;
use Symfony\Component\Validator\Constraints\MaxLength;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Validators\IsUsed;
use Validators\CheckPassword;

/**
* @ORM\Table(name="users")
* @ORM\Entity(repositoryClass="User\ProfilesBundle\Repository\UsersRepository")
*/
class Users
{

/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id_us;

/**
* @ORM\Column(name="login", type="string", length="10", nullable=false)
*/
protected $login;

/**
* @ORM\Column(name="password", type="text", length="255", nullable=false)
*/
protected $password;

/**
* @ORM\Column(name="email", type="text", length="200", nullable=false)
*/
protected $email;

/**
* @ORM\Column(name="registered", type="datetime", nullable=false)
*/
protected $registeredDate;

/**
* @ORM\Column(name="last", type="datetime", nullable=false)
*/
protected $lastLogin;

/**
* @ORM\Column(name="ip", type="text", length="40", nullable=false)
*/
protected $userIp;

/**
* @ORM\Column(name="type", type="integer", length="1", nullable=false)
*/
protected $userType;



/**
* @ORM\Column(name="state", type="integer", length="1", nullable=false)
*/
protected $userState;

/**
* @ORM\Column(name="profile", type="text", nullable=false)
*/
protected $userProfile;

public static $em;
public $fingerprinting;
public $pass1;
public $pass2;

public function setLogin($value)
{
$this->login = $value;
}
public function setEmail($value)
{
$this->email = $value;
}
public function setPassword($value)
{
$this->password = $value;
}
public function setUserState($value)
{
$this->userState = $value;
}

public function getIdUs()
{
return $this->id_us;
}
public function getLogin()
{
return $this->login;
}
public function getPassword()
{
return $this->password;
}
public function getEmail()
{
return $this->email;
}
public function getRegisteredDate()
{
return $this->registeredDate->format('Y-m-d');
}
public function getUserProfile()
{
return $this->userProfile;
}
public function getUserState()
{
return $this->userState;
}
public function getRoles()
{
return array('ROLE_ADMIN');
}
public function getUsername()
{
return $this->login;
}
public function getAttributes()
{
return array(
'id' => $this->id,
'email' => $this->email,
'type' => $this->userType,
'profile' => $this->userProfile,
'fingerprinting' => $this->fingerprinting
);
}

/**
* Gets active state code.
* @access public
* @return integer Integer of active account.
*/
public function getActiveState()
{
return 1;
}

public function eraseCredentials()
{
}

/**
* Makes fingerprinting proof of connected user.
* @access public
* @return void
*/
public function makeFingerprinting()
{
$this->fingerprinting = sha1($_SERVER['HTTP_USER_AGENT']."".$_SERVER['SERVER_ADDR']."".$_SERVER['SERVER_PROTOCOL']."zmaslemiogorkiem".$_SERVER['HTTP_ACCEPT_ENCODING'].'abbbisjqjsjd893732');
}

/**
* Compres to fingerprinting proofs (connected user and based on physically user's data).
* @access public
* @return bool True if the proofs are correct, false otherwise
*/
public function checkFingerprinting($fingerprinting)
{
$this->makeFingerprinting();
return (bool)($this->fingerprinting == $fingerprinting);
}

Cette classe est l'entité du provider définie dans security.yml sous la clé providers -> frontend -> entity. On y retrouve des setters et getters classiques (pour récupérer les valeurs des champs) ainsi que deux méthodes supplémentaires qui valident la trace de l'utilisateur (makeFingerprinting() et checkFingerprinting($fingerprinting)). Il faut également prendre en compte la méthode eraseCredentials() vide, ce qui évite que le user token supprime le mot de passe de la session actuelle.

Provider entity class

namespace Security;

use Doctrine\ORM\EntityManager;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;

class AuthenticationEntity extends EntityUserProvider
{
private $class;
private $repository;
private $property;

/**
* Overrides parent constructor for be able to use $repository var.
* @access public
* @return void
*/
public function __construct(EntityManager $em, $class, $property = null)
{
parent::__construct($em, $class, $property);
$this->repository = $em->getRepository($class);
$this->class = $class;
}

/**
* Gets class repository.
* @access public
* @return Doctrine\ORM\EntityRepository
*/
public function getRepository()
{
return $this->repository;
}

/**
* Method called by authentication provider.
* @access public
* @param string $username User's login.
* @return Doctrine\ORM\EntityRepository Repository object.
*/
public function loadUserByUsername($username)
{
return $this->repository->loadUserByUsername($username);
}
}

Cette classe remplace Symfony\Bridge\Doctrine\Security\User\EntityUserProvider . Sa méthode loadUserByUsername($username) a été simplifiée. En outre, on a rajouté une fonction pour récupérer le repository (getRepository()). On l'utilise dans AuthenticationProvider pour mettre à jour la dernière connexion de l'internaute.

Le mécanisme est prêt. Bien évidemment, les méthodes de salage, de hashage du mot de passe devraient être modifiées et uniques pour chaque projet. Le sel devrait également être dynamique, idéalement unique pour chaque internaute connecté. La trace de l'utilisateur (fingerprinting) devrait aussi changer avec chaque nouvelle application web.

L'article écrit en rythme de:
Kénédy - Enmé mwen an kreyol
Bartosz KONIECZNY 09-08-2011 14:43 Symfony2
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil JavaScript

Comment récupérer la hauteur de l'élément avec jQuery ?

On peut le faire avec deux méthodes. La première fonctionne correctement pour tous les navigateurs sauf Internet Explorer. Pour le cas des navigateurs Microsoft, elle marche uniquement quand la hauteur est spécifiée en dur :

$('#element').css('height')
L'alternative est la méthode height() qui est gérée par tous les navigateurs, même si l'élément a la hauteur "auto" :
$('#element').height();