Autenticazione

Quando una richiesta punta a un’area protetta e se uno degli ascoltatori della mappa dei firewall è in grado di estrarre le credenziali dell’utente dall’oggetto Symfony\Component\HttpFoundation\Request corrente, dovrebbe creare un token, che contiene tali credenziali. La cosa successiva che l’ascoltatore dovrebbe fare è chiedere al gestore di autenticazione di validare il token fornito e restituire un token autenticato, se le credenziali fornite sono state riconosciute come valide. L’ascoltatore quindi dovrebbe memorizzare il token autenticato nel contesto di sicurezza:

use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class SomeAuthenticationListener implements ListenerInterface
{
    /**
     * @var SecurityContextInterface
     */
    private $securityContext;

    /**
     * @var AuthenticationManagerInterface
     */
    private $authenticationManager;

    /**
     * @var string Identifica univocamente l'area protetta
     */
    private $providerKey;

    // ...

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        $username = ...;
        $password = ...;

        $unauthenticatedToken = new UsernamePasswordToken(
            $username,
            $password,
            $this->providerKey
        );

        $authenticatedToken = $this
            ->authenticationManager
            ->authenticate($unauthenticatedToken);

        $this->securityContext->setToken($authenticatedToken);
    }
}

Nota

Un token può essere di qualsiasi classe, a patto che implementi Symfony\Component\Security\Core\Authentication\Token\TokenInterface.

Il gestore di autenticazione

Il gestore di autenticazione predefinito è un’istanza di Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager:

use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;

// instanze di Symfony\Component\Security\Core\Authentication\AuthenticationProviderInterface
$providers = array(...);

$authenticationManager = new AuthenticationProviderManager($providers);

try {
    $authenticatedToken = $authenticationManager
        ->authenticate($unauthenticatedToken);
} catch (AuthenticationException $failed) {
    // autenticazione fallita
}

AuthenticationProviderManager, quando istanziata, riceve vari fornitori di autenticazione, ciascuno che supporta un diverso tipo di token.

Nota

Ovviamente, si può scrivere un proprio gestore di autenticazione, basta che implementi Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface.

Fornitori di autenticazione

Ogni fornitore (poiché implementa Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface) ha un metodo supports() da cui AuthenticationProviderManager può determinare se supporti il dato token. Se questo è il caso, il gestore richiama il metodo Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface::authenticate del fornitore. Tale metodo dovrebbe restituire un token autenticato o lanciare una Symfony\Component\Security\Core\Exception\AuthenticationException (o un’eccezione che la estenda).

Autenticare utenti con nome e password

Un fornitore di autenticazione proverà ad autenticare un utente in base alle credenziali fornite. Solitamente queste sono un nome utente e una password. La maggior parte delle applicazioni web memorizzano i nomi utente e un hash delle password combinate con un sale generato casualmente. Ciò vuol dire che l’autenticazione media consiste nel recuperare il sale e l’hash della password dal sistema di memorizzazione dei dati degli utenti, trasformare in hash la password appena fornita dall’utente (p.e. in un form di login) con il sale e confrontare entrambi, per determinare se la password fornita sia valida.

Tale funzionalità è offerta da Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider. Questa classe recupera i dati dell’utente da un Symfony\Component\Security\Core\User\UserProviderInterface`, usa un Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface per creare un hash della password e restituisce un token autenticato, se la password è valida:

use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\User\UserChecker;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;

$userProvider = new InMemoryUserProvider(
    array(
        'admin' => array(
            // la password è "foo"
            'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
            'roles'    => array('ROLE_ADMIN'),
        ),
    )
);

// per alcuni controlli ulteriori: account abilitato, bloccato, scaduto, ecc.?
$userChecker = new UserChecker();

// un array di codificatori di password (vedere più avanti)
$encoderFactory = new EncoderFactory(...);

$provider = new DaoAuthenticationProvider(
    $userProvider,
    $userChecker,
    'secured_area',
    $encoderFactory
);

$provider->authenticate($unauthenticatedToken);

Nota

L’esempio sopra dimostra l’uso di un fornitore “in-memory” (in memoria), ma si può usare qualsiasi fornitore di utente, purché implementi Symfony\Component\Security\Core\User\UserProviderInterface. È anche possibile far cercare i dati dell’utente a più di un fornitore di utenti, usando Symfony\Component\Security\Core\User\ChainUserProvider.

Il factory codificatore di password

Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider usa un factory codificatore per creare un codificatore di password per un dato tipo di utente. Questo consente di usare diverse strategie di codifica per diversi tipi di utenti. La classe predefinita Symfony\Component\Security\Core\Encoder\EncoderFactory riceve un array di codificatori:

use Symfony\Component\Security\Core\Encoder\EncoderFactory;
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;

$defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000);
$weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1);

$encoders = array(
    'Symfony\\Component\\Security\\Core\\User\\User' => $defaultEncoder,
    'Acme\\Entity\\LegacyUser'                       => $weakEncoder,

    // ...
);

$encoderFactory = new EncoderFactory($encoders);

Ogni codificatore deve implementare Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface o essere un array con chiavi class e arguments, che consente al factory codificatore di costruire il codificatore solo quando necessario.

Creare un codificatore di password

Ci sono molti codificatori di password predefiniti. Se si ha l’esigenza di crearne uno nuovo, basta seguire le seguenti tre regole:

  1. La classe deve implementare Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;

  2. Le implementazioni di encodePassword() e isPasswordValid() devono innanzitutto assicurarsi che la password non sia troppo lunga, vale a dire che la password non superi i 4096 caratteri. Questo per motivi di sicurezza (vedere CVE-2013-5750). Si può usare il metodo isPasswordTooLong() per eseguire questo controllo:

    use Symfony\Component\Security\Core\Exception\BadCredentialsException;
    
    class FoobarEncoder extends BasePasswordEncoder
    {
        public function encodePassword($raw, $salt)
        {
            if ($this->isPasswordTooLong($raw)) {
                throw new BadCredentialsException('Invalid password.');
            }
    
            // ...
        }
    
        public function isPasswordValid($encoded, $raw, $salt)
        {
            if ($this->isPasswordTooLong($raw)) {
                return false;
            }
    
            // ...
    }
    

Usare codificatori di password

Quando il metodo getEncoder() del factory codificatore di password viene richiamato con l’oggetto utente come primo parametro, restituirà un codificatore di tipo Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface, che va usato per codificare la password dell’utente:

// recupera un utente di tipo Acme\Entity\LegacyUser
$user = ...

// la password immessa, p.e. durante una registrazione
$plainPassword = ...;

$encoder = $encoderFactory->getEncoder($user);

// restituirà $weakEncoder (vedere sopra)
$encodedPassword = $encoder->encodePassword($plainPassword, $user->getSalt());

$user->setPassword($encodedPassword);

// ... salvare l'utente

Se ora si vuole verificare se la password fornita (p.e. durante un login) sia corretta, si può usare:

// recupera Acme\Entity\LegacyUser
$user = ...;

// la password immessa, p.e. da un form di login
$plainPassword = ...;

$validPassword = $encoder->isPasswordValid(
    $user->getPassword(), // la password codificata
    $plainPassword,       // la password fornita
    $user->getSalt()
);