A handy shortcut to implementing user providers

Jun 11, 2018 00:00 · 539 words · 3 minutes read symfony

User providers are the primary source for authenticating a user in Symfony and a source of confusion.

The default example in the documentation shows how to create an entity and wrap a user provider around, combined with you having to implement code for loading users from a source and also refreshing the authentication token for active users.

Why do you have to do all that? Well, you do not! There is a shortcut with much less code involved.

Introducing the entity user provider

The Symfony Doctrine Bridge actually includes a premade entity-based user provider class that can load users from any repository implenenting Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface.

First we need a repository implementing the interface. Defau

<?php

namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;

class UserRepository extends ServiceEntityRepository implements UserLoaderInterface
{
    public function loadUserByUsername($username)
    {
        $query = $this
            ->createQueryBuilder('u')
            ->where('u.email = :username')
            ->setParameter('username', $username)
            ->getQuery();

        return $query->getOneOrNullResult();
    }
}

Now that was easy, right? Where basically loading a UserInterface compatible entity with a single method here. Take note of the where() call. For the sake of a real world example, we just load users by their email addresses because usernames are so last century.

Alright, now how does that get us a user provider? Glad you asked! Here goes nothing.

<?php

namespace App\Security;

use App\Security\Exception\BadCredentialsException;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;

class UserProvider extends EntityUserProvider
{
    public function __construct(ManagerRegistry $registry, $classOrAlias, $property = null, $managerName = null)
    {
        parent::__construct($registry, $classOrAlias, $property, $managerName);
    }

    /**
     * Loads a user by the given username/email.
     *
     * @param string $username
     *
     * @return UserInterface
     *
     * @throws BadCredentialsException
     */
    public function loadUserByUsername($username)
    {
        try {
            return parent::loadUserByUsername($username);
        } catch (UsernameNotFoundException $e) {
            // security.hide_user_not_found option is disabled in order to customize the error message
            // So we must handle that logic ourself
            throw new BadCredentialsException(sprintf('Username %s not found.', $username), 0, $e);
        }
    }
}

BadCredentialsException is just an extension of Symfony\Component\Security\Core\Exception\UsernameNotFoundException showing how you can customize the result of loading users.

Finally, we configure the service like this in services.yaml:

    app.security.user_provider:
        class: App\Security\UserProvider
        arguments:
            $classOrAlias: 'App\Entity\User'

… then define the provider in security.yaml:

    providers:

        user_db_provider:

            id: app.security.user_provider

...

    firewalls:
        main:
            provider: user_db_provider

Why this is does not suck

Let’s see!

  1. We now have a single source of truth for user authentication. You can totally write tests now.
  2. The concept of an entity provider is extendable. Since we can inject classes to load from, you can reuse the same provider e.g. if you make use of EasyAdmin.
  3. You now have a single point where you can improve your user experience.

Improvements you say? Yeah, how about tracking every login which failed with a maximum limit of tries? How about automatically offering users the option to recover their password if they fail to authenticate too often?

TL;DR

Symfony provides a giant toolbelt for almost every use case. In this case, we got a free, premade user provider. It’s not really advertised in the documentation but it should be because it is cool.

Symfony sucks? Not really, it just let’s you make an educated choice what you really need for your application. If you are just started using Symfony that could be annoying as hell. I hope this makes your journey more pleasant.

Comments powered by Disqus