Raul Cejas
Raul Cejas

Reputation: 479

custom password encoder symfony 5

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.

Upvotes: 2

Views: 2437

Answers (1)

Cerad
Cerad

Reputation: 48893

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.

Upvotes: 2

Related Questions