0
votes

I'm using FOS User Bundle and I need to use a value called "maxLoginAttempts" from a record of another Entity/table that I use for my parameters.

The Entity it's called Parameters. This is my current code and I would like to change the number 5 for the value from database.

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;

/**
 * @ORM\Entity
 * @ORM\Table(name="`user`")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;

    ...

    public function isAccountNonLocked()
    {
        if($this->getLoginAttempts() >= 5) {
            return false;
        } else {
            return true;
        }
     }
 }

I imagine something like:

  $this->em->getRepository('AppBundle:Parameters')
        ->findOneBy(['name' => 'maxLoginAttempts']);

Obviously right now I don't have access to the repository. As I'm not in the Controller I'm not sure how I should use these values from inside a function of an Entity.

3

3 Answers

0
votes

You probably misunderstands the concept of an Entity:

The class - often called an "entity", meaning a basic class that holds data - is simple and helps fulfill the business requirement of needing products in your application. This class can't be persisted to a database yet - it's just a simple PHP class

That means that the Entity is just the concept, thus you can't have an access to the other Entities or to the EntityManager from inside the class.

If you want to use something like that member function you've described. you should pass maxLoginAttempts as an arg:

public function isAccountNonLocked($maxLoginAttempts)
{
    if($this->getLoginAttempts() >= maxLoginAttempts) {
        return false;
    } else {
        return true;
    }
}

In this case you need to get the value of maxLoginAttempts from your config Entity first and then use it on the object of the User you want to check:

$maxLoginAttempts = $this->em->getRepository('AppBundle:Parameters')
        ->findOneBy(['name' => 'maxLoginAttempts']);
$user = $this->em->getRepository('AppBundle:User')->find($userId);
if ($user->isAccountNonLocked($maxLoginAttempts)) {
   // do something
}
0
votes

I can think of a more proper way, IMHO, to solve this:

The User entity will have an additional property $loginAttempts that will be incremented by the incrementLoginAttempts() method each time the login fails. Its will be initialized to 0 through the ORM and a method isLocked() will tell us if we've reached 5 attempts.

<?php
// AppBundle/Entity/User.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;

/**
 * @ORM\Entity
 * @ORM\Table(name="`user`")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    protected $id;
    public function __construct()
    {
        parent::__construct();
    }

    /** 
     * @ORM\Column(type="integer",options={"default"=0}) 
     */
    private $loginAttempts;

    ...
    public function getLoginAttempts()
    {
       return $this->loginAttemps;
    }
    public function incrementLoginAttempts()
    {
       if($this->loginAttempts<5){
             $this->loginAttempts++;
       }
       return $this;
    }
    public function isLocked()
    {
        return ($this->loginAttempts == 5)
    }
    public function resetLoginAttempts()
    {
        $this->loginAttempts =0;
        return $this;
    }

Then, create an EventSubscriber to the SecuritySubscriber event, and fire an incrementLoginAttempts() each time the login fails; at the same time check if the user has already been locked or not yet

    <?php
// src/AppBundle/EventSubscriber/SecuritySubscriber.php
namespace AppBundle\EventSubscriber;

use AppBundle\Entity\User;  

class SecuritySubscriber implements EventSubscriberInterface  
{

    private $entityManager;
    private $tokenStorage;
    private $authenticationUtils;

    public function __construct(EntityManager $entityManager, TokenStorageInterface $tokenStorage, AuthenticationUtils $authenticationUtils)
    {
        $this->entityManager = $entityManager;
        $this->tokenStorage = $tokenStorage;
        $this->authenticationUtils = $authenticationUtils;
    }

    public static function getSubscribedEvents()
    {
        return array(
            AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthenticationFailure',
        );
    }

    public function onAuthenticationFailure( AuthenticationFailureEvent $event )
    {
        $existingUser = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $username]);

        if ($existingUser) {
            $existingUser->incrementLoginAttempts();
            $this->entityManager->persist($existingUser);
            $this->entityManager->flush();
            if($existingUser->isLocked()){
                // Do your logic here
                // Do not forget to un  $existingUser->resetLoginAttempts() when necessary
            }
        }
    }
}

Don't forget to register the subscriber as a service

# app/config/services.yml
services:  
    app.security.authentication_event_listener:
        class: AppBundle\EventSubscriber\SecuritySubscriber
        arguments:
            - "@doctrine.orm.entity_manager"
            - "@security.token_storage"
            - "@security.authentication_utils"

P.S: The code has not been tested.

0
votes

Finally the solution was to override the UserChecker using another functions with the same functionality.

<?php

namespace AppBundle\Checker;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Security\Core\User\UserChecker as BaseUserChecker;
use Symfony\Component\Security\Core\User\UserInterface;

class UserChecker extends BaseUserChecker
{
    private $em;

    public function __construct( EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function checkPreAuth(UserInterface $user)
    {
        //parent::checkPreAuth($user);
        $maxMinutesLocked = $this->em->getRepository('AppBundle:Parameters')->findOneBy(array('name' => 'maxTimeLocked'))->getValue();

        if (!$user instanceof AdvancedUserInterface) {
            return;
        }

        //So I just added a new function called isAccountLocked() to the User Entity that's a copy from isAccountNonLocked() but I could add a paramater
        if ($user->isAccountLocked($maxMinutesLocked)) {
            $ex = new LockedException('User account is locked.');
            $ex->setUser($user);
            throw $ex;
        }

        if (!$user->isEnabled()) {
            ...
        }
        if (!$user->isAccountNonExpired()) {
            ...
        }
    }

    public function checkPostAuth(UserInterface $user)
    {
         ...
    }
}