Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
248 views
in Technique[技术] by (71.8m points)

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

I tried to create an authenticator for my login form, but I always am unlogged for some unclear reason.

[2016-10-05 18:54:53] security.INFO: Guard authentication successful! {"token":"[object] (Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken: PostAuthenticationGuardToken(user="[email protected]", authenticated=true, roles="ROLE_USER"))","authenticator":"AppBundle\Security\Authenticator\FormLoginAuthenticator"} []
[2016-10-05 18:54:54] security.INFO: An AuthenticationException was thrown; redirecting to authentication entry point. {"exception":"[object] (Symfony\Component\Security\Core\Exception\AuthenticationExpiredException(code: 0):  at /space/products/insurance/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"} []
[2016-10-05 18:54:54] security.INFO: The security token was removed due to an AccountStatusException. {"exception":"[object] (Symfony\Component\Security\Core\Exception\AuthenticationExpiredException(code: 0):  at /space/products/insurance/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"} []

I don't understand this "AuthenticationExpiredException" as I have nothing stateless, nor any expiration in any way nowhere in my app.

Does this issue speak to anyone?


Edit 1

After a bunch of hours, it looks like I am unlogged because of the {{ is_granted('ROLE_USER') }} in Twig. Don't see why anyway.

Edit 2

If I dump() my security token on the onAuthenticationSuccess authenticator's method, authenticated = true.

But, If I dump() my security token after a redirect or when accessing a new page, 'authenticated' = false.

Why the hell my authentication isn't stored.


app/config/security.yml

security:

    encoders:
        AppBundleSecurityUserMember:
            algorithm: bcrypt
            cost: 12

    providers:
        members:
            id: app.provider.member

    role_hierarchy:
        ROLE_ADMIN:       "ROLE_USER"

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

        main:
            pattern: "^/"
            anonymous: ~
            logout: ~
            guard:
                authenticators:
                    - app.authenticator.form_login

    access_control:
        - { path: "^/connect", role: "IS_AUTHENTICATED_ANONYMOUSLY" }
        - { path: "^/register", role: "IS_AUTHENTICATED_ANONYMOUSLY" }
        - { path: "^/admin", role: "ROLE_ADMIN" }
        - { path: "^/user", role: "ROLE_USER" }
        - { path: "^/logout", role: "ROLE_USER" }

AppBundle/Controller/SecurityController.php

<?php

namespace AppBundleController;

use AppBundleBaseBaseController;
use AppBundleFormTypeConnectType;
use AppBundleSecurityUserMember;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SensioBundleFrameworkExtraBundleConfigurationTemplate;
use SymfonyComponentHttpFoundationRequest;

class SecurityController extends BaseController
{
    /**
     * @Route("/connect", name="security_connect")
     * @Template()
     */
    public function connectAction(Request $request)
    {
        $connectForm = $this
           ->createForm(ConnectType::class)
           ->handleRequest($request)
        ;

        return [
            'connect' => $connectForm->createView(),
        ];
    }
}

AppBundle/Form/Type/ConnectType.php

<?php

namespace AppBundleFormType;

use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentFormExtensionCoreType;
use SymfonyComponentValidatorConstraints;
use EWZBundleRecaptchaBundleFormTypeEWZRecaptchaType;
use EWZBundleRecaptchaBundleValidatorConstraintsIsTrue as RecaptchaTrue;

class ConnectType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
           ->add('email', TypeEmailType::class, [
               'label'    => 'Your email',
               'required' => true,
               'constraints' => [
                   new ConstraintsLength(['min' => 8])
               ],
           ])
           ->add('password', TypePasswordType::class, [
                'label'       => 'Your password',
                'constraints' => new ConstraintsLength(['min' => 8, 'max' => 4096]), /* CVE-2013-5750 */
            ])
           ->add('recaptcha', EWZRecaptchaType::class, [
               'label'       => 'Please tick the checkbox below',
               'constraints' => [
                   new RecaptchaTrue()
               ],
           ])
           ->add('submit', TypeSubmitType::class, [
               'label' => 'Connect',
           ])
        ;
    }
}

AppBundle/Security/Authenticator/FormLoginAuthenticator.php

<?php

namespace AppBundleSecurityAuthenticator;

use SymfonyComponentSecurityGuardAuthenticatorAbstractFormLoginAuthenticator;
use SymfonyComponentDependencyInjectionContainerInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreExceptionBadCredentialsException;
use AppBundleFormTypeConnectType;

class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
    private $container; // ˉ\_(ツ)_/ˉ

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function getCredentials(Request $request)
    {
        if ($request->getPathInfo() !== '/connect') {
            return null;
        }

        $connectForm = $this
           ->container
           ->get('form.factory')
           ->create(ConnectType::class)
           ->handleRequest($request)
        ;

        if ($connectForm->isValid()) {
            $data = $connectForm->getData();

            return [
                'username' => $data['email'],
                'password' => $data['password'],
            ];
        }

        return null;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        return $userProvider->loadUserByUsername($credentials['username']);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        $isValid = $this
           ->container
           ->get('security.password_encoder')
           ->isPasswordValid($user, $credentials['password'])
        ;

        if (!$isValid) {
            throw new BadCredentialsException();
        }

        return true;
    }

    protected function getLoginUrl()
    {
        return $this
           ->container
           ->get('router')
           ->generate('security_connect')
        ;
    }

    protected function getDefaultSuccessRedirectUrl()
    {
        return $this
           ->container
           ->get('router')
           ->generate('home')
        ;
    }
}

AppBundle/Security/Provider/MemberProvider.php

<?php

namespace AppBundleSecurityProvider;

use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreExceptionUsernameNotFoundException;
use SymfonyComponentSecurityCoreExceptionUnsupportedUserException;
use AppBundleSecurityUserMember;
use ApiGatewayRequestResponseRequestResponseHandlerInterface;
use ApiBusinessInsuranceWebsiteActionGetInsuranceMemberGetInsuranceMemberRequest;
use ApiGatewayExceptionNoResultException;

class MemberProvider implements UserProviderInterface
{
    protected $gateway;

    public function __construct(RequestResponseHandlerInterface $gateway)
    {
        $this->gateway = $gateway;
    }

    public function loadUserByUsername($username)
    {
        try {
            $response = $this->gateway->handle(
               new GetInsuranceMemberRequest($username)
            );
        } catch (NoResultException $ex) {
            throw new UsernameNotFoundException(
                sprintf('Username "%s" does not exist.', $username)
            );
        }

        $member = new Member();
        $member->setId($response->getId());
        $member->setUsername($response->getEmail());
        $member->setPassword($response->getPassword());
        $member->setCompanyId($response->getCompanyId());
        $member->setFirstname($response->getFirstname());
        $member->setLastname($response->getLastname());
        $member->setIsManager($response->isManager());
        $member->setIsEnabled($response->isEnabled());

        return $member;
    }

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

        return $this->loadUserByUsername($user->getUsername());
    }

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

AppBundle/Security/User/Member.php

<?php

namespace AppBundleSecurityUser;

use SymfonyComponentSecurityCoreUserUserInterface;

class Member implements UserInterface
{
    private $id;
    private $username;
    private $password;
    private $companyId;
    private $firstname;
    private $lastname;
    private $isManager;
    private $isEnabled;
    private $roles = ['ROLE_USER'];

    public function getId()
    {
        return $this->id;
    }

    public function setId($id)
    {
        $this->id = $id;

        return $this;
    }

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

    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

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

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

    public function getCompanyId()
    {
        return $this->companyId;
    }

    public function setCompanyId($companyId)
    {
        $this->companyId = $companyId;

        return $this;
    }

    public function getFirstname()
    {
        return $this->firstname;
    }

    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;

        return $this;
    }

    public function getLastname()
    {
        return $this->lastname;
    }

    public function setLastname($lastname)
    {
        $this->lastname = $lastname;

        return $this;
    }

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

    public function setIsManager($isManager)
    {
        $this->isManager = $isManager;

        return $this;
    }

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

    public function setIsEnabled($isEnabled)
    {
        $this->isE

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 SymfonyComponentSecurityCoreAuthenticationTokenAbstractToken::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 AppBundleSecurityUser;

use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreUserEquatableInterface;

class Member implements UserInterface, EquatableInterface
{

    // (...)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...