Skip to main content

Adding post-login logic to FOSUserBundle

Having finally figured out how to use FOSUserBundle in my project, I decided to keep track of all logins next. The implementation turned out to be a breeze thanks to Symfony2's security listener mechanism.

As usual, the first step is to create a MongoDB document for this purpose. This is a very simple document that contains a user's id, session id, IP address, and login date.

src/Acme/UserBundle/Document/LoginHistory.php

namespace Acme\UserBundle\Document;

class LoginHistory
{
    protected $id;
    protected $userId;
    protected $sessionId;
    protected $ip;
    protected $createdAt;
   
    /**
     * Get id
     *
     * @return custom_id $id
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set userId
     *
     * @param int $userId
     */
    public function setUserId($userId)
    {
        $this->userId = $userId;
    }

    /**
     * Get userId
     *
     * @return int $userId
     */
    public function getUserId()
    {
        return $this->userId;
    }

    /**
     * Set sessionId
     *
     * @param string $sessionId
     */
    public function setSessionId($sessionId)
    {
        $this->sessionId = $sessionId;
    }

    /**
     * Get sessionId
     *
     * @return string $sessionId
     */
    public function getSessionId()
    {
        return $this->sessionId;
    }

    /**
     * Set ip
     *
     * @param string $ip
     */
    public function setIp($ip)
    {
        $this->ip = $ip;
    }

    /**
     * Get ip
     *
     * @return string $ip
     */
    public function getIp()
    {
        return $this->ip;
    }

    /**
     * Set createdAt
     *
     * @param date $createdAt
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;
    }

    /**
     * Get createdAt
     *
     * @return date $createdAt
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    
    public function callbackPrePersist()
    {
        $this->setCreatedAt(new \DateTime());
    }
}

Second step is to map this document to a MongoDB collection:

src/Acme/UserBundle/Resources/config/doctrine/LoginHistory.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\UserBundle\Document\LoginHistory" db="acme" collection="loginHistory" customId="true">
        
        <field fieldName="id" id="true" strategy="INCREMENT" />
        <field fieldName="userId" type="int" />
        <field fieldName="sessionId" type="string" />
        <field fieldName="ip" type="string" />
        <field fieldName="createdAt" type="date" />
        
        <lifecycle-callbacks>
            <lifecycle-callback method="callbackPrePersist" type="prePersist" />
        </lifecycle-callbacks>
        
        <indexes>
            <index>
                <key name="userId" order="asc" />
            </index>
            <index>
                <key name="createdAt" order="desc" />
            </index>
        </indexes>
        
    </document>
    
</doctrine-mongo-mapping>

Next step is to create a new listener to do the work for us when a user successfully logs in to the application:

src/Acme/UserBundle/Resources/Core/Listenere/InteractiveLogin.php

namespace Acme\UserBundle\Core\Listener;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use DateTime;
use Acme\UserBundle\Document\LoginHistory;

class InteractiveLogin
{
    protected $container;
    protected $dm;

    public function __construct(ContainerInterface $container, DocumentManager $dm)
    {
        $this->container = $container;
        $this->dm = $dm;
    }
    
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if ($user instanceof UserInterface) {
            
            $loginHistory = new LoginHistory();
            $loginHistory->setUserId($user->getId());
            $loginHistory->setSessionId($this->container->get('session')->getId());
            $loginHistory->setIp($this->container->get('request')->getClientIp());
            
            $this->dm->persist($loginHistory);
            $this->dm->flush();
            
        }
    }
}

The last step is to attach this listener to Symfony's security.interactive_login event:

src/Acme/UserBundle/Resources/config/doctrine/LoginHistory.mongodb.xml

<service id="acme.security.login_listener" class="Acme\UserBundle\Core\Listener\InteractiveLogin">
    <argument type="service" id="service_container" />
    <argument type="service" id="fos_user.document_manager" />
    <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
</service>

That's it!

Comments

  1. hi,
    do you have adapt this method for mysql ?
    thanks

    ReplyDelete
    Replies
    1. You just have to convert the Document to an (ORM) Entity and you are done

      Delete
  2. This is awesome. Thanks for sharing.

    ReplyDelete
  3. Thanks for this. Here's some info that might be useful to others. Maybe it's obvious but it took me some digging.

    I found this and your previous article about how FOSUserBundle works when I was looking for a way to add extra login criteria in addition to the user name and password match. Lets say I only want to allow people to login at a certain time of day or whatever. I thought I'd have to write my own provider or something but I eventually discovered that it's quite simple.

    In onSecurityInteractiveLogin above, you can cause the login to fail nicely by throwing an appropriate exception. If you throw a Symfony\Component\Security\Core\Exception\BadCredentialsException("My Message") then the login form return with "My Message" displayed just as if the password was wrong.

    ReplyDelete
  4. Hey. Thanks a lot for this. I have now some idea how this works, but i still dont get how works logout process. Can you also explain this.

    ReplyDelete
  5. Why injecting the whole Container ?
    inside the onSecurityInteractiveLogin(InteractiveLoginEvent $event) you can do

    $event->getRequest() for the request and inject only the session service

    ReplyDelete

Post a Comment

Popular posts from this blog

Securing Symfony2 REST services with FOSOAuthServerBundle

Overview In my previous article, I wrote about setting up a Symfony2 REST service using FOSRestBundle. However, this REST service was behind a firewall protected by a generic form_login provider. Not really ideal if you wish to open your REST API to other applications. So in this article, I will try to explain how to set up FOSOAuthServerBundle to protect your REST API methods using OAuth2. Before we start getting into the gritty details, it is a good idea to have a look at the official OAuth2 documentation . Let's begin... FOSOAuthServerBundle Installation You have to install v1.1.0 of FOSOAuthServerBundle if you are using Symfony 2.0.x. If not, see the docs . First, add the following entries to your deps file: [FOSOAuthServerBundle] git=git://github.com/FriendsOfSymfony/FOSOAuthServerBundle.git target=bundles/FOS/OAuthServerBundle version=origin/1.1.x [oauth2-php] git=git://github.com/FriendsOfSymfony/oauth2-php.git Run the vendors script to install these...

Unexpected token "name" of value "if" ("end of statement block" expected) in "WebProfilerBundle:Collector:logger.html.twig"

Encountered this WebProfilerBundle error message when I ran the bin/vendors script to update my Symfony2 bundles. Make sure your deps file is up to date; you need to pay special attention to your version values. In this case, update your twig version to v1.2.0 as illustrated below: [twig] git=http://github.com/fabpot/Twig.git version=v1.2.0 Run the vendors script to update your bundle and the error message should disappear. You can get the most up to date deps file from the symfony-standard repository located at: https://github.com/symfony/symfony-standard/blob/master/deps

A Parcelable Tutorial for Android

Parcelable Interface Overview In one of my earlier posts, I mentioned writing an article about FOSOAuthBundle integration with an Android client. To keep that article to the point, I need to explain some concepts beforehand. One of the important concepts is the Android Parcelable interface that allows data to be transferred between different processes/threads. Certain network operations with Android such as authentication with OAuth2 and then fetching data from a REST endpoint should be performed in the background in order not to block the UI thread. This requires data to be fetched by a service (I have opted for Intent Services in my implementation) in the background and then passed back to the calling activity/fragment with a result callback. This is where the Parcelable interface comes into play. Basically, the Parcelable interface allows your classes to be flattened inside a message container called a Parcel to facilitate high performance inter process communication. The rece...