How to build a custom Symfony Security Authentication on Silex 2

Symfony Security Component is a very complex/flexible system, there are a lot of concepts with a lot of features and require some time to figure out it's workflow. 

  • First of all the Security is designed as a firewall, each Request.route is matched against Firewall.patterns
  • The Firewall is no more than a Listener that waits for requests (onKernelRequest) 
  • All Requests are analised and if there is a Firewall.pattern who match the Request.route then an Security.Context is generated with the defined rules.
  • The Firewall rules define the Security workflow actions that are taken before Request main goal is reached.
  • Each Firewall.pattern define its own Security.Context.
  • Each Security.Context  is defined for one or more Authentication.Listeners and one or More Authentication.Providers
  • The Authentication.Listeners when dispatched they try Authenticate the Token
  • The Authentication.Providers when called by the Authentication.Listeners and case the Token is supported then they try Authenticate the Token against to the Users list provided by the UserProvider.

 

# Custom Symfony Security Authentication and Silex 2
# Register Security Service and define Firewalls

$app->register(new SecurityServiceProvider());
$app['security.firewalls'] = array(
'myhttpsecured' => array(
            'pattern' => '/howto-security/case1/admin/*',
            'myhttp' => true,
        ),
)

# Define Custom Autentication Listener and Custom Authentication Provider

$app['security.authentication_listener.factory.myhttp'] = $app->protect(function ($name, $options) use ($app) {
    // define the authentication provider object
    $app['security.authentication_provider.'.$name.'.myhttp'] = function () use ($app) {
        return new ArrayAuthenticationProvider(array('admin2' => array('password' => 'adminpasswd', 'roles' => array('ROLE_ADMIN'))));
    };

    // define the authentication listener object
    $app['security.authentication_listener.'.$name.'.myhttp'] = function () use ($app) {
        return new HttpBasicAuthenticationListener($app['security.authentication_manager'], $app['security.token_storage']);
    };

    return array(
        // the authentication provider id
        'security.authentication_provider.'.$name.'.myhttp',
        // the authentication listener id
        'security.authentication_listener.'.$name.'.myhttp',
        // the entry point id
        null,
        // the position of the listener in the stack
        'pre_auth'
    );
});

# HttpBasicAuthenticationListener

<?php

namespace app\SimpleSecurity\AuthenticationListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpFoundation\Response;
use app\SimpleSecurity\Token\LoginPasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use app\SimpleSecurity\AuthenticationEntryPoint\HttpBasicAuthenticationEntryPoint;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class HttpBasicAuthenticationListener implements ListenerInterface
{
    private $authenticationManager;
    private $tokenStorage;
    private $authenticationEntryPoint;

    public function __construct(AuthenticationManagerInterface $authenticationManager, TokenStorageInterface $tokenStorage)
    {
        $this->authenticationManager = $authenticationManager;
        $this->tokenStorage = $tokenStorage;
        $this->authenticationEntryPoint = new HttpBasicAuthenticationEntryPoint();
    }

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

        if (false === $username = $request->headers->get('PHP_AUTH_USER', false)) {
            $event->setResponse($this->authenticationEntryPoint->start($request));
            return;
        }

        if (null !== $token = $this->tokenStorage->getToken()) {
            if ($token->isAuthenticated() && $token->getUsername() === $username) {
                return;
            }
        }
       

        // We retrieve info required to authenticate current user from request and encapsulate them into a Token.
        $token = new LoginPasswordToken($request->headers->get('PHP_AUTH_USER'), $request->headers->get('PHP_AUTH_PW'));

        try {
            $authenticatedToken = $this->authenticationManager->authenticate($token);
            $this->tokenStorage->setToken($authenticatedToken);
            //
        } catch (AuthenticationException $e) {
            $event->setResponse($this->authenticationEntryPoint->start($request, $e));
        }
    }
}

# ArrayAuthenticationProvider

<?php

namespace app\SimpleSecurity\AuthenticationProvider;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use app\SimpleSecurity\Token\LoginPasswordToken;

/**
* This authentication provider can authenticate ONLY a LoginPasswordToken
*/
class ArrayAuthenticationProvider implements AuthenticationProviderInterface
{
    private $users;

    public function __construct(array $users)
    {
        $this->users = $users;
    }

    public function authenticate(TokenInterface $token)
    {
        foreach ($this->users as $username => $info) {
            if ($username === $token->getUsername() && $info['password'] === $token->getCredentials()) {
                return new LoginPasswordToken($username, $info['password'], $info['roles']); // authenticated token
            }
        }
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof LoginPasswordToken;
    }
}

# HttpBasicAuthenticationEntryPoint 
 

<?php

namespace app\SimpleSecurity\AuthenticationEntryPoint;

use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

/**
* Goal: If the current user is not identified "an HTTP login box should appear".
*/
class HttpBasicAuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new Response(null, 401, array('WWW-Authenticate' => 'Basic realm="You are accessing a restricted area"'));
    }
}

# LoginPasswordToken

<?php

namespace app\SimpleSecurity\Token;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use app\SimpleSecurity\User\Entity\User as UserEntity;

class LoginPasswordToken extends AbstractToken
{
    public function __construct($login, $password, array $roles = array())
    {
        parent::__construct($roles);
        parent::setAuthenticated(count($roles) > 0); // this avoid to mark a token authenticated in AuthenticationListener
        $this->setUser(new UserEntity(array('username' => $login, 'password' => $password, 'roles' => $roles )));
        $this->credentials = $password;
    }

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

## References:
* http://silex.sensiolabs.org/doc/master/providers/security.html
* http://symfony.com/doc/current/cookbook/security/index.html
* https://github.com/silexphp/Silex/blob/master/doc/providers/security.rst
* http://symfony.com/doc/current/cookbook/security/custom_authentication_p...
* https://github.com/quazardous/silex-user-pack
* http://www.jasongrimes.org/2014/09/simple-user-management-in-silex/
* http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements
* https://www.youtube.com/watch?v=C1y6fxetP5k
* https://speakerdeck.com/rouffj/mastering-the-security-components-authent...
* https://www.youtube.com/watch?v=xQyEXzug7P8
* http://www.slideshare.net/kriswallsmith/love-and-loss-a-symfony-security...