Search code examples
phpencodingsymfony-formssymfonysymfony-security

Symfony3 Extending UserRepository With UserLoaderInterface & Using Encryption, Login Not Working


I can't login to my restricted areas. I'm following the tutorials on symfony.com.

I'll provide my security.yml file.

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
            cost: 12
        AppBundle\Entity\User:
            algorithm: bcrypt
            cost: 12

    providers:
        # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
         our_db_provider:
            entity:
                class: AppBundle:User
                # if you're using multiple entity managers
                # manager_name: customer

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

        main:
            pattern: ^/
            http_basic: ~
            provider: our_db_provider

            anonymous: ~
            # 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

            logout:
                path:   /logout
                target: /

    access_control:
        # require ROLE_ADMIN for /admin*
        #- { path: ^/admin, roles: ROLE_ADMIN }

Next here is my User class

<?php
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;

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

/**
 * @ORM\Table(name="app_users")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
 */
class User implements AdvancedUserInterface, \Serializable
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=25, unique=true)
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=64)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=60, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */
    private $isActive;

    public function __construct()
    {
        $this->isActive = true;
        // may not be needed, see section on salt below
        // $this->salt = md5(uniqid(null, true));
    }

    public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

    public function isEnabled()
    {
        return $this->isActive;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function getSalt()
    {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getRoles()
    {
        return array('ROLE_USER','ROLE_ADMIN');
    }

    public function eraseCredentials()
    {
    }

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            $this->active
            // see section on salt below
            // $this->salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            $this->active
            // see section on salt below
            // $this->salt
        ) = unserialize($serialized);
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     *
     * @return User
     */
    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

    /**
     * Set password
     *
     * @param string $password
     *
     * @return User
     */
    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Set email
     *
     * @param string $email
     *
     * @return User
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Get email
     *
     * @return string
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set isActive
     *
     * @param boolean $isActive
     *
     * @return User
     */
    public function setIsActive($isActive)
    {
        $this->isActive = $isActive;

        return $this;
    }

    /**
     * Get isActive
     *
     * @return boolean
     */
    public function getIsActive()
    {
        return $this->isActive;
    }
}

and finally the User Repository class

<?php
// src/AppBundle/Repository/UserRepository.php
namespace AppBundle\Repository\Entity;

use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository implements UserLoaderInterface
{
    public function loadUserByUsername($username)
    {
        $user = $this->createQueryBuilder('u')
            ->where('u.username = :username OR u.email = :email')
            ->setParameter('username', $username)
            ->setParameter('email', $username)
            ->getQuery()
            ->getOneOrNullResult();

        if (null === $user) {
            $message = sprintf(
                'Unable to find an active admin AppBundle:User object identified by "%s".',
                $username
            );
            throw new UsernameNotFoundException($message);
        }

        return $user;
    }
}
?>

My route for admin looks like below:

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        // replace this example code with whatever you need
        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
        ]);
    }

    /**
     * @Route("/admin")
     * @Security("has_role('ROLE_USER') or has_role('ROLE_ADMIN')")
     */
    public function adminAction()
    {
        return new Response('<html><body>Admin page!</body></html>');
    }
}

I was trying to follow the instructions here http://symfony.com/doc/current/cookbook/security/entity_provider.html#using-a-custom-query-to-load-the-user so that I could login with the username or email. When I go to /admin however no matter how I type the email or username into the http_basic prompt I can't seem to get in.

I'm guessing it might have something to do with encoding maybe? so in the security.yml file I put both encoder types, but it doesn't work either with just 1 or the other.

Keep in mind /admin has security role user or admin, so since I default return the role in the class of ROLE_USER it should still be able to get in.

If there is an error log somewhere I'm not sure how to find it. Help would be appreciated here since I'm still new to Symphony.

EDIT:

Forgot to mention in the database, I currently have the following::

1   admin   $2y$12$qvLb/T2Xs4aWsFU6D4U2f.gmZi/blKYtspXeqiPLrHPFOPxwMaHY.    joe@domain.com  1

2   joe $2y$12$M/7nTIEfQ1Ajpr/IhmEVoejskvt0dIb/FfIvT8i9LXdSR95zjT5OS    joe@someotherdomain.com 1

The columns are ID, username, password, email, is_active

The encryption I did manually using: bin/console security:encode-password and then put in the database fields. This worked previously for some other logging in tests I did, but just in case this is the problem putting this here. I tried putting just plaintext in the database also and typing that in to login and did not work either.

Thank you!


Solution

  • The issue was related to my namespace. When using HTTP_BASIC there was no easy way to see the error, but the name space on UserRepository.php should have been namespace AppBundle\Repository; instead of namespace AppBundle\Repository\Entity; which in retrospect makes no sense at all now since the file path wasn't even close to that.

    I did make some changes to my security.yml file as well which may have helped I'll post it below.

    Security.yml

    # To get started with security, check out the documentation:
    # http://symfony.com/doc/current/book/security.html
    security:
        encoders:
            Symfony\Component\Security\Core\User\User:
                algorithm: bcrypt
                cost: 12
            AppBundle\Entity\User:
                algorithm: bcrypt
                cost: 12
    
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    
        providers:
            # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
             database:
                  entity: { class: AppBundle:User }
                    #property: username
                    # if you're using multiple entity managers
                    # manager_name: customer
    
        firewalls:
            # disables authentication for assets and the profiler, adapt it according to your needs
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
            main:
                pattern: ^/
                anonymous: true
                # activate different ways to authenticate
    
                http_basic:
                    provider: database
                # 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
                #form_login:
                    #check_path: /login_check
                    #login_path: /login
                    #default_target_path: /home
                    #always_use_default_target_path: true
                    #provider: database
                logout:
                    path:   /logout
                    target: /
    
        access_control:
            # require ROLE_ADMIN for /admin*
            #- { path: ^/admin, roles: ROLE_ADMIN }
            - { path: ^/admin, roles: ROLE_USER }
    

    In particular,I gave a provider to http_basic so it knows to use my database as the source for logging in. The reason I had 2 encoders was so I could use the console command bin/console security:encode-passwordsince that console expects that particular class to work. I just gave it the same algorithm for hashing with the same cost so that when I use it to generate passwords for my users it will work with the AppBundle\Entity\User type as well. When you are creating users manually without a registration form that console command comes in handy to do the bcrypt hashing.

    User.php

    <?php
    // src/AppBundle/Entity/User.php
    namespace AppBundle\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    use Symfony\Component\Security\Core\User\AdvancedUserInterface;
    
    /**
     * @ORM\Table(name="app_users")
     * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
     * @UniqueEntity(fields="email", message="Email already taken")
     * @UniqueEntity(fields="username", message="Username already taken")
     */
    class User implements AdvancedUserInterface, \Serializable
    {
        /**
         * @ORM\Column(type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @ORM\Column(type="string", length=25, unique=true)
         * @Assert\NotBlank()
         */
        private $username;
    
        /**
         * The below length depends on the "algorithm" you use for encoding
         * the password, but this works well with bcrypt
         *
         * @ORM\Column(type="string", length=64)
         */
        private $password;
    
        /**
         * @Assert\NotBlank()
         * @Assert\Length(max = 4096)
         */
        private $plainPassword;
    
        /**
         * @ORM\Column(type="string", length=60, unique=true)
         * @Assert\NotBlank()
         * @Assert\Email()
         */
        private $email;
    
        /**
         * @ORM\Column(name="is_active", type="boolean")
         */
        private $isActive;
    
        public function __construct()
        {
            $this->isActive = true;
            // may not be needed, see section on salt below
            // $this->salt = md5(uniqid(null, true));
        }
    
        public function isAccountNonExpired()
        {
            return true;
        }
    
        public function isAccountNonLocked()
        {
            return true;
        }
    
        public function isCredentialsNonExpired()
        {
            return true;
        }
    
        public function isEnabled()
        {
            return $this->isActive;
        }
    
        public function getUsername()
        {
            return $this->username;
        }
    
        public function getSalt()
        {
            // you *may* need a real salt depending on your encoder
            // see section on salt below
            return null;
        }
    
        public function getPassword()
        {
            return $this->password;
        }
    
        public function getPlainPassword(){
            return $this->plainPassword;
        }
    
        public function getRoles()
        {
            return array('ROLE_USER');
        }
    
        public function eraseCredentials()
        {
        }
    
        /** @see \Serializable::serialize() */
        public function serialize()
        {
            return serialize(array(
                $this->id,
                $this->username,
                $this->password,
                $this->isActive
                // see section on salt below
                // $this->salt,
            ));
        }
    
        /** @see \Serializable::unserialize() */
        public function unserialize($serialized)
        {
            list (
                $this->id,
                $this->username,
                $this->password,
                $this->isActive
                // see section on salt below
                // $this->salt
            ) = unserialize($serialized);
        }
    
        /**
         * Get id
         *
         * @return integer
         */
        public function getId()
        {
            return $this->id;
        }
    
        /**
         * Set username
         *
         * @param string $username
         *
         * @return User
         */
        public function setUsername($username)
        {
            $this->username = $username;
    
            return $this;
        }
    
        /**
         * Set password
         *
         * @param string $password
         *
         * @return User
         */
        public function setPassword($password)
        {
            $this->password = $password;
    
            return $this;
        }
    
        /**
         * Set plainPassword
         *
         * @param string $plainPassword
         *
         * @return User
         */
        public function setPlainPassword($plainPassword)
        {
            $this->plainPassword = $plainPassword;
    
            return $this;
        }
    
        /**
         * Set email
         *
         * @param string $email
         *
         * @return User
         */
        public function setEmail($email)
        {
            $this->email = $email;
    
            return $this;
        }
    
        /**
         * Get email
         *
         * @return string
         */
        public function getEmail()
        {
            return $this->email;
        }
    
        /**
         * Set isActive
         *
         * @param boolean $isActive
         *
         * @return User
         */
        public function setIsActive($isActive)
        {
            $this->isActive = $isActive;
    
            return $this;
        }
    
        /**
         * Get isActive
         *
         * @return boolean
         */
        public function getIsActive()
        {
            return $this->isActive;
        }
    }
    

    UserRepository.php

    <?php
    // src/AppBundle/Repository/UserRepository.php
    namespace AppBundle\Repository;
    
    use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
    use Doctrine\ORM\EntityRepository;
    
    class UserRepository extends EntityRepository implements UserLoaderInterface
    {
        public function loadUserByUsername($username)
        {
            $user = $this->createQueryBuilder('u')
                ->where('u.username = :username OR u.email = :email')
                ->setParameter('username', $username)
                ->setParameter('email', $username)
                ->getQuery()
                ->getOneOrNullResult();
    
            if (null === $user) {
                $message = sprintf(
                    'Unable to find an active admin AppBundle:User object identified by "%s".',
                    $username
                );
                throw new UsernameNotFoundException($message);
            }
    
            return $user;
        }
    }
    ?>
    

    DefaultController.php

    <?php
    
    namespace AppBundle\Controller;
    
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
    
    class DefaultController extends Controller
    {
        /**
         * @Route("/", name="homepage")
         */
        public function indexAction(Request $request)
        {
            // replace this example code with whatever you need
            return $this->render('default/index.html.twig', [
                'base_dir' => realpath($this->container->getParameter('kernel.root_dir').'/..'),
            ]);
        }
    
        /**
         * @Route("/admin")
         */
        public function adminAction()
        {
            return new Response('<html><body>Admin page!</body></html>');
        }
    }