A handy shortcut to implementing user providers

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

User providers are the main source for authenticating a user within Symfony. It can also be a great way to confuse yourself. In the documentation, the default example shows how to create an entity and wrap a user provider around it. Then you have to manually implement code for loading users and refresh the authentication token for your active users.

Naturally, with so many code additions, this process can be frustrating. Fortunately, you are not required to do any of it!

I have a shortcut for you that requires significantly less code.

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();
    }
}

Easy, right? We are essentially 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.

How does this get us a user provider? Read on:

<?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 does not suck

Let’s see!

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

How can the UX be improved? Now we can track every login failure that hit the maximum limit of attempts. Users can now be automatically offered the option to recover their password should authentication fail.

TL;DR

Symfony provides a giant toolbelt for almost every use case. In this case, we get 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 allows you to make an educated choice as to what you really need for your application. If you are just starting to use Symfony, this could be rather annoying.

Comments powered by Disqus