Symfony & Guard: "The Security Token Was Removed Due to an Accountstatusexception"

Symfony & Guard: The security token was removed due to an AccountStatusException

I found my bug, after 8 hours of hard work. I promise, I'll drink a bulk of beers after this comment!

I located my issue in the Symfony\Component\Security\Core\Authentication\Token\AbstractToken::hasUserChanged() method, which compares user stored in the session, and the one returned by the refreshUser of your provider.

My user entity was considered changed because of this condition:

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

In fact, before being stored in the session, the eraseCredentials() method is called on your user entity so the password is removed. But the password exists in the user the provider returns.

That's why in documentations, they show plainPassword and password properties... They keep password in the session, and eraseCredentials just cleans up `plainPassword. Kind of tricky.

Se we have 2 solutions:

  • having eraseCredentials not touching password, can be useful if you want to unauthent your member when he changes his password somehow.

  • implementing EquatableInterface in our user entity, because the following test is called before the one above.

    if ($this->user instanceof EquatableInterface) {
    return !(bool) $this->user->isEqualTo($user);
    }

I decided to implement EquatableInterface in my user entity, and I'll never forget to do it in the future.

<?php

namespace AppBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class Member implements UserInterface, EquatableInterface
{

// (...)

public function isEqualTo(UserInterface $user)
{
return $user->getId() === $this->getId();
}
}

Symfony authenticator: In controllers, Doctrine returns user item with empty string field, although a value is set

This function of your Entity/User.php is the reason of your behavior:

/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}

When authentication comes in play on Symfony, after the authentication, the AuthenticationProviderManager would call that eraseCredentials function, so it doesn't leak sensible information, or even worse, the sensible information does not end up in your session.

Just try to comment the setter in that function and you should have what you expect.

Symfony Twig Extension breaks other service - Is templating done before security?

don't interact with the tokenStorage in the constructor but only in the userHasPurchases method.

namespace AppShopBundle\Service;

use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
...

class AppShopService {
protected $tokenStorage;

public function __construct(TokenStorageInterface $tokenStorage, ...) {
$this->tokenStorage = $tokenStorage;
}

public function userHasPurchases(User $user) {
$user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
$result = $user...
return result;
}
}

Hope this help

Can't login with Symfony Guard, maybe because of cookies & multidomain

Finally, thanks to @Lynn and @Wirone on the public Symfony Slack channels, I ended up with a solution.

First, the bug was the fact that my User entity (which is serialized in the session) did not store the password.

And when a token is created, it checks if the "old" (in session) user password is the same as the "new" (refreshed) one, thanks to the Abstractoken::hasUserChanged() method.

And if the user object has changed, the firewall logs the user out.

To fix this, either serialize the password with the user, or implement EquatableInterface and use your own checks.

And this is what I did:

  • Implement EquatableInterface
  • Serialize only id and the timestampable fields createdAt and updatedAt
  • Make isEqualTo() check class and date fields to know if the user has changed.

Hope this helps /p>

How to prevent user from being unauthenticated after dynamic password change

Prelude

  1. Symfony authenticate user on every request, so it calls getPassword() method from user entity each time you refresh the view.
  2. Symfony stores only post authenticated (logged in) user data (also password hash) in serialized token PostAuthenticationGuardToken in '_security_main' session key and also every (normal or anonymous) authenticated user token inside container. To see data inside specific token use:

    1. dump(\unserialize($request->getSession()->get('_security_main'))); for logged in user
    2. dump($this->container->get('security.token_storage')->getToken()); for every user
  3. When you change password in your database and you are logged in at the same time the PostAuthenticationGuardToken user token is changed to AnonymousToken and because of that is_granted('ROLE_ADMIN') fails.

Behind the scenes

Check out namespace Symfony\Component\Security\Core\Authentication\Token: https://github.com/symfony/security-core/tree/4.2/Authentication/Token.
Click it so info below becomes 150% easier.

There is AbstractToken class that is base for PostAuthenticationGuardToken and AnonymousToken. AbstractToken has hasUserChanged(UserInterface $user) method that determines whether user sensitive data has changed.
Inside this class is another method setUser($user) that determines whether user should be authenticated (also based on hasUserChanged) and it sets user entity for token class that extends this class.

AbstractToken.php

if ($changed) {
$this->setAuthenticated(false);
}

Unauthenticated user after password change?! Yes but only for abstract class.
Normal user as well as anonymous user are authenticated. Very easy:

AnonymousToken.php

class AnonymousToken extends AbstractToken
{
private $secret;

public function __construct(string $secret, $user, array $roles = [])
{
parent::__construct($roles);
$this->secret = $secret;
$this->setUser($user);
$this->setAuthenticated(true); // here you go :-)
}
// (...)

Now the actual if that makes all the problems...

AbstractToken.php

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

compares user stored in the session, and the one returned by the refreshUser of your provider.
Symfony & Guard: "The security token was removed due to an AccountStatusException"


Solution

credits to: Alain Tiemblo

Implement EquatableInterface in you user entity

User.php

use Symfony\Component\Security\Core\User\{UserInterface, EquatableInterface};

class User implements UserInterface, EquatableInterface
{
// (...)
}

Override isEqualTo method from this interface

User.php

public function isEqualTo(UserInterface $user)
{
return $user->getId() === $this->getId();
}

Done. Now each time hasUserChanged is called when you have password changed in your database, the password check if is not executed.
Prove:

AbstractToken.php

private function hasUserChanged(UserInterface $user)
{
if (!($this->user instanceof UserInterface)) {
throw new \BadMethodCallException('Method "hasUserChanged" should be called when current user class is instance of "UserInterface".');
}

if ($this->user instanceof EquatableInterface) {
return !(bool) $this->user->isEqualTo($user);
// THIS is executed and FALSE is returned because user instances have the same ids but different passwords :)
}

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

// other checks like salt or username below

Now you can back to prelude 2.1 and 2.2 and check results.

Controller SecurityController::loginAction() requires that you provide a value for the $authenticationUtils argument

If you would use autowiring, this was easier (as you would not need any complex configuration to use services in actions) - but let's see why this currently does not work.

Through your service configuration, you provide arguments for the constructor of AppBundle\Controller\SecurityController. That class does not contain any constructor in the current state, so the class does not contain any reference to the AuthenticationUtils service.

If you don't want to use autowiring, this would help: add a constructor to your controller class, and Symfony's container will inject the service

class SecurityController extends Controller
{
private $authenticationUtils;

public function __construct(AuthenticationUtils $authenticationUtils) {
$this->authenticationUtils = $authenticationUtils;
}

public function loginAction()
{
// get the login error if there is one
$error = $this-authenticationUtils->getLastAuthenticationError();

// last username entered by the user
$lastUsername = $this-authenticationUtils->getLastUsername();

return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
}

Symfony - Authentication with an API Token - Request token user is null

So it turns out that calling $request->getUser() in the controller doesn't actually return the currently authenticated user as I would have expected it to. This would make the most sense for this object API imho.

If you actually look at the code for Request::getUser(), it looks like this:

/**
* Returns the user.
*
* @return string|null
*/
public function getUser()
{
return $this->headers->get('PHP_AUTH_USER');
}

That's for HTTP Basic Auth! In order to get the currently logged in user, you need to do this every single time:

$this->get('security.token_storage')->getToken()->getUser();

This does, indeed, give me the currently logged in user. Hopefully the question above shows how to authenticate successfully by API token anyway.

Alternatively, don't call $this->get() as it's a service locator. Decouple yourself from the controller and inject the token service instead to get the token and user from it.



Related Topics



Leave a reply



Submit