Token Was Deauthenticated After Trying to Refresh It

Token was deauthenticated after trying to refresh it

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.

symfony 3.4 Refreshing a deauthenticated user is deprecated

the problem of broken authentication

2017-12-07 15:48:24] security.DEBUG: Token was deauthenticated after trying to refresh it. {"username":"aaa","provider":"Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider"} []

was, that I was not following the documentation https://symfony.com/doc/3.4/security/entity_provider.html#create-your-user-entity which says, that there should be also password field (I would not let symfony to put credentials on disk too many times). In symfony 3.3 it was ok, in symfony 3.4 the field must be present ...

diff --git a/src/GuserBundle/Entity/User.php b/src/GuserBundle/Entity/User.php
index 4adeaf9..b1b33fd 100644
--- a/src/GuserBundle/Entity/User.php
+++ b/src/GuserBundle/Entity/User.php
@@ -152,13 +152,13 @@ class User implements AdvancedUserInterface, \Serializable {
/** @see \Serializable::serialize() */
public function serialize() {
- return serialize(array($this->id, $this->username, $this->active,));
+ return serialize(array($this->id, $this->username, $this->password, $this->active, $this->locked));
}
/** @see \Serializable::unserialize() */
public function unserialize($serialized) {
- list($this->id, $this->username, $this->active,) = unserialize($serialized);
+ list($this->id, $this->username, $this->password, $this->active, $this->locked) = unserialize($serialized);
}

Cannot refresh token because user has changed - Syfmony 4 - LDAP

This issue has been fixed in symfony 4.4, in
symfony/src/Symfony/Component/Ldap/Security/LdapUserProvider.php

the password is not set to null anymore

public function refreshUser(UserInterface $user)
{
if (!$user instanceof LdapUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
}
return new LdapUser($user->getEntry(), $user->getUsername(), $user->getPassword(), $user->getRoles());
}

Symfony 5.3 new authentication doesn't keep my authentication

Like I said in my answer to the comment of Patrickkenekayoro, I forgot a change in the User entity: The getUserIdentifier() function should bring back the username, instead of the email.

symfony 4: after successful authentication it redirects to admin area and populates the TokenStorage with an anonymous Token

my colleague figured out what is the problem. Actually there are multiple problems with the code above.

  1. using GuardAuthenticator inteface has been removed from sf4:
    https://github.com/symfony/symfony/blob/4.4/UPGRADE-4.0.md#security
  2. logout_on_user_change is not necessary
  3. no need of LoginFormAuthenticator.
  4. stateless: true is a wrong setting in the firewall but when I removed it then it throw a previous error: "Cannot refresh token because user has changed. Token was deauthenticated after trying to refresh it." and it happened because
  5. in isEqualTo I checked the $this->salt !== $user->getSalt() but it was not serialized

so the working solution looks like this

  • the routing is the same
  • the backend controller is the same
  • LoginFormAuthentication.php was removed

security.yml

security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
cost: 12

providers:
user_provider:
entity:
class: AppBundle:User
property: email

firewalls:
dev:
pattern: ^/(_(profiler|wdt|error)|css|images|js)/
security: false

main:
anonymous: ~
provider: user_provider

form_login:
login_path: app_login
check_path: app_login
default_target_path: app_admin

logout:
path: app_logout

access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }

User.php

class User implements UserInterface, \Serializable, EquatableInterface
{

// ..

public function serialize()
{
return serialize(array(
$this->id,
$this->email,
$this->password,
$this->salt,
));
}

public function unserialize($serialized)
{
list (
$this->id,
$this->email,
$this->password,
$this->salt
) = unserialize($serialized, array('allowed_classes' => false));
}

public function isEqualTo(UserInterface $user)
{
if (!$user instanceof User) {
return false;
}

if ($user->getId() == $this->getId()) {
return true;
}

if ($this->password !== $user->getPassword()) {
return false;
}

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

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

return true;
}
}

Decouple authentication from your user entity in Symfony 4?

Why do I need to do this?

This is explained more in detail here

...this also comes with side effects:
You will end up with this Entity in your session Developers tend to
also use this entity in forms Session Entities If you end up with
Entities in your session, you will get synchronization issues. If you
update your entity, that means your session entity won't be updated as
it's not from the database. In order to solve this issue, you can
merge the entity back into the entity manager each request.

While this solves one of the problems, another common issue is the
(un)serialization. Eventually your User Entity will get relations to
other objects and this comes with several side-effects:

Relations will be serialized as well If a relation is lazy loaded
(standard setting), it will try to serialize the Proxy which contains
a connection. This will spew some errors on your screen as the
connection cannot be serialized. Oh and don't even think about
changing your Entity such as adding fields, this will cause
unserialization issues with incomplete objects because of missing
properties. This case is triggered for every authenticated user.

Basically the problem is that if you use the same entity for authentication AND for dealing with users/employees/clients/etc. you will come into the problem of when you change a property of the entity it will cause the authenticated user to not be in sync with what's in the database - leading to issues with roles not being correct, the user being suddenly forced to logout (thanks to the logout_on_user_change setting), or other problems depending on how the user class is used in your system.

How do I fix this?

Assumptions: I'm going to assume you have a 'User' Entity that has at least username, password and roles

In order to fix this we need to make a couple separate services that will work as a bridge between the User entity and the User for authentication.

This first is to create a security user which utilizes fields from the user class

SecurityUser /app/Security/SecurityUser.php

<?php

namespace App\Security;

use App\Entity\User;
use Symfony\Component\Security\Core\User\UserInterface;

class SecurityUser implements UserInterface, \Serializable
{
private $username;
private $password;
private $roles;

public function __construct(User $user)
{
$this->username = $user->getUsername();
$this->password = $user->getPassword();
$this->roles = $user->getRoles();
}

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

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

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

/** @see \Serializable::serialize() */
public function serialize()
{
return serialize(array(
$this->username,
$this->password,
// Should only be set if your encoder uses a salt i.e. PBKDF2
// This example uses Argon2i
// $this->salt,
));
}

/** @see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
$this->username,
$this->password,
// Should only be set if your encoder uses a salt i.e. PBKDF2
// This example uses Argon2i
// $this->salt
) = unserialize($serialized, array('allowed_classes' => false));
}

public function getRoles()
{
return $this->roles;
}

public function eraseCredentials()
{
}
}

With this, we pull the records from our User entity - which means we don't need to have a separate table for storing user information, and yet we have decoupled the authentication user from our Entity - meaning that changes to the Entity will now not directly impact the SecurityUser.

In order for Symfony to authenticate with this SecurityUser class, we will need to create a Provider:

SecurityUserProvider /app/Security/SecurityUserProvider

<?php

namespace App\Security;

use App\Repository\UserRepository;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

class SecurityUserProvider implements UserProviderInterface
{
private $userRepository;

public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}

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

public function refreshUser(UserInterface $user)
{
if (!$user instanceof SecurityUser) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}

$username = $user->getUsername();

$this->logger->info('Username (Refresh): '.$username);

return $this->fetchUser($username);
}

public function supportsClass($class)
{
return SecurityUser::class === $class;
}

private function fetchUser($username)
{
if (null === ($user = $this->userRepository->findOneBy(['username' => $username]))) {
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}

return new SecurityUser($user);
}
}

This service will basically ask query the database for the username and then the roles for the associated username. If the username is not found then it will create an error. It then return a SecurityUser object back to Symfony for authentication.

Now we need to tell Symfony to use this object

Securty.yaml /app/config/packages/security.yaml

security:
...
providers:
db_provider:
id: App\Security\SecurityUserProvider

the name "db_provider" doesn't matter - you can use anything you wish. This name is only used to map the provider to the firewall. How to configure the firewall is a little beyond the scope of this document, see here for the pretty good documentation on it. Regardless, if for some reason you are curious what mine looks like (though I won't go into explaining it):

security:
...
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
provider: db_provider
form_login:
login_path: login
check_path: login
logout:
path: /logout
target: /
invalidate_session: true

access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }

Lastly, we need to configure an encoder so that we can encrypt the passwords.

security:
...
encoders:
App\Security\SecurityUser:
algorithm: argon2i
memory_cost: 102400
time_cost: 3
threads: 4

Side note (off-topic):
Note that I'm using Argon2i. The values for the memory_cost, time_cost and threads are quite subjective depending on your system. You can see my post here which can help you get the correct values for your system

At this point your Security should be working and you have completely decoupled from your User Entity - congrats!

Other related areas of interest

Now that you have this, perhaps you should add some code so that your users sessions will be destroyed after they are Idle for so long. To do that please look at my answer here.



Related Topics



Leave a reply



Submit