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 fieldscreatedAt
andupdatedAt
- 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
- Symfony authenticate user on every request, so it calls
getPassword()
method from user entity each time you refresh the view. - 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:dump(\unserialize($request->getSession()->get('_security_main')));
for logged in userdump($this->container->get('security.token_storage')->getToken());
for every user
- When you change password in your database and you are logged in at the same time the
PostAuthenticationGuardToken
user token is changed toAnonymousToken
and because of thatis_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
Interview Question: How to Have an Echo Before Header
Getting Elements of a Div from Another Page (Php)
Is It Acceptable to Use a Mix of Object Oriented Style with Procedural Style in Coding PHP
Nl2Br() Equivalent in JavaScript
Elegant Way to Search an PHP Array Using a User-Defined Function
Shell_Exec() Timeout Management & Exec()
Using PHP Replace Regex with Regex
Http Referer Not Always Being Passed
Soapclient: How to Pass Multiple Elements with Same Name
Key of Null Variable Equals Null Not Error
How to Validate Non-English (Utf-8) Encoded Email Address in JavaScript and PHP
Strip_Tags() Function Blacklist Rather Than Whitelist
Call to Undefined Function Oci_Connect, PHP_Oci8_12C.Dll, Windows 8.1, PHP5.6.6