Custom MongoDB Security Provider in Symfony2

At work, we’ve decided to upgrade our entire workflow. One of the main decisions we’ve had to make, was which PHP framework we wanted to start using. I have quite some experience with Zend Framework, so I was kinda biased towards that framework. However, the team didn’t want to create a new codebase on ZF1. Since waiting till ZF2 is released was not an option, we decided to give Symfony2 a try.

To each build up some experience with SF2, each team member was tasked to create a blog system with SF2. I decided to use MongoDB as my preferred method of data persistence, since the rest of the team also showed quite some interest in using this technology. Following the documentation on the Symfony2 website, I quickly set up my BlogBundle, with an administration interface and everything. The next step was to secure the backoffice with user credentials coming from a MongoDB collection. This is when a whole world of hurt opened up: almost no information on the intertubes on how to do this. I figured out I would need a custom UserInterface and UserProviderInterface. People kept referring to the FOSUserBundle. The Friends of Symfony are a cool bunch, but I wanted to write my own code, as part of the learning process. A couple of hours and an almost-headache later, I had figured it out. Since there will probably be other people struggling with this, here’s how I did it.

Setting up the Security configuration

When you want to create parts of an application which need to be accessed by logged in users, you need to create firewall rules for this. Here’s my configuration.
File: /app/config/security.yml

security:
    encoders:
      MediaMates\BlogBundle\Document\User: md5

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        default:
          id: mm_security_provider

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
          pattern: ^/blog/backoffice/login$
          anonymous: ~
          security: false

        secured_area:
            pattern:    ^/
            anonymous: ~
            logout:
              path: /blog/backoffice/logout
              target: /blog
            form_login:
                login_path: /blog/backoffice/login
                check_path: /blog/backoffice/login_check

    access_control:
        - { path: ^/blog/backoffice, roles: ROLE_ADMIN}

Important here is the “providers” part. As you can see, I specified a provider named “default”. I want to use a custom provider, and I refer to it in the services via its ID. The ID here is “mm_security_provider”. The rest of the config is pretty standard. The firewall will allow anonymous users to the entire path, but the access controll requires an admin role if you access anything from the backoffice.

The “mm_security_provider” is still unknown to the dependency injection container, so we have to configure it. Here’s the configuration:
File: /src/MediaMates/BlogBundle/Resources/config/services.yml

services:
  mm_service_blog:
    class: MediaMates\BlogBundle\Service\Blog
    arguments: [@doctrine.odm.mongodb.document_manager]
  mm_service_user:
    class: MediaMates\BlogBundle\Service\User
    arguments: [@doctrine.odm.mongodb.document_manager]
  mm_security_provider:
    class: MediaMates\BlogBundle\Security\MongoProvider
    arguments: [@mm_service_user]

The “mm_security_provider” is configured to create an object of class MediaMates\BlogBundle\Security\MongoProvider. The provider will need to access the User collection in my MongoDB. I have taken a Service Oriented Architecture (SOA) approach, where I gather all business intelligence in so called Service Classes. My provider needs the correct service to talk to the database. So I configured an argument for the constructor which refers to my User service. In case you don’t work with a SOA, you can just replace the argument with @doctrine.odm.mongodb.document_manager. This should inject the Doctrine MongoDB ODM into your provider instead of a User service class.

Implementing the UserProviderInterface

Now that we have made the necessary configurations, it’s time to implement the actual custom provider that will allow the Security layer to talk to a MongoDB collection. First I’ll show you the code, and then I’ll explain what happens.
File: /src/MediaMates/BlogBundle/Security/MongoProvider.php

namespace MediaMates\BlogBundle\Security;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;

use MediaMates\BlogBundle\Service;

class MongoProvider implements UserProviderInterface
{
    protected $userService;
    
    public function __construct (Service\User $userService)
    {
        $this->userService = $userService;
    }
    
    public function loadUserByUsername($username)
    {
        $user = $this->userService->getUserByUsername($username);

        if (null === $user) {
            throw new UsernameNotFoundException(sprintf('User "%s" not found', $username));
        }
        
        return $user;
    }

    public function refreshUser (UserInterface $user)
    {
        return $this->loadUserByUsername( $user->getUsername() );
    }

    public function supportsClass($class)
    {
        return $class === 'MediaMates\BlogBundle\Document\User';
    }
}

The most important part is to extend Doctrine\ODM\MongoDB\DocumentRepository and implement Symfony\Component\Security\Core\User\UserProviderInterface. The interface only requires you to implement 3 methods: “loadUserbyUsername”, “refreshUser” and “supportsClass”. I have also implemented the constructor. The argument of the constructor, in my case, is my User service, like I specified earlier in my services configuration. In case you changed the configuration to the Doctrine MongoDB ODM, then you will have to update the constructor argument to be of the correct type.

loadUserByUsername is called by the Security layer, when the given username & password need to be verified. You have to find a user by his/her username. If this fails, you throw a UsernameNotFoundException. If you do find a user, you just return the object.

refreshUser is called by the Security layer, whenever it deems it necessary to reload the given user. This already happens after login, because it is possible that the object retrieved for verifying the credentials, is incomplete. With refreshUser, you should return a complete User object.

supportsClass returns wether or not the given class name is supported by your provider.

Implementing the UserInterface

The hard part is over now. Only thing remaining, is creating a User model which implements the correct interface, so that the Security layer knows what to expect. Here’s my model, complete with MongoDB ODM annotations, properties, getters & setters, and required methods by the interface.
File: /src/MediaMates/BlogBundle/Document/User.php

namespace MediaMates\BlogBundle\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @MongoDB\Document
 */
class User implements UserInterface
{
    /**
     *
     * @MongoDB\Id
     */
    protected $userID;
    
    /**
     * @MongoDB\String
     */
    protected $username;
    
    /**
     * @MongoDB\String
     */
    protected $password;
    
    /**
     * @MongoDB\String
     */
    protected $firstname;

    /**
     * @MongoDB\String
     */
    protected $lastname;
    
    /**
     * @var string A salt for the password
     */
    protected $salt = "Blog";
    
    /**
     * The roles a user has
     *
     * @return array
     */
    public function getRoles ()
    {
        return array(
            'ROLE_ADMIN'
        );
    }
    
    /**
     * Erases the credential information
     */
    public function eraseCredentials ()
    {
        $this->password = null;
    }
    
    /**
     * Verifies if given user equals the current user
     *
     * @param mixed $user
     * @return Boolean
     */
    public function equals (UserInterface $user)
    {
        return ($this->getUsername() === $user->getUsername());
    }
    
    /**
     * Returns the salt
     *
     * @return string
     */
    public function getSalt ()
    {
        return $this->salt;
    }

    /**
     * Get userID
     *
     * @return id $userID
     */
    public function getUserID()
    {
        return $this->userID;
    }

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

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

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

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

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

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

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

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

Methods required by the interface are: “getUsername”, “getSalt”, “getRoles”, “eraseCredentials”, “equals”. I hard coded the getRoles return value. The eraseCredentials method should remove sensitive information from your model. In our case, the password. The equals method lets you implement how two User models should be compared. I just made a comparison against the username, but of course this could (an should!) be more elaborate.

Conclusion

As it turns out, writing your own provider for authenticating against a MongoDB User collection wasn’t all that hard. You just have to figure out which classes & methods to implement, and it just works. Nevertheless, Symfony2 is quite young, and documentation still is scarce. The official documentation has some examples yes, but they usually don’t go beyond the basics. The cookbook has some specific use cases, which is nice. I hope this write-up will help you to implement your custom provider. If you have any suggestions or remarks, don’t hesitate to comment.

12 Comments

  • Pingback: Symfony2 + MongoDB for user managment | Push The Limits

  • Julien
    December 27, 2011 - 20:28 | Permalink

    Thank you very much, your post really helped me!

  • Jordan Stout
    November 21, 2011 - 01:03 | Permalink

    Just a tip, your refreshUser method uses loadUserByUsername()… This is a security issue that was recently fixed. Here’s the ODM fix: https://github.com/symfony/DoctrineMongoDBBundle/commit/bca3e54f1bb2041ce4179d9fa221851de6c65917

  • John
    September 14, 2011 - 14:35 | Permalink

    Can you please explain me how can I implement roles? I need to do that but keep getting exceptions….

    • September 14, 2011 - 19:45 | Permalink

      John,

      Can you please be more specific? Where do you get the errors? In your security configuration, or your User Document? What do you need to know about roles? I hard-coded in the example that each user has the ROLE_ADMIN. This works, but as Stof pointed out, should be given a custom implementation.

      • John
        September 14, 2011 - 21:41 | Permalink

        in user document I wrote like this.

        /**
        * @MongoDB\EmbedMany
        */
        protected $roles = array();

        and from controller, at the time of user registration, I am doing

        $user->setRoles(array(“ROLE_USER”)); (setroles and getroles are definedin class)
        here $user is object of class in which roles is declared

        I get this error:
        Warning: spl_object_hash() expects parameter 1 to be object, string given

        • September 14, 2011 - 22:16 | Permalink

          Your annotation “@MongoDB\EmbedMany” signifies you want to embed multiple documents. Yet, you provide an array of strings. So either your annotation must change to “@MongoDB\Collection”, or in your setRoles invocation, you have to provide an array of (Role-)documents.

          • John
            September 14, 2011 - 22:38 | Permalink

            Wow, Thanks for the solution. I changed to @MongoDB\Collection and it worked…. Found new problem at the login form I always get “The presented password is invalid.” error (I am entering correct password). Any solution? I am using plaintext password encoding. Any good documentation link along with the solution will also be appreciated…

          • September 16, 2011 - 16:35 | Permalink

            John, good that it worked. Unfortunately, I have no idea why you are receiving the password error. Have you tried returning null in the getSalt() method? I did that before I added the MD5 encoder, and I didn’t have a problem. Not sure if that is a solution though.

            As for documentation, I can only recommend the official symfony documentation on http://symfony.com. Just read the chapters in “The Book” that you need to get a basic grasp of how things work. But for more complex stuff, I’m afraid you’re going to have to use Google.

  • Stof
    September 7, 2011 - 22:35 | Permalink

    Some thoughts about your post to let you improve your trial with the Security component:

    I don’t see why you need to extend DocumentRepository: you don’t call any of its methods, and you would not be able to do so anyway as you don’t initialize it with the original constructor.

    Removing the encoded password in eraseCredentials is generally a bad idea:
    - as it changes a field persisted by Doctrine, it could change the value in Mongo depending of when you call flush()
    - it does not allow you to compare the password in equals() (see the next point)
    The main goal of this method is to allow removing the raw password if you stored it in a property for some reason (for instance during the registration process as we do in FOSUserBundle)

    You should compare the encoded password in equals() so that a user is considered as changed when the password is changed. with your current code, if someone has a session cookie containing this user, it will stay authenticated even if the password is changed, which may be considered as a security issue.

    Your UserProvider will fail currently if Doctrine gives you a proxy object as you compare the class name as string instead of allowing inheritance. This will often work because the firewall runs very early (and so a proxy will probably not have been needed before this point) but it could happen.
    You should also check if the UserInterface is supported in refreshUser and throw the appropriate exception otherwise.

    To improve the security, you should use a different salt for each user (which requires storing it in Mongo).

    On a side note, hardcoding the roles is probably a bad idea as all users will have access to the admin panel. Using a Mongo field could be better (storing it as an array of strings is the simpliest way)
    And another note: DoctrineMongoDBBundle provides a DocumentUserProvider for Mongo making it easy to use a Mongo provider when you don’t need the features of FOSUserBundle. The only need is to register a service using it (I understand you wanted to write your own one for a learning purpose. It is mainly to point readers to the built-in implementation)

    • September 8, 2011 - 07:56 | Permalink

      Hey Stof,

      Thanks for your long list of suggestions. That’s exactly the kind of comment I was hoping for.
      Your point about eraseCredentials & equals totally made sense, and I’ll work on it. Same thing for hard coding the roles. However, I knew this was not good, but since I postponed working with the roles, I just hard coded them to see it working.

      At first I didn’t extend from DocumentRepository, and no matter what credentials I used in my login form, I would always get the username “admin” in the loadUserByUsername method. Then I stumbled upon some code where someone created his own provider for Doctrine ORM, and there he extended from EntityRepository (or what the equivalent is called). I just tried doing the same with DocumentRepository, and magically the correct username was presented to the loadUserByUsername method. I’ll try again today if this wasn’t some weirdness from my session.

      I might write a follow up with the adjustments you suggested, and things I learned from it. I should let you proof read it first though ;) Thanks for the info!!

    • September 8, 2011 - 08:39 | Permalink

      Just wanted to let you know that extending the DocumentRepository indeed isn’t necessary. I have quickly updated that part of the code, because it’s kinda important. It was probably some kind of strange thing going on with my session.

  • Leave a Reply

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

    *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>