0
votes

I'm working with Symfony 4.2,

I'm working with Security Component and I'm trying to add remember me.

At first Remember Me worked for me, but when my User Entity Became Customized, the remember me doesn't work anymore,

My Custom User:

I'm connected to a wordpress DB that I can't do any change on it ( I must Read Only ), And I need to add some field to the User, So I have to create a new table User OneToOne with WpUsers (wordpress Users),

So, I use doctrine to create entity from the existing DB, I didn't touch to those entities, I just create my User Entity just to add roles field to the User System:

Wordpress pass is hashed with phpass.

Entity\WpUsers (generated by doctrine):

/**
 * WpUsers
 *
 * @ORM\Table(name="wp_users", indexes={@ORM\Index(name="user_nicename", columns={"user_nicename"}), @ORM\Index(name="user_login_key", columns={"user_login"}), @ORM\Index(name="user_email", columns={"user_email"})})
 * @ORM\Entity(repositoryClass="App\Repository\WpUsersRepository")
 */
class WpUsers
{
    /**
     * @var int
     *
     * @ORM\Column(name="ID", type="bigint", nullable=false, options={"unsigned"=true})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="user_login", type="string", length=60, nullable=false)
     */
    private $userLogin = '';

    /**
     * @var string
     *
     * @ORM\Column(name="user_pass", type="string", length=255, nullable=false)
     */
    private $userPass = '';

    /**
     * @var string
     *
     * @ORM\Column(name="user_nicename", type="string", length=50, nullable=false)
     */
    private $userNicename = '';

    /**
     * @var string
     *
     * @ORM\Column(name="user_email", type="string", length=100, nullable=false)
     */
    private $userEmail = '';

    /**
     * @var string
     *
     * @ORM\Column(name="user_url", type="string", length=100, nullable=false)
     */
    private $userUrl = '';

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="user_registered", type="datetime", nullable=false, options={"default"="0000-00-00 00:00:00"})
     */
    private $userRegistered = '0000-00-00 00:00:00';

    /**
     * @var string
     *
     * @ORM\Column(name="user_activation_key", type="string", length=255, nullable=false)
     */
    private $userActivationKey = '';

    /**
     * @var int
     *
     * @ORM\Column(name="user_status", type="integer", nullable=false)
     */
    private $userStatus = '0';

    /**
     * @var string
     *
     * @ORM\Column(name="display_name", type="string", length=250, nullable=false)
     */
    private $displayName = '';

Entity\User.php:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User implements UserInterface 
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="json_array")
     */
    private $roles = [];

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\WpUsers", cascade={"persist", "remove"})
     * @ORM\JoinColumn(name="wp_user_id", referencedColumnName="ID",nullable=false)
     */
    private $wpUser;

    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUsername(): string
    {
        return $this->getWpUser()->getUserLogin();
    }

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function getPassword()
    {
        return $this->getWpUser()->getUserPass();
    }

    /**
     * @see UserInterface
     */
    public function getSalt()
    {
        // not needed for apps that do not check user passwords
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }

    public function getWpUser(): ?WpUsers
    {
        return $this->wpUser;
    }

    public function setWpUser(WpUsers $wpUser): self
    {
        $this->wpUser = $wpUser;

        return $this;
    }

}

security.yaml:

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    # encoders:
    #         App\Entity\WpUsers:
    #             algorithm: bcrypt
    providers:
        # in_memory: { memory: ~ }
        app_user_provider:
            entity:
                class: App\Entity\User
                property: wpUser.userLogin 
firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator

            remember_me:
                secret:   '%kernel.secret%'
            lifetime: 604800 # 1 week in seconds

Security\LoginFormAuthenticator:

namespace App\Security;
// use ...
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    // private $passwordEncorder;
    private $router;

    public function __construct(
            EntityManagerInterface $entityManager, 
            UrlGeneratorInterface $urlGenerator, 
            CsrfTokenManagerInterface $csrfTokenManager,
            // UserPasswordEncoderInterface $passwordEncorder,
            // PasswordHash $passwordHash
            RouterInterface $router
            )
    {
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        // $this->passwordEncorder = $passwordEncorder;
        $this->router = $router;
        $this->passwordHash = new PasswordHash(8,false);
    }

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

    public function getCredentials(Request $request)
    {
        $credentials = [
            'userLogin' => $request->request->get('userLogin'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['userLogin']
        );

        return $credentials;
    }

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

        $wpUser = $this->entityManager->getRepository(WpUsers::class)->findOneBy(['userLogin' => $credentials['userLogin']]);

        if (!$wpUser) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('User Login could not be found.');
        }

        $user = $this->entityManager->getRepository(User::class)->findOneBy(['wpUser' => $wpUser ]);

        if(!$user){
            $user = new USER();
            $user->setWpUser($wpUser);
            $this->entityManager->persist($user);
            $this->entityManager->flush();
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {

        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        // return $this->passwordEncorder->isPasswordValid($user, $credentials['password']);  
        return $this->passwordHash->CheckPassword($credentials['password'],$user->getPassword());  

        // Check the user's password or other credentials and return true or false
        // If there are no credentials to check, you can just return true
        throw new \Exception('TODO: check the credentials inside '.__FILE__);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
        // throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
        return new RedirectResponse($this->router->generate('commandes'));
    }

    protected function getLoginUrl()
    {
        return $this->urlGenerator->generate('app_login');
    }


}

login.twig.html:

{% extends 'myBase.html.twig' %}

{% block title %}Log in!{% endblock %}

{% block body %}
<form method="post">
    {% if error %}
        <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputUserLogin" class="sr-only">User Login</label>
    <input type="text" value="{{ last_username }}" name="userLogin" id="inputUserLogin" class="form-control" placeholder="User Login" required autofocus>
    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>

    <input type="hidden" name="_csrf_token"
           value="{{ csrf_token('authenticate') }}"
    >


<!--     Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
    See https://symfony.com/doc/current/security/remember_me.html -->

    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" name="_remember_me"> Remember me
        </label>
    </div>


    <button class="btn btn-lg btn-primary" type="submit">
        Sign in
    </button>
</form>
{% endblock %}
2

2 Answers

0
votes

I believe what you are missing is a supportsRememberMe() Guard Authenticator method. As you can read in the documentation:

supportsRememberMe()
    If you want to support "remember me" functionality, return true from this method. You will still 
    need to activate remember_me under your firewall for it to work...

So the solution should be adding above mentioned method to your authenticator:

public function supportsRememberMe()
{
    return true;
}
0
votes

The Answer is that I have to implement a custom User Provider because my loading User Process is not related to direct Entity.

bin/console make:user

And choose that the user shouldn't be saved in the DB, So that the CLI will create for you the UserProvider.