Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

The UserProvider: Custom Logic to Load Security Users

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

The UserProvider: Custom Logic to Load Security Users

Hey there repository expert. So our actual goal was to let the user login using a username or email. If we could get the security system to use our shiny new findOneByUsernameOrEmail method to look up users at login, we’d be done. And back to our real job of crushing the rebel forces.

Open up security.yml and remove the property key from our entity provider:

# app/config/security.yml
security:
    # ...

    providers:
        our_database_users:
            entity: { class: UserBundle:User }

Try logging in now! Ah, a great error:

The Doctrine repository “Yoda\UserBundle\Entity\UserRepository” must implement UserProviderInterface.

The UserProviderInterface

Without the property, Doctrine has no idea how to look up the User. Instead it tries to call a method on our UserRepository. But for that to work, our UserRepository class must implement UserProviderInterface.

So let’s open up UserRepository and make this happen:

// src/Yoda/UserBundle/Entity/UserRepository.php
// ...

use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserRepository extends EntityRepository implements UserProviderInterface
{
    // ...
}

As always, don’t forget your use statement! This interface requires 3 methods: refreshUser, supportsClass and loadUserByUsername. I’ll just paste these in:

// src/Yoda/UserBundle/Entity/UserRepository.php
// ...

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

class UserRepository extends EntityRepository implements UserProviderInterface
{
    // ...

    public function loadUserByUsername($username)
    {
        // todo
    }

    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if (!$this->supportsClass($class)) {
            throw new UnsupportedUserException(sprintf(
                'Instances of "%s" are not supported.',
                $class
            ));
        }

        if (!$refreshedUser = $this->find($user->getId())) {
            throw new UsernameNotFoundException(sprintf('User with id %s not found', json_encode($user->getId())));
        }

        return $refreshedUser;
    }

    public function supportsClass($class)
    {
        return $this->getEntityName() === $class
            || is_subclass_of($class, $this->getEntityName());
    }
}

Tip

You can get this code from the resources directory of the code download.

Filling in loadUserByUsername

The really important method is loadUserByUsername because Symfony calls it when you login to get the User object for the given username. So we can use any logic we want to find or not find a user, like never returning User’s named “Jar Jar Binks”:

public function loadUserByUsername($username)
{
    if ($username == 'jarjarbinks') {
        // nope!
        return;
    }
}

We can just re-use the findOneByUsernameOrEmail method we created earlier. If no user is found, this method should throw a special UsernameNotFoundException:

// src/Yoda/UserBundle/Entity/UserRepository.php
// ...

class UserRepository extends EntityRepository implements UserProviderInterface
{
    // ...

    public function loadUserByUsername($username)
    {
        $user = $this->findOneByUsernameOrEmail($username);

        if (!$user) {
            throw new UsernameNotFoundException('No user found for username '.$username);
        }

        return $user;
    }

    // ... refreshUser and supportsClass from above...
}

Try logging in again using the email address. It works! Behind the scenes, Symfony calls the loadUserByUsername method and passes in the username we submitted. We return the right User object and then the authentication just keeps going like normal. We don’t have to worry about checking the password because Symfony still does that for us.

Ok, enough about security and Doctrine! But give yourself a high-five because you just learned some of the most powerful, but difficult stuff when using Symfony and Doctrine. You now have an elegant form login system that loads users from the database and that gives you a lot of control over exactly how those users are loaded.

Now for a registration page!

Leave a comment!

53
Login or Register to join the conversation
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | posted 5 years ago

Hello Ryan!
If I want to do authentication myself (say, via JWT) and initiate a session the same way as you do here via Symfony's Authentication facility, what would I use for that?
Thank you!

6 Reply

Hey Vladimir Z.!

I'd definitely recommend using the Guard authentication system, but you can certainly do whatever you want :). But with JWT, there is no session: you're just validation the token on each request and decoding it to get some user information. You could honestly put this all into a service and then call that service from anywhere (e.g. a controller). You would then be completely avoiding Symfony's security system, for better or worse (i.e. you would not appear to be "authenticated" to Symfony). That service class would look something like this:


class JWTAuthManager
{
    public function getUser()
    {
        // inside, you would read the JWT from the Request object, verify it and decode it
        // you could then use the information from the JWT to fetch the User object from
        // the database and return it
    }
}

Again, I recommend using Guard. But ultimately, verifying a JWT, decoding it, and using the information on it is a pretty simple task. You could then make decisions based on the User. You could also move all of the verifying/decoding to a listener on kernel.request so that it happens automatically on each request. You would then set the User object on a service so that you could fetch it easily later. That would also make sending back error JSON responses (e.g. for an invalid JWT) easier.

Cheers!

1 Reply
Vladimir Z. Avatar

Thank you, Ryan!

Reply
Default user avatar
Default user avatar Mike Ritter | posted 5 years ago

This is my picture every time Weaver says it.

Good ol' Gene Wilder.

http://thevillagevidiot.com...

2 Reply

hahaha that's funny ;)

Reply
Default user avatar
Default user avatar Simon Carr | posted 5 years ago

I have followed almost all your tutorials on Symfony 3, but one thing I have noticed is that in the debug bar, you response time is more often than not around 50 to 100ms. Mine is constantly over 1000ms. I have a powerful modern PC I7 with 16GB and SSD drive. I have tried turning off virus checker but that does not make much difference. What could the difference be between your setup and mine?

1 Reply

Hey Simon,

What's your OS? Windows? I had the same problem on Windows, opening a DB connection was always taking about 1s and I had response time over 1s (I used to use OpenServer). Btw, you can debug what exactly consumes a lot of this response time with Symfony's Web Debug Toolbar - just click on this time icon and you will see Performance section with a nice Execution timeline. Find your bottle neck there, I bet it's something like DB connection, etc. which is not related to PHP scripts execution.

Cheers!

1 Reply

isnt this a recycled vid!! lol well i caught up! hope the next batch of vids comes soon

1 Reply

Nice job! It's actually quite different than the *old* "User Provider" chapter we had for Symfony2. Not a lot of has technically changed, but the way I like to explain it has :).

Cheers!

2 Reply
Default user avatar
Default user avatar Léo Li | posted 5 years ago

Hello again,

For this lesson, I've done everything, and it works well for login by username or by email. But, when I enter a username that doesn't exist, I still get an error "Wrong password bro!". But I think it should be "No user found for XXXX", right?

Reply

Hey Léo!

Ah, I missed this message! And it's a really good question. The reason is because of this "hide_user_not_found" option under security: http://symfony.com/doc/curr.... This makes it so that when we throw a UsernameNotFoundException, it turns it into a different exception. Here's the code for that: https://github.com/symfony/...

There's a bit more behind the scenes to controlling the exception messages in Symfony, and it's something that's been improved in Symfony 2.8 so that you have more control via a class called CustomUserMessageAuthenticationException: https://github.com/symfony/....

Cheers!

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 5 years ago | edited

Everything works, but I get a deprecation error:

Autowiring services based on the types they implement is deprecated since Symfony 3.3 and won't be supported in version 4.0. Try changing the type-hint for argument "$em" of method "AppBundle\Security\LoginFormAuthenticator::__construct()" to one of its parents: interface "Doctrine\ORM\EntityManagerInterface", or interface "Doctrine\Common\Persistence\ObjectManager".

If I change:
use Doctrine\ORM\EntityManager;<br />...<br />public function __construct(FormFactoryInterface $formFactory, EntityManager $em,

To:
<br />use Doctrine\ORM\EntityManagerInterface;<br />...<br />public function __construct(FormFactoryInterface $formFactory, EntityManagerInterface $em,

the deprecation message vanishes.
Is this the correct way to do it?

Reply

Yo Mike P.!

Very good! That *is* the correct way to handle it! We have a blog post about this stuff, if you want a bit more background: https://knpuniversity.com/b.... Either code will work in 3.2-3.4, but only the updated code will work in 4.0 and beyond.

Cheers!

1 Reply
weaverryan Avatar weaverryan | SFCASTS | posted 5 years ago | edited

Yo there!

Ah yes, the famous user provider :). Can you tell us more about what you're trying to achieve? What do you mean by "use Drupal users inside a Symfony custom application"? Do you actually want to use the Drupal User class and code directly? Or do you mean that you want to just read from a Drupal application's User database and use that information inside Symfony?

It's likely that the best solution will be to expose the Drupal user information via some sort of API inside Drupal, so that in Symfony, when the user logs in, your Symfony code can make an API request back to Drupal to say "Please check this password and tell me what permissions this user has". But a lot depends on what you need!

Cheers!

Reply
MolloKhan Avatar MolloKhan | SFCASTS | posted 5 years ago | edited

Hey there

So you have an independent application in Drupal and another in Symfony, right? If so, the cleaner way to do it, would be to create a very basic API endpoint (In your Drupal app) for fetching your user's information (you may want to pass a secret key just to keep that endpoint secure). Going this way, you won't have to do a rework in the future when you scale those applications.

Have a nice day :)

Reply
Default user avatar
Default user avatar Medoune | posted 5 years ago

Hello,
I have done everything for this lesson, and everything works well but when I try to log in with a username who is active, I get "Account is locked"! And I can't found what that issues is from?

Sorry for my bad English

Reply

Hi there!

This message can only be displayed because of 1 reason: your User class implements AdvancedUserInterface and the isAccountNonLocked() method returns false. Make sure this returns *true* for the User. I know, it has a confusing name "non-locked" is kind of a double negative. Anyways, here's the spot in the core that calls this method and gives you that error: https://github.com/symfony/...

Cheers!

1 Reply
Default user avatar

Hello, I works! It was due to the isAccountNonLocked() which was set to false!

Thanks

Reply
Default user avatar

Following everything and I have just something different as a result, the user username does not appear in the dev bar like you. It is just n/a; the authentification went through and I am logged in. What could be the cause?

Thanks

Reply

Hi James!

That's really interesting! It appears that you logged in... but somehow are missing your User class. There's probably some tiny issue somewhere. Let's check out a few things:

1) What does the getUsername() method look like in your User class? If there is some bug in this, and it's returning a blank string, then I think it might cause this problem :).

2) If you hover over the icon in the web debug toolbar, what does it say for "Token class"? What about "Logged in as" - is that just blank?

3) If you turn intercept_redirects temporarily back to true and login, does the web debug toolbar look any different before you actually redirect?

4) After logging in, go into your favorite controller and run this code:


dump($this->get('security.token_storage')->getToken());die;

Basically, you're getting logged in, but something is wrong with the User class, which is inside of this "Token" class. The above dump would tell us more info about what's going on.

Let me know what you find out!

Reply
Default user avatar

Yes found it just a few minutes ago, I forgot some private fields in LoginFormAuthenticator... My bad!

Thanks anyway for the lightning fast answer!

Reply

Sweet! Catching your own errors? No better way to learn something :). Cheers!

Reply
Patrick Avatar
Patrick Avatar Patrick | posted 5 years ago | edited

Hi there,
Thanks for this awesome tutorial - I keep coming back to it when I want to extend our authentication system as a reference-guide!

I have a question about enabling `remember me` functionality for a social oAuth login. What I want to be able to do is enable the same 'remember me' behaviour for users who have logged via the 'login with facebook' button, as those who have logged in via the login form, and checked the 'remember me' box.

I had a look through the code, and followed it in xdebug step-by-step, but all the references to a `remember_me` setting seemed to be looking for a suitable value coming from a symfony form.

Am I missing something or is there a way to tell a particular authenticator - 'I want to enable remember me all the time, please'?

I'm using the fantastic KnpU oAuth Client Bundle (thanks!) to handle the social authentication, and my implementation is pretty much the same as the reference implementation in the readme of that repository.

I thought I'd ask the question before I went back to xdebug again!

Thanks for the great KnpU resource,

Patrick

Reply

Hey Patrick,

In case you're going to use a Symfony Guard authenticator - it has a fancy method to helps with it, see supportsRememberMe(): https://github.com/symfony/...

Also, look at "always_remember_me: true" option: https://symfony.com/doc/cur...

Cheers!

Reply
Patrick Avatar
Patrick Avatar Patrick | Victor | posted 5 years ago | edited

Hi victor

Thanks for the information - that's really helpful!

I'd missed the "always_remember_me: true" option - that looks like just what I'm looking for!

Is there a way to enable "always_remember_me: true" for just one (Guard) authenticator? So that, for example, you could have a FacebookAuthenticator which always set remember_me to true, but you had a GoogleAuthenticator which (for some reason) didn't set remember_me automatically?

Thanks again!

Reply

Hey Patrick!

Hmm. So, it's not exactly possible, because always_remember_me is at the firewall level. You could of course create multiple firewalls, but that's taking a hammer to the situation :).

But, the way that remember me NORMALLY knows that it should be activated, is by looking for a _remember_me checkbox in your form. So, you could add that to one form, but not the other. Or... you can even be a bit sneakier :). Even without that checkbox, I think (never tried before, but 99% sure) that you could set this value into the request, from inside your authenticator (could be anywhere):


$request->attributes->set('_remember_me', true);

That should activate it :).

Cheers!

Reply

Hi,
How i can replace Loged in as "myusername" with "my emaill adresse" ?
is it possible to made the authentification via email not via username?

Reply

Hey ahmedbhs!

Yes, absolutely! In fact, it's very easy, but a little bit confusing :). Basically, Symfony forces you to use a "username" in a few places: in your User class, you must have a getUsername() method. And in your user provider, you must have a loadUserByUsername method.

But.... if you want don't want a username, and instead just want an email, then simply make both of these methods work with an email instead. What I mean is, in User::getUsername(), return $this->email And in loadUserByUsername, query the database via your email field. Symfony is using the term "username"... but really, "username" is whatever unique thing you want - the "username", an email, a uuid or whatever.

Cheers!

Reply

How did you get Autocomplete in security.yml?

Reply

Hey Mark,

A lot of autocompletion for Symfony Framework comes from Symfony Plugin for PhpStorm, see https://knpuniversity.com/s... where we show how to install and configure it.

Cheers!

Reply

Have been running this the entire series, but cannot get yaml autocomplete to work :-( Tryed googling around a bit, but couldn't get an answer on how to "enable" really.

Reply

Hm, probably it also depends on your PhpStorm / Symfony Plugin versions... but not all the yaml files could have autocomplete. It works mainly for app/config/config.yml. Go to PhpStorm preferences and find Symfony Plugin preferences - make sure you tick all the necessary checkboxes and enabled the plugin. Sometimes it may just stop working, especially after some internal PhpStorm errors, but after upgrade to the latest versions and PhpStorm restart it works again.

Cheers!

Reply

Hi Ryan,
looks like something has changed in symfony3, I've done everything thru this course and I'm always getting this error **Authentication request could not be processed due to a system problem.** when I try to login after doing the user provider change
I just can't figure it out how to fix it, any help would be very appreciated

Reply

I found it, in symfony3 no longer requires to implement "UserProviderInterface" instead it needs to implement "UserLoaderInterface"

Reply

Yes, good find! We changed that in Symfony 2.8 - the new interface is a little bit easier than the old one. DX improvement... as long as you know it exists :). Thanks for posting your solution!

Reply

No problem, I hope someone find this useful

Reply
Default user avatar
Default user avatar Dominik | posted 5 years ago | edited

Hey Ryan,

the Symfony 3 Track is absolutely awesome!
I followed the whole track without any problem .. until NOW ;-)

Something seems to be wrong with my UserProvider setup.
After the successful login and first redirect after the login the profiler shows my username correctly with token "PostAuthenticationGuardToken".
but after a refresh of the page I am an anon user again with token "AnonymousToken".
So it seems that i don't stay logged in.

Dump after redirect:

MainController.php on line 11:
PostAuthenticationGuardToken {#129 ▼
  -providerKey: "main"
  -user: User {#38 ▼
    -id: 1
    -email: "dominik.kuss+1@gmail.com"
  }
  -roles: array:1 [▼
    0 => Role {#113 ▼
      -role: "ROLE_USER"
    }
  ]
  -authenticated: true
  -attributes: []

Dump after refresh:

MainController.php on line 11:
AnonymousToken {#113 ▼
  -secret: "58f76d7b47db04.57141949"
  -user: "anon."
  -roles: []
  -authenticated: true
  -attributes: []

Strange: i don't get auto completion for the User entity in security.yml for the provider class property.

Any idea what could cause this problem?

Cheers!

Reply

Hey Dominik!

We are glad that you are liking our tutorials :)

Could you show me how looks like your security.yml and config.yml files?
Also what symfony version are you using ?

Have a nice day!

Reply
Default user avatar
Default user avatar Dominik | MolloKhan | posted 5 years ago | edited

Thanks for your help. Here we go:

Symfony 3.1.4 - app - dev
PHP 5.5.38 - no cache or APC
i use the build in web server like in the tutorials.

uhm .. perhaps this is the problem?
<cite>PHP needs to be a minimum version of PHP 5.5.9</cite>
The PHP versions seems to be little bit old on this machine cought

or perhaps missing serialize functions in the authenticator?

security.yml:
i needed the : notation for the entity to get the auth working at all.

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        our_users:
            entity: { class: AppBundle:User, property: email }

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            anonymous: ~
            guard:
                authenticators:
                    - app.security.login_form_authenticator

            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html

config.yml:
the different locale is not the problem

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }

# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
    locale: de
    cache_type: file_system

framework:
    #esi:             ~
    translator:      { fallbacks: ["%locale%"] }
    secret:          "%secret%"
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: ~
    form:            ~
    csrf_protection: ~
    validation:      { enable_annotations: true }
    #serializer:      { enable_annotations: true }
    templating:
        engines: ['twig']
        #assets_version: SomeVersionScheme
    default_locale:  "%locale%"
    trusted_hosts:   ~
    trusted_proxies: ~
    session:
        # handler_id set to null will use default session handler from php.ini
        handler_id:  ~
        save_path:   "%kernel.root_dir%/../var/sessions/%kernel.environment%"
    fragments:       ~
    http_method_override: true
    assets: ~

# Twig Configuration
twig:
    debug:            "%kernel.debug%"
    strict_variables: "%kernel.debug%"
    number_format:
        thousands_separator: '.'
    form_themes:
        - bootstrap_3_layout.html.twig

# Doctrine Configuration
doctrine:
    dbal:
        driver:   pdo_mysql
        host:     "%database_host%"
        port:     "%database_port%"
        dbname:   "%database_name%"
        user:     "%database_user%"
        password: "%database_password%"
        charset:  UTF8
        # if using pdo_sqlite as your database driver:
        #   1. add the path in parameters.yml
        #     e.g. database_path: "%kernel.root_dir%/data/data.db3"
        #   2. Uncomment database_path in parameters.yml.dist
        #   3. Uncomment next line:
        #     path:     "%database_path%"

    orm:
        auto_generate_proxy_classes: "%kernel.debug%"
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true

doctrine_cache:
    providers:
        my_markdown_cache:
            type: %cache_type%
            file_system:
                directory: %kernel.cache_dir%/markdown_cache

and my LoginFormAuthenticator because i have to use the new onAuthenticationSuccess function


<?php

namespace AppBundle\Security;


use AppBundle\Form\LoginForm;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
    /**
     * @var FormFactoryInterface
     */
    private $formFactory;
    /**
     * @var EntityManager
     */
    private $em;
    /**
     * @var RouterInterface
     */
    private $router;

    function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router)
    {
        $this->formFactory = $formFactory;
        $this->em = $em;
        $this->router = $router;
    }

    public function getCredentials(Request $request)
    {
        $isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
        if (!$isLoginSubmit) {
            return;
        }

        $form = $this->formFactory->create(LoginForm::class);
        $form->handleRequest($request);
        $data = $form->getData();

        return $data;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $username = $credentials['_username'];

        return $this->em->getRepository('AppBundle:User')
            ->findOneBy(['email' => $username]);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        $password = $credentials['_password'];

        if ($password == 'iliketurtles') {
            return true;
        }

        return false;
    }

    protected function getLoginUrl()
    {
        return $this->router->generate('security_login');
    }

    use TargetPathTrait;

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // if the user hits a secure page and start() was called, this was
        // the URL they were on, and probably where you want to redirect to
        $targetPath = $this->getTargetPath($request->getSession(), $providerKey);

        if (!$targetPath) {
            $targetPath = $this->router->generate('homepage');
        }

        return new RedirectResponse($targetPath);
    }


}
Reply
Default user avatar

It seems that i have solved the problem.

I updated my PHP to 5.6.30
I had to change the provider entity class name back to AppBundle/Entity/User for the login to work again.
But i still got logged out again after a refresh.

After that i run "composer update" to update the packages and dependencies.
Now Symfony is running at version 3.1.10 and I finally stay logged in. Hooray!

Setting my the course to "Logout" tutorial now .. lol :-)

Reply

Hey Dominik!

Sorry for the late response, I'm glad you could fix your problem without my help, you defenitely had a weird one ;)

Cheers!

Reply
Default user avatar

Hello, I have a question about the user provider.

Is it mandatory to implement the UserProviderInterface class?
http://symfony.com/doc/curr...

As I understand it, if we implement UserProviderInterface in User Entity, we can make use of remember me functionality, save user in session, (this part, currently in the application does not perform it?) And perform impersonation users (currently, we do it and not We implemented UserProviderInterface and we use this).

I have a little of trouble with this ...

Thanks for your job

Reply

Hey Carlos!

Oh boy, the user provider is very confusing. First, you don't implement UserProviderInterface in the User entity - the User entity implements UserInterface. So, the User and your "user provider" are 2 separate things. But, what you said about the UserProvider is true: it allows you to use remember me functionality, save user to the session and do impersonation. And, in practice, all authentication systems *must* have a User Provider. So, it's a necessary part of your system, but many people don't even realize it exists or what it does. If you're using Doctrine and your User class is an entity, then you've probably configured the "entity" user provider in security.yml under the "providers" key (http://symfony.com/doc/curr.... Doing that is enough to configure a user provider that does all of the things you mentioned. I think it should be quite rare that you need a custom user provider.

I hope this helps! It *is* confusing!

Reply
Default user avatar

Ah, I understood.
We use the Symfony default user provider. Indicated in security.yml.

Could you tell me a case where a custom user provider really needs it?
Thanks Ryan

Reply

Hey Carlos!

Originally, people used the user provider inside of their authentication system in order to load their users. In fact, for many of the built-in authentication mechanisms, like form_login, http_basic, etc, this is still true: you submit a username + password form, and the form_login authentication mechanism uses your user provider (e.g. the entity provider) to know where to load that User. So, if you're using one of these mechanisms, and you need to load your users from a different location (perhaps your User information is stored across an API), you'll need a custom user provider.

However, thanks to Guard, I recommend now that people use it to create their own authentication mechanisms whenever they have any non-standard situation (a login form that loads from the database is standard, so using form_login is fine). In Guard, you are in complete control of loading your User object however you want (in your getUser() method), and I recommend for clarity, not using your user provider to do this work. Instead, for example, if you need to load User information from some API, then just write the code in your authenticator and do it! In this situation, when would you need a custom user provider? Well, remember, a user provider does basically 2 things:

A) It refreshes the User from the session (UserProviderInterface::refreshUser(UserInterface $user)
B) It loads a user by username for remember_me and switch_user (UserProviderInterface::loadUserByUsername($username))

So, you'll need a custom user provider basically if your User information is not stored in a database. If it is, just use "entity". If you do originally load User information from some other place (e.g. an API) but then save a new User database record for each user, then you should still use the entity provider. A custom provider is needed only if your User object can only be populated by loading data from some custom (i.e. non-database) source.

So, in theory, it's not SO uncommon... but I rarely see it: almost always, people are loading users from a database. Even if they have some big central-authentication system, they often store a local User object in their database so that they can store application-specific information about that User. And as I mentioned, if this is the case, you should still use the entity provider.

I hope this helps! The user provider is unfortunately confusing in Symfony, which is why I try to minimize how much people worry about it. In 99% of the use-cases, you should configure the "entity" provider in security.yml and never think of it again :).

Cheers!

Reply
Default user avatar
Default user avatar Carlos | weaverryan | posted 5 years ago | edited

Hi Ryan, excuse me again ... =(

I have a question with this subject.

In my application I use the default Provider Entity of symfony, (I have not created a user provider).

I want to use the bundle fos oauth server https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/blob/master/Resources/doc/index.md

I imagine that you have used it sometime =)

The documentation says:

  • If you're authenticating users, don't forget to set the user provider. Here's an example using the FOSUserBundle user provider:

#config.yml
fos_oauth_server
    service:
        user_provider: fos_user.user_provider.username

For this case, I enter the doubt written in my previous comments,
In this case, I need to create my non-user provider?

Regards Ryan, thank you very much

Reply

Hi Carlos!

I have not actually used this bundle yet, but I think I can answer your question :). And, I can understand the confusion - it wasn't clear to me why this was needed in that bundle! So, It did a little bit of digging!

It turns out, that the user provider is only needed by that bundle if you allow the "password" grant type (also called user credential grant type) to be used to get a token. In this model, your user sends their username and password directly to the server. IF you use this grant type, then the bundle calls the loadUserByUsername method on your user provider (https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/blob/master/Storage/OAuthStorage.php#L161) to fetch the user. So, you would only need a custom user provider if, for example, you allowed your users to send something other than a username (e.g. their "id") - you would then need a user provider whose loadUserByUsername() expects the $username to actually be an id, and then fetches the user by id.

I hope that helps! If you don't set the user provider in the configuration for that bundle, I believe you won't have any problems, unless you try to use this grant type.

Cheers!

Reply
Default user avatar
Default user avatar Paul-André Duchesne | posted 5 years ago | edited

Hello Ryan,

I come towards you because I'm stuck with the implementation of authenticating against an ldap server with guard.
I've followed a bit of this: http://symfony.com/doc/current/security/ldap.html - adapted a bit due to a bit outdated version of the documentation and I've got this:
In services.yml:


app.security.ldap.adapter:
    class: Symfony\Component\Ldap\Adapter\ExtLdap\Adapter
    arguments:
        - host: "%ldap_server%"
    autowire: true
app.security.ldap:
    class: Symfony\Component\Ldap\Ldap
    arguments: [ "@app.security.ldap.adapter" ]
    autowire: true
app.security.login_form_ldap_authenticator:
    class: AppBundle\Security\LoginFormLdapAuthenticator
    autowire: true

In security.yml:


security:

    encoders:
        AppBundle\Entity\Person: bcrypt

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        chain_provider:
            chain:
                providers:
                    - ldap_users
                    - db_users
        db_users:
            entity: { class: AppBundle\Entity\Person, property: uid }
        ldap_users:
            ldap:
                service: app.security.ldap
                base_dn: cn=Users,dc=rbins,dc=be
                search_dn: "cn=%ldap_user%,ou=%ldap_ou%,dc=rbins,dc=be"
                search_password: "%ldap_pwd%"
                default_roles: [ROLE_USER]
                filter: (&(ObjectClass=Person)({uid_key}={username}))
    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
               security: false

        rest:
            pattern: ^/rest/
                anonymous: true
                stateless: true
                security: false

        main:
            anonymous: ~
              guard:
                authenticators:
                    - app.security.login_form_ldap_authenticator
                    - app.security.login_form_authenticator
                entry_point: app.security.login_form_ldap_authenticator

In the AppBundle\Security\LoginFormLdapAuthenticator in the constructor, I've been well defined a Symfony\Component\Security\Core\User\LdapUserProvider that I set in a $this->ldapUser variable that I use in the getUser method to check existence of the username in the Ldap...
'Till that point everything's fine...
But in the checkCredentials, I don't know what (and how) to call (and to inject in my constructor) to authenticate the user against the ldap server...

Could you suggest me any direction that could help me going further ?

Kind regards,

Paul-André

PS.: You've certainly seen that I've defined a chain provider and two authenticators. My expectations were that if the first fails, it tries to fallback on the second one... But it didn't appeared to act so. For instance, if the user wasn't found in the ldap, it stopped and I got back to the login screen with the corresponding 'user unknown' exception displayed... and I've seen that the UserProvider parameter in the getUser was a ChainProvider containing the two others... What could I do to expect fall back on the second provider ? Should I only make one authenticator and handle the fallback in each methods ?

Reply

Hi Paul-André!

Unfortunately, I don't know a lot personally about LDAP. But, I do know a lot about Guard, so I'll do my best to help :).

1) Since you're using Guard, obviously you need to do a little bit more work yourself in the authenticator (which is ok, because you also get a lot more flexibility). But, we can use the built-in LDAP authentication classes as inspiration for what we do. For example, the form_login_ldap authentication listener, is actually this class: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php. So, how should you check the users password? Mimic was it's doing here: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php#L71. Specifically, the $this->ldap library is your app.security.ldap service. So this is what you should inject into your authenticator.

2) And yes, I noticed your "chain" user provider. User providers can be complex to understand, and we definitely have a problem here. First, make sure you review this: http://knpuniversity.com/screencast/symfony-security/user-provider - it tries to explain the purpose of the user provider. Your exact correct setup depends on your needs. So, I'm going to ask a bunch of questions:

As I understand it, your users should be able to login via their LDAP credentials or by logging in with their username/password that is stored in the database. Is this correct? Do you also have 2 different user classes? And, will you need to store application information about all users, or is the purpose for the users ONLY to be able to login? Will some users only exist in the database, but not in LDAP? And will some where only exist in LDAP and not in our local database? If a user exists in the database, but not in LDAP, will they thus manage their password in your system (instead of LDAP)? And if they exist in LDAP and not in your local database, I assume they will thus manage their password in LDAP and not your system? Or asked differently, is it true that some users will be logging in with their password in LDAP and other users will be logging in with their password that's manage by your system?

Pass me some more information and I should be able to help your get your providers worked out correctly! But remember this point about providers:

A) The point of a user provider is only to reload the user (e.g. from the database) at the beginning of each request. So, if ultimately - once the user has logged in - you have a local "user" record in your database for every user, then you only need the entity provider. Otherwise, yes, you'll need a chain provider like you have.

B) When the user is originally logging in, you are in 100% full control of how that works - via your getUser() method. You could absolutely first look for the user in LDAP (you'll need a try-catch around loadUserByUsername, since this throws a UsernameNotFoundException) and if it's not found, look for it in your database.

Cheers!

1 Reply
Default user avatar
Default user avatar Paul-André Duchesne | weaverryan | posted 5 years ago

Hi Ryan,

First, Great, Great thanks for having me lead in the right direction... with the LdapBindAuthenticationProvider. I've extended that class in one of my own and adapted the things a bit to mimic the checkAuthentication method.
The main difficulties I encountered were at last with our AD authentication itself ;)

So, to help you helping me with the chain provider, here are answers to the list of questions:
* All the users of the application are stored in the database - our AD has got only the people who are still considered in activity in our institution - the AD has got thus only a subset of what is stored in the database. So, in the getUser method of my authenticator, I used only the EntityManager to get the corresponding user out of database.
* As the AD is responsible of authenticating the "active" users and as the users can modify their password through their windows desktop, my intention (and what I've done) was:
* if a user is authenticated against the AD, that meant his/her password was recognized... and that's ok... so in the checkCredentials method of my authenticator, I putted the call to the adapted checkAuthentication method in a try (as you suggested), and if it succeed, I return true.
* if a user is not authenticated against the AD, I try to find him/her in the AD with the loadUserByUsername method of the ldap client. If (s)he's found, I return false meaning the username was right but password not and if (s)he's not found, I check if password is valid against the database. If yes, the user is logged in, otherwise, it sends back false (and thus invalid credentials)

Concerning the update of password in the database, if the user succeed to login against the AD, I update (and encrypt) the password field of the user in the database.

So the moment I'm extending the LdapBindAuthenticatorProvider who needs a UserProviderInterface and the moment I'm playing with a user coming from database, I guess I need both of them in a ChainProvider... Am I right ?

Thanks again a hundred time for the help provided Ryan,

Kind regards,

Paul-André

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "~2.4", // v2.4.2
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.0.1
        "symfony/assetic-bundle": "~2.3", // v2.3.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.5
        "symfony/monolog-bundle": "~2.4", // v2.5.0
        "sensio/distribution-bundle": "~2.3", // v2.3.4
        "sensio/framework-extra-bundle": "~3.0", // v3.0.0
        "sensio/generator-bundle": "~2.3", // v2.3.4
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
        "ircmaxell/password-compat": "~1.0.3", // 1.0.3
        "phpunit/phpunit": "~4.1" // 4.1.0
    }
}
userVoice