Symfony Security Component as a Standalone (Part 1)

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

Symfony has good documentation for its security system that is related to 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 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 these content:

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

and run:

composer install

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:

<?php

namespace
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:

<?php

use
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 user. 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 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 Firewall. Firewall has a firewall map that defines secured areas and its authenticators. So before we create 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:

<?php

namespace
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: 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 firewall and its dependencies in App\Kernel class:

<?php

namespace
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 first argument of its constructor. Lets define an implmentation of AuthenticationProviderInterface:

<?php

namespace
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:

<?php

namespace
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.

<?php

namespace
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 user by username and password from query string, check that with hardcoded username and password and set token to token storage. Although that isn’t useful in real web application but 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 database as user provider, and how you can have multiple secured areas.

Like what you read? Give Amir Modarresi a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.