Symfony2: Security enhancements part II
Matthias Noback
Part II of this series is all about validating the user’s session. You can find Part I here, if you missed it.
Collect Failed Authentication Attempts
Now and then a user will forget his password and try a few times before going to the “reset password” page. However, when a “user” keeps trying to authenticate with bad credentials, you may be subject to a brute-force attack. Therefore, you should collect failed authentication attempts. Your strategy may then be to block the account until further notice, while providing the user with a way to re-activate his account. When authentication fails, an event is fired, which you may intercept by registering an event listener or subscriber:
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
class AuthenticationFailureListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure',
);
}
public function onAuthenticationFailure(AuthenticationFailureEvent $event)
{
$token = $event->getAuthenticationToken();
$username = $token->getUsername();
// ...
}
}
The corresponding service definition:
<service id="matthias_security.authentication_failure_event_listener"
class="Matthias\ApplicationBundle\EventListener\AuthenticationFailureListener">
<tag name="kernel.event_subscriber" />
</service>
You may also want to listen to the AuthenticationEvents::AUTHENTICATION_SUCCESS
event to reset the failed attempts counter.
Block Users
When accounts are somehow suspicious (for instance you encountered an unusual number of login attempts) you may want to block the account and prevent the user from authenticating successfully. This is the purpose of the UserChecker from the Security Component. Normally it does some checks on the user object, but only if it implements AdvancedUserInterface
. You may extend it yourself, and do some “pre-checks” (before verifying the password) or “post-checks” (after verifying the password - this means the password is found to be valid).
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserChecker;
class MyUserChecker extends UserChecker
{
public function checkPreAuth(UserInterface $user)
{
// check anything
parent::checkPreAuth($user);
}
public function checkPostAuth(UserInterface $user)
{
// check anything
parent::checkPostAuth($user);
}
}
When you want to prevent the given user from logging in: throw an exception which is an instance of Symfony\Component\Security\Core\Exception\AccountStatusException
.
You should probably replace the existing security.user_checker
service or set the parameter security.user_checker.class
on the service container (at compile time).
Check Your PHP Settings
You should read everything there is about PHP sessions and (session) cookies. I thought I knew everything I needed to know, but there were some important distinctions I was not aware of, for instance between the expiry date of session cookies, the lifetime of a session and the period in which sessions are stale, but not yet garbage collected. Please check out the Runtime Configuration for Sessions.
Some settings can be changed by modifying config.yml
(see the Framework configuration reference), and some may require you to modify your php.ini
file.
Invalidate the Session Based on its Age
When the session cookie’s lifetime is “0”, which means it lives until the browser session ends, it could be wise to kill sessions after a number of inactive minutes anyway. This can be accomplished by inspecting the so-called MetadataBag
of the Session
.
Listen to the kernel.request
event and check if the session is still valid. The priority of the listener should be lower than 128.
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class SessionListener
{
public function onKernelRequest(GetResponseEvent $event)
{
if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST) {
return;
}
$session = $event->getRequest()->getSession();
$metadataBag = $session->getMetadataBag();
$lastUsed = $metadataBag->getLastUsed();
if ($lastUsed === null) {
// the session was created just now
return;
}
// "last used" is a Unix timestamp
// if a session is being revived after too many seconds:
$session->invalidate();
}
}
And the corresponding service definition:
<service id="matthias_security.verify_session_listener"
class="Matthias\ApplicationBundle\EventListener\SessionListener">
<tag name="kernel.event_listener"
event="kernel.request"
priority="100"
method="onKernelRequest" />
</service>