Symfony Security Component as a Standalone (Part 2)

Saving security information in the session

Amir Modarresi
5 min readDec 20, 2019

In the previous story, I had discussed defining a Firewall to authenticate users. I wrote a simple PHP framework that gets username and password from the query string and after validating the user’s credentials it authenticates users only in the same request.

*** You need to read the first part before reading this part ***

In a real application, users must be authenticated in all requests until they logged out. To do this, the application must save users’ information in the session.

To do that we must do these steps in our simple PHP framework :

  • Reads the security token from the session on kernel.request event
  • Writes the security token into the session on kernel.response event

Fortunately, Symfony\Component\Security\Http\Firewall\ContextListener does these two operations for us. This service is a Symfony authentication listener and implements Symfony\Component\Security\Http\Firewall\ListenerInterface . So it will fetch user information from the request (For the case of ContextListener it will fetch this information from the request’s session) and then create a security token and set that to the token storage. On the other hand, ContextListener is a listener for the kernel response event. It will trigger at the end of every request to get the security token from token storage and save that to the session. So we can say, fetching and saving security token from and to the session is being done by ContextListener and that is the main job of ContextListener .

So we need an instance of ContextListener class. It has some mandatory dependencies that described below:

  • An instance of TokenStorageInterface
  • An array of user providers that keeps at least one instance of Symfony\Component\Security\Core\User\UserProviderInterface
  • A string that contains the name of the security context
  • An instance of EventDispatcherInterface

We have all of above dependencies in our Kernel class except for the second one, UserProviderInterface. Implementations of this interface are responsible for loading user objects from some source for the authentication system. For example, fetching user objects from a database or from a LDAP server or every other source. Symfony has some built-in implementations of UserProviderInterface:

  • EntityUserProvider that fetch users from Doctrine repositories
  • InMemoryUserProvider that stores all user information in a configuration file, including their passwords and load user objects from those configurations
  • LdapUserProvider that fetch user objects from an LDAP server
  • ChainUserProvider that combines two or more of the other provider types (entity, memory, and LDAP) to create a new user provider

The ContextListener needs a user provider to refresh the fetched user object from the session to make sure our application has a fresh version of the user object.

Remember that we hadSimpleAuthenticationProvider from the previous part to do authentication over the security token. In that class, we had created the user object manually and haven’t used an instance of UserProviderInterface. Now we can have a user provider object to use in both SimpleAuthenticationProvider and ContextListener.

I am going to use InMemoryUserProvider in this part, but I will describe how to fetch users from a database in the next parts. To create an InMemoryUserProvider add getUserProvider method to the Kernel class:

use Symfony\Component\Security\Core\User\InMemoryUserProvider;...protected function getUserProvider()
{
return new InMemoryUserProvider([
'amir' => [
'password' => 'foo',
'roles' => ['ROLE_USER'],
]
]);
}

and then call getUserProvider method in the handle method of the Kernel class and set that to a variable with the name $userProvider and then pass that variable to the constructor of the SimpleAuthenticationProvider:

class Kernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
$userProvider = $this->getUserProvider();

$tokenStorage = new TokenStorage();
$authenticationManager = new AuthenticationProviderManager([new SimpleAuthenticationProvider($userProvider)]);
// ...
}

// ...
}

Of course, we need to change our SimpleAuthenticationProvider to use a user provider instead of directly create a user object. Change the SimpleAuthenticationProvider class as below:

<?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;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class SimpleAuthenticationProvider implements AuthenticationProviderInterface
{
/**
*
@var UserProviderInterface
*/
private $userProvider;

public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}

public function authenticate(TokenInterface $token)
{
$username = $token->getUsername();
$password = $token->getCredentials();

$user = $this->userProvider->loadUserByUsername($username);

if ($user && $user->getPassword() === $password) {
$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();
}
}

Now I need an instance of ContextListener to add to the $listeners array to be used in the firewall map that we had added to our firewall. Create a method with the name getContextListener as below:

protected function getContextListener(
TokenStorage $tokenStorage,
UserProviderInterface $userProvider,
EventDispatcherInterface $dispatcher,
$contextKey = 'main'
) {
return new ContextListener(
$tokenStorage,
array($userProvider),
$contextKey,
null,
$dispatcher
);
}

Note that we need to pass an event dispatcher to the constructor of the ContextListener class. In the previous part, we had created an instance of EventDispatcher just before creating an instance of Firewall. I move that line to some lines upper in the handle method. Finally, I call the getContextListener and pass all required objects to create a$contextListener object:

$eventDispatcher = new EventDispatcher();

$contextListener = $this->getContextListener($tokenStorage, $userProvider, $eventDispatcher);

Our request object is not supporting session. In order for being able to handle the session, we need to set a session factory on the request object:

$request->setSessionFactory(function () {
return new Session();
});

To add $contextListener object to the firewall add that to the $listeners array:

$listeners = [$contextListener, $simpleAuthenticationListener];

As the final step, we need to trigger the kernel.response event when we are going to return the response object in the handle method. Because the handle method returns the response object in two places I create a method called filterResponse to do that:

protected function filterResponse(EventDispatcherInterface $dispatcher, Response $response, Request $request, int $type)
{
$event = new FilterResponseEvent($this, $request, $type, $response);

$dispatcher->dispatch(KernelEvents::RESPONSE, $event);

return $event->getResponse();
}

and call that method whenever we are going to return a response object:

if ($getResponseEvent->hasResponse()) {
$response = $getResponseEvent->getResponse();

return $this->filterResponse($eventDispatcher, $response, $request, $type);
}
...$response = new Response(sprintf('Greeting %s', $user));

return $this->filterResponse($eventDispatcher, $response, $request, $type);

The final version of Kernel class is as below:

<?php

namespace App;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
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\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Security\Http\Firewall\ContextListener;
use Symfony\Component\Security\Http\FirewallMap;

class Kernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
$userProvider = $this->getUserProvider();

$tokenStorage = new TokenStorage();

$authenticationManager = new AuthenticationProviderManager([new SimpleAuthenticationProvider($userProvider)]);

$eventDispatcher = new EventDispatcher();

$request->setSessionFactory(function () {
return new Session();
});

$contextListener = $this->getContextListener($tokenStorage, $userProvider, $eventDispatcher);

$simpleAuthenticationListener = new SimpleAuthenticationListener($tokenStorage, $authenticationManager, 'main');

$firewallMap = new FirewallMap();

$requestMatcher = new RequestMatcher('^/');
$listeners = [$contextListener, $simpleAuthenticationListener];
$firewallMap->add($requestMatcher, $listeners);

$firewall = new Firewall($firewallMap, $eventDispatcher);

$eventDispatcher->addSubscriber($firewall);

$getResponseEvent = new GetResponseEvent($this, $request, $type);
$eventDispatcher->dispatch(KernelEvents::REQUEST, $getResponseEvent);

if ($getResponseEvent->hasResponse()) {
$response = $getResponseEvent->getResponse();

return $this->filterResponse($eventDispatcher, $response, $request, $type);
}

if (($token = $tokenStorage->getToken()) && $token->getUser()) {
$user = $token->getUsername();
} else {
$user = 'anonymous';
}

$response = new Response(sprintf('Greeting %s', $user));

return $this->filterResponse($eventDispatcher, $response, $request, $type);
}

private function filterResponse(EventDispatcherInterface $dispatcher, Response $response, Request $request, int $type)
{
$event = new FilterResponseEvent($this, $request, $type, $response);

$dispatcher->dispatch(KernelEvents::RESPONSE, $event);

return $event->getResponse();
}

private function getUserProvider()
{
return new InMemoryUserProvider([
'amir' => [
'password' => 'foo',
'roles' => ['ROLE_USER'],
]
]);
}

private function getContextListener(
TokenStorage $tokenStorage,
UserProviderInterface $userProvider,
EventDispatcherInterface $dispatcher,
$contextKey = 'main'
) {
return new ContextListener(
$tokenStorage,
array($userProvider),
$contextKey,
null,
$dispatcher
);
}
}

Now if we open this url:

http://localhost:8000?username=amir&password=foo

And user will be authenticated in further requests.

I’ll describe fetching user from database in next part.

--

--