2

I am trying to integrate my legacy database password validator, for that I have configured a custom encoding password: https://symfony.com/doc/current/security/named_encoders.html

I am using symfony 5.1 and php 7.4.

It is my security.yaml

security:
    encoders:
        App\Entity\User:
            algorithm: auto 
            #para oracle puse auto
        app_encoder:
            id: 'App\Security\Encoder\MyCustomPasswordEncoder'
            #para oracle
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: app_user_provider
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
            logout:
                path: app_logout
                #target: app_logout
            remember_me:
                secret:   '%kernel.secret%'
                lifetime: 2592000 # 30 days in seconds

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        #- { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

This is my password custom encoder src/Security/Encoder/MyCustomPasswordEncoder.php

<?php
namespace App\Security\Encoder;

use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class MyCustomPasswordEncoder implements UserPasswordEncoderInterface
{

    /**
     * {@inheritdoc}
     */
    public function encodePassword(UserInterface $user, string $plainPassword)
    {
        $encoder = $this->encoderFactory->getEncoder($user);

        return $encoder->encodePassword($plainPassword, $user->getSalt());
    }

    /**
     * {@inheritdoc}
     */
    public function isPasswordValid(UserInterface $user, string $raw)
    {
        if (null === $user->getPassword()) {
            return false;
        }

        die('Esta usando la mia');

        $encoder = $this->encoderFactory->getEncoder($user);

        return $encoder->isPasswordValid($user->getPassword(), $raw, $user->getSalt());
    }

    /**
     * {@inheritdoc}
     */
    public function needsRehash(UserInterface $user): bool
    {
    if (null === $user->getPassword()) {
            return false;
        }

        $encoder = $this->encoderFactory->getEncoder($user);

        return $encoder->needsRehash($user->getPassword());
    }
}

this is my src/Security/LoginFormAuthenticator.php

<?php

namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use App\Repository\UserRepository;
use Symfony\Component\Routing\RouterInterface; //segudo parametro constructor
use Symfony\Component\Security\Core\Security; //Security::

use Symfony\Component\HttpFoundation\RedirectResponse; //redirect response
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; //CSR Token
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Http\Util\TargetPathTrait;


use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; //password
use App\Security\Encoder\MyCustomPasswordEncoder;


class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;
    
    private $userRepository;
    private $router;
    private $csrfTokenManager;
    private $passwordEncoder;
    
    public function __construct(UserRepository $userRepository, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->userRepository = $userRepository;
        $this->router = $router;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
    }

    public function supports(Request $request)
    {
        return $request->attributes->get('_route') === 'app_login'
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        // todo
        //dd($request->request->all()); //esto es lo mismo que die(dump())
        /*return [
            'email' => $request->request->get('email'),
            'password' => $request->request->get('password'),
        ];*/

        $credentials = [
            'email' => $request->request->get('email'),
            'csrf_token' => $request->request->get('_csrf_token'),
            'password' => $request->request->get('password'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['email']
        );

        return $credentials;

    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        // todo
        //dd($credentials);
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        return $this->userRepository->findOneBy(['email' => $credentials['email']]);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // todo
        //dd($user);
        //return true;
        //dd($this);

        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }
        // todo
        //dd('Success');
        return new RedirectResponse($this->router->generate('app_homepage'));
    }

    protected function getLoginUrl()
    {
        // TODO: Implement getLoginUrl() method.
        return $this->router->generate('app_login');
    }
}

My problem is that it does not run my custom password validator it is taking the default password validator.

Thank you.

5
  • What exactly are you trying to achieve? To use your own encoder for all the users or just a part of them? Commented Nov 17, 2020 at 23:59
  • The link you gave talks about named encoders but I not seeing any evidence that that is what you are doing. You need to understand the difference between UserPasswordEncoderInterface and PasswordEncoderInterface. Your custom encoder would implement PasswordEncoderInterface. You map your encoder to a user in the security file. I can give you a complete example but it's not clear what exactly you are trying to do. Commented Nov 18, 2020 at 0:24
  • I need use my own encoder for all users. I do not understand the difference between UserPasswordEncoderInterface and PasswordEncoderInterface, I do not find documentation. Commented Nov 18, 2020 at 1:14
  • If I can use my own encoder I will try to add a call to a database function that validates the password in isValidPassword method Commented Nov 18, 2020 at 1:16
  • 1
    @RaulCejas This is all getting a bit messy. Some interactions just don't fit well on stackoverflow. Consider opening a post on Symfony Reddit and just reference this question here. I monitor it as well. My answer below does show injecting UserPasswordEncoderInterface works as expected when everything is wired up. "bin/console debug:container UserPasswordEncoderInterface" should yield the default "Symfony\Component\Security\Core\Encoder\UserPasswordEncoder" class. Commented Nov 19, 2020 at 12:59

1 Answer 1

2

Things can get confusing because there are two interfaces involved: PasswordEncoderInterface and UserPasswordEncoderInterface. There is a tendency to want to create a custom UserPasswordEncoderInterface because, well, you are encoding a user password. But in fact the UserPasswordEncoder object is basically just a wrapper for the underlying PasswordEncoders.

So you need to implement your legacy database password validator in a PasswordEncoder object:

namespace App\Security;

use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;

class MyPasswordEncoder implements PasswordEncoderInterface
{
    public function encodePassword(string $raw, ?string $salt)
    {
        return 'ENCODED' . $raw;
    }
    public function isPasswordValid(string $encoded, string $raw, ?string $salt)
    {
        return true;
    }
    public function needsRehash(string $encoded): bool
    {
        return false;
    }
}

Next you need to tell Symfony to use your custom encoder for a given type of user:

# config/packages/security.yaml
security:
    encoders:
        App\Entity\User:
            id: App\Security\MyPasswordEncoder

At this point you can confirm that your encoder is being used with:

$ bin/console security:encode-password xxx
Encoder used       App\Security\MyPasswordEncoder            
Encoded password   ENCODEDxxx   

That should be enough to get your going. But at the risk of adding even more confusion, here is a little test command which attempts to show the relationship UserPasswordEncoderInterface, PasswordEncoderInterface and the EncoderFactoryInterface which essentially picks the correct encoder for a given user based on the security.yaml mappings:

class UserCommand extends Command
{
    protected static $defaultName = 'app:user';

    private $encoderFactory;
    private $userPasswordEncoder;

    public function __construct(EncoderFactoryInterface $encoderFactory, UserPasswordEncoderInterface $userPasswordEncoder)
    {
        parent::__construct();
        $this->encoderFactory = $encoderFactory;
        $this->userPasswordEncoder = $userPasswordEncoder;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $encoder = $this->encoderFactory->getEncoder(User::class);
        echo get_class($encoder) . "\n";

        $user = new User();
        $encoded = $this->userPasswordEncoder->encodePassword($user,'zzz');
        echo $encoded . "\n";

        return Command::SUCCESS;
    }
}

Also wanted to point out that the link in the question points to using named encoders. Named encoders allow mapping multiple encoders to a single entity class and then allowing the entity to pick the encoder based on some property. For example, an admin user might use a different encoder than a regular user. Named encoders are not applicable to this use case.

You might however want to take a look at how to automatically upgrade passwords. Once configured users can login with the legacy encoder and then be automatically updated to a new encoder.

Sign up to request clarification or add additional context in comments.

1 Comment

Been ripping my hair out over this for 3 days. Thank you for clarifying this for us who are trying to work with legacy systems.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.