Symfony Security Component as a Standalone (Part 2)
Saving security information in the session
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 repositoriesInMemoryUserProvider
that stores all user information in a configuration file, including their passwords and load user objects from those configurationsLdapUserProvider
that fetch user objects from an LDAP serverChainUserProvider
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.