Logging failed log-in attempts with Symfony2

Sometimes you have to log failed attempts to access your site – here’s how I do it;

Create a FailedLogin entity with fields to hold the username that was used to attempt to log in, together with the IP address. Obviously, we don’t want to store the password that was used.

src/AppBundle/Entity/FailedLogin.php

<?php
/**
 * Failed Login
 *
 * @package AppBundle\Entity
 */

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\Traits\TimestampableTrait;

/**
 * Failed Login
 *
 * entity to track failed log-in attempts
 *
 * @author Andrew Battye
 *
 * @ORM\Entity()
 * @ORM\Table(name="failed_login")
 */
class FailedLogin implements ManagedEntityInterface
{
    use TimestampableTrait;

    /**
     * the id.
     *
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * username when trying to log in
     *
     * @var string
     *
     * @ORM\Column(name="username", type="string", length=255)
     */
    protected $username;

    /**
     * ip address which they tried logging in from
     *
     * @var int
     *
     * @ORM\Column(name="ip_address", type="integer", options={"unsigned"=true})
     */
    protected $ipAddress;

    /**
     * FailedLogin constructor
     *
     * just log the created datetime
     */
    public function __construct()
    {
        $this->created = new \DateTime();
        $this->updated = new \DateTime();
    }

    /**
     * set the username
     *
     * @param string $name
     *
     * @return $this
     */
    public function setUsername($name)
    {
        $this->username = $name;

        return $this;
    }

    /**
     * get username
     *
     * @return string
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * set the IP address - converts to long int before
     * persisting
     *
     * @param string $addr
     *
     * @return $this
     */
    public function setIPAddress($addr)
    {
        $this->ipAddress = ip2long($addr);

        return $this;
    }

    /**
     * get ip address
     *
     * @return string
     */
    public function getIPAddress()
    {
        return long2ip($this->ipAddress);
    }

    /**
     * get the id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }
}

This is standard Doctrine entity, the only thing to note is the conversion of a dotted string representation of an IP address to an unsigned 32-bit integer for storage in the database, and vice versa. I’m using the long2ip and ip2long functions here.

Then, we need an EventListener that listens to Symfony’s Authentication Failure event;

src/App/EventListener/AuthenticationListener.php

<?php
/**
 * Authentication Failure Listener
 *
 * @package AppBundle\EventListener
 */

namespace AppBundle\EventListener;

use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\DomainManager\FailedLoginAttemptManager;

/**
 * Authentication Failure Listener
 *
 * event listener that leaps into action whenever an authentication failure event
 * is triggered
 *
 * @author Andrew Battye
 *
 */
class AuthenticationFailureListener implements EventSubscriberInterface
{
    /**
     * the request object
     *
     * @var Request
     */
    protected $request;

    /**
     * the domain manager for FailedLogin entities
     *
     * @var FailedLoginAttemptManager
     */
    protected $manager;

    /**
     * constructor
     *
     * inject the Domain Manager
     *
     * @param FailedLoginAttemptManager $manager
     */
    public function __construct(FailedLoginAttemptManager $manager)
    {
        $this->manager = $manager;
    }

    /**
     * get Subscribed Events
     *
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure',
        ];
    }

    /**
     * injects the Request object
     *
     * @param RequestStack $requestStack
     */
    public function setRequest(RequestStack $requestStack)
    {
        $this->request = $requestStack->getCurrentRequest();
    }

    /**
     * on Authentication Failure
     *
     * what happens when a failed login is detected
     *
     * @param AuthenticationFailureEvent $event
     */
    public function onAuthenticationFailure(AuthenticationFailureEvent $event)
    {
        // create a new FailedLoginAttempt
        $attempt = $this->manager->getNewEntity();

        // get username that was entered
        $token = $event->getAuthenticationToken();
        $username = $token->getUsername();

        // get the ip address from the request
        $ipAddress = $this->request->getClientIp();

        // set the properties, and persist and flush
        $attempt->setUsername($username);
        $attempt->setIPAddress($ipAddress);

        $this->manager->save($attempt);
    }
}


You can see that this listener, on the event being triggered, gets the authentication token from the event, and then username that was supplied to the login form from that token. Getting the IP address is a little bit fiddly – it requires the current RequestStack to be injected into the listener – see below. After that, we just use the FailedLogin’s domain manager (which is simply a way of building new FailedLogin entities and of accessing the Doctrine Entity Manager to persist them) to get a new FailedLogin, set its details and persist it.

To get the RequestStack into the EventListener, we use the “calls” key in the services definition file;

src/AppBundle/Resources/config/services.yml

app.event_listener.authentication_failure_listener:
    class: AppBundle\EventListener\AuthenticationFailureListener
    arguments:
        - "@app.domain_manager.failed_login_manager"
    tags:
        - { name: kernel.event_subscriber }
    calls:
        - [setRequest, ["@request_stack"]]

Now, all failed login attempts are logged in database for further analysis.

Leave a Reply

Your email address will not be published. Required fields are marked *