Token was deauthenticated after trying to refresh it
Asked Answered
T

2

13

I updated my Symfony environment form 3.3 to 4.0. After the update I have problems with the login (user provided by database). When I submit the login form, I just got right back to the login form without any error message. When I use invalid credentials, I got the corresponding error message. Here is the log after trying to login. The login with the "in_memory" user provider is working. Do you need more information?

[2017-12-06 13:57:05] security.INFO: User has been authenticated successfully. {"username":"***"} []
[2017-12-06 14:22:39] doctrine.DEBUG: "START TRANSACTION" [] []
[2017-12-06 13:57:05] security.DEBUG: Read existing security token from the session. {"key":"_security_secured_area","token_class":"Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken"} []
[2017-12-06 13:57:05] doctrine.DEBUG: SELECT t0.username AS username_1, t0.password AS password_2, t0.email AS email_3, t0.email_new AS email_new_4, t0.first_name AS first_name_5, t0.last_name AS last_name_6, t0.is_active AS is_active_7, t0.email_confirmed AS email_confirmed_8, t0.shibboleth_state AS shibboleth_state_9, t0.shibboleth_hash AS shibboleth_hash_10, t0.shibboleth_persistent_id AS shibboleth_persistent_id_11, t0.confirmation_email_send AS confirmation_email_send_12, t0.last_login AS last_login_13, t0.expires AS expires_14, t0.session_id AS session_id_15, t0.id AS id_16, t0.hidden AS hidden_17, t0.deleted AS deleted_18, t0.created AS created_19, t0.modified AS modified_20, t0.sorting AS sorting_21, t0.salutation_id AS salutation_id_22, t0.creator_id AS creator_id_23, t0.modifier_id AS modifier_id_24 FROM User t0 WHERE t0.id = ? AND ((t0.deleted = 0)) [2] []
[2017-12-06 13:57:05] security.DEBUG: Token was deauthenticated after trying to refresh it. {"username":"user","provider":"Symfony\\Component\\Security\\Core\\User\\ChainUserProvider"} []
[2017-12-06 13:57:05] security.INFO: Populated the TokenStorage with an anonymous Token. [] []
[2017-12-06 13:57:05] security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at /vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:68)"} []
[2017-12-06 13:57:05] security.DEBUG: Calling Authentication entry point. [] []

Entity\User:

class User extends EntitySuperclass implements AdvancedUserInterface, \Serializable
{
    /**
     * @ORM\Column(type="string")
     */
    private $username;

    /**
     *
     * @Assert\Length(max=4096,groups={"account_complete","account_password","user"})
     * @Assert\Length(min = 8,groups={"account_complete","account_password","user"}, minMessage="user.password_length")
     */
    private $plainPassword;

    /**
     * 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;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank(groups={"account_register","user"})
     * @Assert\Email(
     *      groups = {"account_register", "account","user"},
     *      strict = true,
     *      checkMX = true
     * )
     */
    private $email;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $emailNew = '';

    /**
     * @ORM\ManyToOne(targetEntity="Salutation")
     * 
     */
    private $salutation;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank(groups={"account_complete","user"})
     * @Assert\Regex(pattern = "/^[a-zA-ZäöüÄÖÜß0-9 ]+$/",groups={"account_complete","user"}, message="user.first_name.regex")
     */
    private $firstName;

    /**
     * @ORM\Column(type="string")
     * @Assert\NotBlank(groups={"account_complete","user"})
     * @Assert\Regex(pattern = "/^[a-zA-ZäöüÄÖÜß0-9 ]+$/",groups={"account_complete","user"}, message="user.last_name.regex")
     */
    private $lastName;

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

    /**
     * @ORM\Column(name="email_confirmed", type="boolean")
     */
    private $emailConfirmed = false;

    /**
     * @ORM\Column(type="integer")
     */
    private $shibbolethState = 0;

    /**
     * @ORM\Column(type="string")
     */
    private $shibbolethHash = '';

    /**
     * @ORM\Column(type="string")
     */
    private $shibbolethPersistentId = '';

    /**
     * @ORM\ManyToMany(targetEntity="UserGroup")
     * @ORM\JoinTable(name="User_UserGroup",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
     *      )
     */
    private $userGroups;

    /**
     * @ORM\Column(type="integer")
     */
    private $confirmationEmailSend;

    /**
     * @ORM\Column(type="integer")
     */
    private $lastLogin = 0;

    /**
     * @ORM\Column(type="integer")
     */
    protected $expires = 0;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $sessionId = '';

    /**
     * @ORM\ManyToMany(targetEntity="BankDetails", cascade={"persist"})
     * @ORM\JoinTable(name="User_BankDetails",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="bank_details_id", referencedColumnName="id")}
     * )
     * @Assert\Valid
     */
    private $bankDetails;

    /**
     * @ORM\ManyToMany(targetEntity="Address", cascade={"persist"})
     * @ORM\JoinTable(name="User_BillingAddress",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="billing_address_id", referencedColumnName="id")}
     * )
     * @Assert\Count(
     *      min = 1,
     *      minMessage = "user.billing_addresses.min",
     * )
     * @Assert\Valid
     */
    private $billingAddresses;

    public function __construct()
    {
        parent::__construct();
        $this->isActive = true;
        $this->confirmationEmailSend = 0;
        $this->userGroups = new ArrayCollection();
        $this->bankDetails = new ArrayCollection();
        $this->billingAddresses = new ArrayCollection();
        // may not be needed, see section on salt below
        // $this->salt = md5(uniqid(null, true));
    }

    /**
     * @ORM\PrePersist
     */
    public function prePersist()
    {
        $currentTimestamp = time();

        if($this->getConfirmationEmailSend() == NULL)
            $this->setConfirmationEmailSend(0);

   }

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

    public function getSalt()
    {
        // The bcrypt algorithm doesn't require a separate salt.
        return null;
    }

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

    public function getRoles()
    {
        $roles = array();
        $userGroups = $this->getUserGroups();
        if(!empty($userGroups)) {
            foreach($userGroups as $userGroup) {
                $role = $userGroup->getRole();
                $roles[] = 'ROLE_'.strtoupper($role);
            }
        }
        return $roles;
    }

    public function isGranted($role)
    {
        return in_array($role, $this->getRoles());
    }

    public function eraseCredentials()
    {
    }

    public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

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

    /** @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);
    }

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

        return $this;
    }

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }

    /**
     * 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;
        $this->setUsername($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;
    }

    /**
     * Add userGroup
     *
     * @param \AppBundle\Entity\UserGroup $userGroup
     *
     * @return User
     */
    public function addUserGroup(\AppBundle\Entity\UserGroup $userGroup)
    {
        $this->userGroups[] = $userGroup;

        return $this;
    }

    /**
     * Remove userGroup
     *
     * @param \AppBundle\Entity\UserGroup $userGroup
     */
    public function removeUserGroup(\AppBundle\Entity\UserGroup $userGroup)
    {
        $this->userGroups->removeElement($userGroup);
    }

    /**
     * Get userGroups
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getUserGroups()
    {
        return $this->userGroups;
    }

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

        return $this;
    }

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

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

        return $this;
    }

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

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

        return $this;
    }

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

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

        return $this;
    }

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

    public function removeAllUserGroups() {
        $userGroups = $this->getUserGroups();
        foreach($userGroups as $userGroup) {
           $this->removeUserGroup($userGroup);
        }
    }

    public function hasUserGroup($userGroupId) {
        foreach($this->getUserGroups() as $userGroup) {
            if($userGroup->getId() == $userGroupId)
                return true;
        }
        return false;
    }

    /**
     * Set lastLogin
     *
     * @param integer $lastLogin
     *
     * @return User
     */
    public function setLastLogin($lastLogin)
    {
        $this->lastLogin = $lastLogin;

        return $this;
    }

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

    /**
     * Set confirmationEmailSend
     *
     * @param integer $confirmationEmailSend
     *
     * @return User
     */
    public function setConfirmationEmailSend($confirmationEmailSend)
    {
        $this->confirmationEmailSend = $confirmationEmailSend;

        return $this;
    }

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

    /**
     * Set validTill
     *
     * @param integer $validTill
     *
     * @return User
     */
    public function setValidTill($validTill)
    {
        $this->validTill = $validTill;

        return $this;
    }

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

    /**
     * Set shibbolethValid
     *
     * @param integer $shibbolethValid
     *
     * @return User
     */
    public function setShibbolethValid($shibbolethValid)
    {
        $this->shibbolethValid = $shibbolethValid;

        return $this;
    }

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

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

        return $this;
    }

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

    /**
     * Set shibbolethState
     *
     * @param integer $shibbolethState
     *
     * @return User
     */
    public function setShibbolethState($shibbolethState)
    {
        $this->shibbolethState = $shibbolethState;

        return $this;
    }

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

    /**
     * Set expires
     *
     * @param integer $expires
     *
     * @return User
     */
    public function setExpires($expires)
    {
        $this->expires = $expires;

        return $this;
    }

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

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

        return $this;
    }

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

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

        return $this;
    }

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

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

        return $this;
    }

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

    /**
     * Set salutation
     *
     * @param \AppBundle\Entity\Salutation $salutation
     *
     * @return User
     */
    public function setSalutation(\AppBundle\Entity\Salutation $salutation = null)
    {
        $this->salutation = $salutation;

        return $this;
    }

    /**
     * Get salutation
     *
     * @return \AppBundle\Entity\Salutation
     */
    public function getSalutation()
    {
        return $this->salutation;
    }

    /**
     * Add bankDetail
     *
     * @param \AppBundle\Entity\BankDetails $bankDetail
     *
     * @return User
     */
    public function addBankDetail(\AppBundle\Entity\BankDetails $bankDetail)
    {
        $this->bankDetails[] = $bankDetail;

        return $this;
    }

    /**
     * Remove bankDetail
     *
     * @param \AppBundle\Entity\BankDetails $bankDetail
     */
    public function removeBankDetail(\AppBundle\Entity\BankDetails $bankDetail)
    {
        $this->bankDetails->removeElement($bankDetail);
    }

    /**
     * Get bankDetails
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getBankDetails()
    {
        return $this->bankDetails;
    }

    /**
     * Add billingAddress
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     */
    public function addBillingAddress(\AppBundle\Entity\Address $billingAddress)
    {
        $this->billingAddresses[] = $billingAddress;

        return $this;
    }

    /**
     * Remove billingAddress
     *
     * @param \AppBundle\Entity\Address $billingAddress
     */
    public function removeBillingAddress(\AppBundle\Entity\Address $billingAddress)
    {
        $this->billingAddresses->removeElement($billingAddress);
    }

    /**
     * Set billingAddresses
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     * 
     */
    public function setBillingAddresses(\AppBundle\Entity\Address $billingAddress)
    {
        if($this->billingAddresses !== NULL and $this->billingAddresses->contains($billingAddress)){
            return false;
        }
        $this->addBillingAddress($billingAddress);
        return $this;
    }

    /**
     * Set one billingAddresses
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     * 
     */
    public function setOneBillingAddresses(\AppBundle\Entity\Address $billingAddress)
    {
        $this->billingAddresses = $billingAddress;

        return $this;
    }

    /**
     * Set one billingAddresses
     *
     * @param \AppBundle\Entity\Address $billingAddress
     *
     * @return User
     * 
     */
    public function unsetBillingAddresses()
    {
        $this->billingAddresses = new ArrayCollection();

        return $this;
    }

    /**
     * Get billingAddresses
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getBillingAddresses()
    {
        return $this->billingAddresses;
    }
}

config/security.yml

providers:
        chain_provider:
            chain:
                providers: [in_memory, database_user]
        in_memory:
            memory:
                users:
                    admin:
                        password: ***
                        roles: 'ROLE_ADMIN'
        database_user:
            entity:
                class: AppBundle:User

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        secured_area:
            # pattern: match to pages
            anonymous: ~
            pattern:    ^/
            access_denied_handler: AppBundle\Security\AccessDeniedHandler
            provider: chain_provider
            form_login:
                login_path: /login
                check_path: /login_check
                default_target_path: account
                # Configuring CSRF protection
                csrf_parameter: _csrf_security_token
                csrf_token_id: a_private_string
                success_handler: AppBundle\Handler\LoginSuccessHandler
            logout:
                path: /logout
                target: /login

    access_control:
        ...

    role_hierarchy:
        ...

    encoders:
        AppBundle\Entity\User:
            algorithm: bcrypt
        Symfony\Component\Security\Core\User\User:
            plaintext
Tellus answered 6/12, 2017 at 13:19 Comment(1)
yes, I confirm Serializable interface working but you should compare $id, $username and $password into for it works but with Symfony 4, you should implements EquatableInterface and isEqualTo method inside. Symfony was not updated, I created pr github.com/symfony/symfony-docs/pull/9914Cal
D
44

As of Symfony 4.0, logout_on_user_change is set to true. That means a user will be logged out if it has been changed.

You should implement Symfony\Component\Security\Core\User\EquatableInterface and add the isEqualTo method:

class User implements EquatableInterface
{
    public function isEqualTo(UserInterface $user)
    {
        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->salt !== $user->getSalt()) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }
}

Changelog

https://github.com/symfony/security-bundle/blob/master/CHANGELOG.md

4.1.0

The logout_on_user_change firewall option is deprecated and will be removed in 5.0.

4.0.0

the firewall option logout_on_user_change is now always true, which will trigger a logout if the user changes between requests

3.4.0

Added logout_on_user_change to the firewall options. This config item will trigger a logout when the user has changed. Should be set to true to avoid deprecations in the configuration.

The option wasn't documented by the time of writing this answer: https://github.com/symfony/symfony-docs/issues/8428, but it now is: https://symfony.com/doc/4.4/reference/configuration/security.html#logout-on-user-change

Side note on updating to a new major release

If you want to upgrade to a new major version, always update to the latest minor version first. That means update to 2.8 before updating to 3.0 and updating to 3.4 before going to 4.0. See Symfony 4: Compose your Applications by Fabien Potencier.

Symfony 3.0 = Symfony 2.8 - deprecated features

(..)

Symfony 4.0 = Symfony 3.4 - deprecated features + a new way to develop applications

Updating to a new major release is much easier if you're already on the latest minor release, because you can see all deprecation notices.

Disherison answered 6/12, 2017 at 13:59 Comment(8)
Thanks a lot! I checked the code for hours but did not find anything by myself! Should not this be added to the documentation of entity provider?Tellus
You're welcome! I search for hours too, a couple of days ago, so happy to share the solution. I don't know where it should be documented, but at least it should be somewhere.Disherison
Hello, I have exactly the same problem but this solution doesn't work :( Any idea ?Blithesome
Ok I finaly found ! in the serialize function I had to put the salt because i use sha512 encryptionBlithesome
Hello, which function did you mean?Cosimo
I think ArGh had another problem which is not related to this one.Disherison
@StephanVierkant I had the same issue as ArGh and it was necessary that the fields of isEqualTo() are matching the fields in the serialize() functionAstarte
Hi @StephanVierkant, in my case, $this->password, $this->salt and $this->username are always null. What am I doing wrong? Thanks!Pesticide
S
2

I haved the problem du to the getRoles function. My user didn't have any roles

When the token is contruct in UsernamePasswordToken , the token is not authenticated if there is empty roles :

class UsernamePasswordToken extends AbstractToken
{
..
    public function __construct($user, $credentials, string $providerKey, array $roles = [])
    {
        parent::__construct($roles);
    ...

        parent::setAuthenticated(\count($roles) > 0);
    }

In other word, when user have empty roles, he is not authenticated.

I solved my problem by coding getRoles in my user class like the current doc https://symfony.com/doc/current/security.html#roles to guarantee every user at least has ROLE_USER

public function getRoles()
{
    $roles = $this->roles;
    $roles[] = 'ROLE_USER';    
    return array_unique($roles);
}

Hope that help.

Stepaniestepbrother answered 2/3, 2020 at 12:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.