Symfony Security Component as a Standalone (Part 1)

Amir Modarresi
Jun 22, 2018 · 6 min read

Symfony is a set of components that you can use in your own PHP application regardless of what framework you use. One of those great components is the Security component that provides authentication and authorization mechanism.

Symfony has good documentation for its security system that is related to the Symfony framework, but unfortunately, the documentation for the security component is not good enough for using that component as a standalone.

In this tutorial, I will describe how to use security component to authenticate your users with the Security component in a custom simple PHP framework.

For our simple framework we need to create a directory and then install dependencies via composer:

mkdir symfony-security-standalone

create a composer.json file with this content:

{
"require": {
"symfony/security": "^4.1"
},
"autoload": {
"psr-4": {
"App\\": "src"
}
}
}

and run:

composer install

Notice: You cat get the source code of this tutorial from GitHub.

First of all, we must have a Kernel class to handle a request and send a response to the client. For our Kernel, we use HttpKernelInterface that exists in symfony/http-kernel component. The symfony/http-kernel component is one of symfony/security’s dependencies so you don’t need to install it explicitly. Create a file named Kernel.php in src directory:

<?phpnamespace App;use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class Kernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
return new Response('Greeting');
}
}

and create an index.php file as our frameworks’ front controller:

<?phpuse App\Kernel;
use Symfony\Component\HttpFoundation\Request;
require_once __DIR__.'/vendor/autoload.php';$request = Request::createFromGlobals();$kernel = new Kernel();/** @var \Symfony\Component\HttpFoundation\Response $response */
$response = $kernel->handle($request);
$response->send();

Now we have a simple web application with two files:

  • index.php: Its job is creating a request object from PHP global variables, give the request object to Kernel for handling the request and get a response from that, finally send the response to the client
  • Kernel: The Kernel's job is getting request and return a response

Now its time to authenticate users. Every application may have multiple secured areas with different authentication and authorization mechanisms. For example, we may have an admin panel in our application that authenticates users via a login form and save user’s data in the session. The admin panel is a secured area. Also, we may have a restful API with JWT authentication. The API is another secured area. For handling authentication for these secured areas, we use Symfony’s firewall.

Authenticating a user is done by the Firewall. The firewall has a firewall map that defines secured areas and its authenticators. So before we create a firewall we must create firewall map and define our secured areas. Every secured area is defined by a RequestMatcher and an array of authentication listeners that implement ListenerInterface. Based on symfony’s documentation authentication listener’s job is as below:

When a request points to a secured area, and one of the listeners from the firewall map is able to extract the user’s credentials from the current Request object, it should create a token, containing these credentials. The next thing the listener should do is ask the authentication manager to validate the given token, and return an authenticated token if the supplied credentials were found to be valid. The listener should then store the authenticated token using the token storage

We need at least one authentication listener:

<?phpnamespace App;use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
class SimpleAuthenticationListener implements ListenerInterface
{
private $tokenStorage;
private $authenticationManager;private $providerKey;public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, string $providerKey) {
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->providerKey = $providerKey;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->query->has('username') && $request->query->has('password')) {
$username = $request->query->get('username');
$password = $request->query->get('password');
$unauthenticatedToken = new UsernamePasswordToken(
$username,
$password,
$this->providerKey
);
try {
$authenticatedToken = $this
->authenticationManager
->authenticate($unauthenticatedToken);
$this->tokenStorage->setToken($authenticatedToken);
} catch (AuthenticationException $exception) {
$response = new Response($exception->getMessage());
$event->setResponse($response);
}
}
}
}

Some keyword you may need to know:

  • Token: a token is an object that holds user authentication information. Every token class must implement TokenInterface. For example, an instance of UsernamePasswordToken contains the username and password that user is going to log in with.
  • Token storage: It gives access to the token representing the current user authentication.
  • Authentication manager: An authentication manager takes an unauthenticated token, validate that, and return an authenticated token if its information is valid, or throw an AuthenticationException if token information is not valid. Validating a token could be fetching user from user provider by username and checking user password. Of course, it depends on the token implementation.
  • Provider key: Provider key is a key for grouping authenticators. We specify a unique key for every record in our firewall map.

Now we can create a firewall and its dependencies in App\Kernel class:

<?phpnamespace App;use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Security\Http\FirewallMap;
class Kernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
$tokenStorage = new TokenStorage();
$authenticationManager = new AuthenticationProviderManager([]);
$simpleAuthenticationListener = new SimpleAuthenticationListener($tokenStorage, $authenticationManager, 'main');
$firewallMap = new FirewallMap();$requestMatcher = new RequestMatcher('^/');
$listeners = [$simpleAuthenticationListener];
$firewallMap->add($requestMatcher, $listeners);
$eventDispatcher = new EventDispatcher();
$firewall = new Firewall($firewallMap, $eventDispatcher);
return new Response('Greeting');
}
}

AuthenticationProviderManager gets an array of AuthenticationProviderInterface’s implementation in the first argument of its constructor. Let's define an implementation of AuthenticationProviderInterface:

<?phpnamespace App;use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\User;
class SimpleAuthenticationProvider implements AuthenticationProviderInterface
{
public function authenticate(TokenInterface $token)
{
$username = $token->getUsername();
$password = $token->getCredentials();
if ($username == 'amir' && $password == 'foo') {
$user = new User($username, $password, ['ROLE_USER']);
$authenticatedToken = new UsernamePasswordToken($user, $password, $token->getProviderKey(), $user->getRoles());

return $authenticatedToken;
}

throw new AuthenticationException("Username or password is invalid.");
}
public function supports(TokenInterface $token)
{
return $token instanceof UsernamePasswordToken && !$token->isAuthenticated();
}
}

We use SimpleAuthenticationProvider in our Kernel class:

<?phpnamespace App;use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Security\Http\FirewallMap;
class Kernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
$tokenStorage = new TokenStorage();
$authenticationManager = new AuthenticationProviderManager([new SimpleAuthenticationProvider()]);
$simpleAuthenticationListener = new SimpleAuthenticationListener($tokenStorage, $authenticationManager, 'main');
$firewallMap = new FirewallMap();$requestMatcher = new RequestMatcher('^/');
$listeners = [$simpleAuthenticationListener];
$firewallMap->add($requestMatcher, $listeners);
$eventDispatcher = new EventDispatcher();
$firewall = new Firewall($firewallMap, $eventDispatcher);
return new Response('Greeting');
}
}

It’s time to use the firewall. The firewall is an event subscriber so we must add it to event dispatcher then dispatch events that firewall is subscribing to.

<?phpnamespace App;use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Security\Http\FirewallMap;
class Kernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
$tokenStorage = new TokenStorage();
$authenticationManager = new AuthenticationProviderManager([new SimpleAuthenticationProvider()]);
$simpleAuthenticationListener = new SimpleAuthenticationListener($tokenStorage, $authenticationManager, 'main');
$firewallMap = new FirewallMap();$requestMatcher = new RequestMatcher('^/');
$listeners = [$simpleAuthenticationListener];
$firewallMap->add($requestMatcher, $listeners);
$eventDispatcher = new EventDispatcher();
$firewall = new Firewall($firewallMap, $eventDispatcher);
$eventDispatcher->addSubscriber($firewall);$getResponseEvent = new GetResponseEvent($this, $request, $type);
$eventDispatcher->dispatch(KernelEvents::REQUEST, $getResponseEvent);
if ($getResponseEvent->hasResponse()) {
return $getResponseEvent->getResponse();
}
if (($token = $tokenStorage->getToken()) && $token->getUser()) {
$user = $token->getUsername();
} else {
$user = 'anonymous';
}

return new Response(sprintf('Greeting %s', $user));
}
}

With above implementation, we only authenticate users by username and password from the query string, check that with hardcoded username and password and set token to token storage. Although that isn’t useful in real web application it helps you understand how Symfony firewall works. In next parts, I will describe how to save the token to the session and get that from the session in further requests, how to interact with a database as user provider, and how you can have multiple secured areas.