Symfony's custom login handler


Extra precious stuff missing

Posted by Pedro Resende on 12/03/2016 22:11

This week, while working for a client's task, I had to implement a custom user provider for Symfony which would validate the user from a webservice.

At first glance, this is a quite simple task, the only thing you really need to do is follow this tutorial. However, while implementing the solution when I had this

<?php

namespace AppBundle\Security\User;

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

class WebserviceUserProvider implements UserProviderInterface

{

    public function loadUserByUsername($username)
    {
        // make a call to your webservice here
        $userData = ...
        // pretend it returns an array on success, false if there is no user

        if ($userData) {
            $password = '...';
            // ...

            return new WebserviceUser($username, $password, $salt, $roles);
        }

        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );

    }

 

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'AppBundle\Security\User\WebserviceUser';
    }
}

I asked myself

"This looks wunderfull, however where the hell is the password entered by the user ?"

This is the perfect question, since the tutorial shows the example where the custom User Provider would authenticate with an external webservice but no information how to pass the user password to that service.

That's when, after diging into the container and the request I've manage to get to this solution 

<?php

namespace AppBundle\Security\User;

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

class WebserviceUserProvider implements UserProviderInterface
{
    private $container;
    private $logger;

    public function __construct($container, $logger)
    {
        $this->container = $container;
        $this->logger = $logger;
    }
 
    public function loadUserByUsername($username)
    {
        $request = $this->container->get('request');
        $password = $request->request->get('_password'); //User Password
        
        // make a call to your webservice here
        $userData = ...
        // pretend it returns an array on success, false if there is no user
 
        if ($userData) {
            $password = '...';
 
            // ...
 
            return new WebserviceUser($username, $password, $salt, $roles);
        }
 
        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }
 
    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }
        return $this->loadUserByUsername($user->getUsername());
    }

 
    public function supportsClass($class)
    {
        return $class === 'AppBundle\Security\User\WebserviceUser';
    }
}

The service will be declared has

    custom.user_provider:
        class: AppBundle\Security\User
        arguments: [ @service_container, @logger ]

The logger is not needed, however I'm using it to log the authentication