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

Tags: , ,



I had an old Symfony 3.1 site that I upgraded to Symfony 3.4.x then to Symfony 4.4.11 but I didn’t upgrade it to symfony flex. I fixed many things and the public sites seem working.

I had to rebuild the authentication because the old one was not compatible with sf4.

I followed this https://symfony.com/doc/4.4/security/form_login_setup.html

and this: https://symfonycasts.com/screencast/symfony-security/make-user

I ended up in a situation that after a successful authentication when it redirects to the admin area then it always checks the LoginFormAuthenticator again which obviously doesn’t support the admin area and it redirects back to the login page with anonyous user.

There are many discussions about this issue and tried out all what I found but I didn’t find the solution. Not even with debugging it.

The session saved in the defined path. Its id is same like the PHPSESSID in the browser. Site runs HTTP protocol.

security.yml

security:
    encoders:
        AppBundleEntityUser:
            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:
            stateless: true
            pattern: ^/
            anonymous: true
            logout_on_user_change: true

            guard:
                authenticators:
                    - AppBundleSecurityLoginFormAuthenticator

            form_login:
                provider: user_provider
                username_parameter: email
                csrf_token_generator: security.csrf.token_manager
                login_path: app_login

            logout:
                path: app_logout


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

routing:

app_login:
    path:     /login
    defaults: { _controller: AppBundleControllerBackendController:loginAction }

app_logout:
    path:     /logout
    defaults: { _controller: AppBundleControllerBackendController:logoutAction }

app_admin:
    path:         /admin/{page}/{entry}
    defaults:     { _controller: AppBundleControllerBackendController:showAction, entry: null }

User.php

<?php

namespace AppBundleEntity;

use DoctrineORMMapping as ORM;
use SymfonyComponentSecurityCoreUserEquatableInterface;
use SymfonyComponentSecurityCoreUserUserInterface;

/**
 * User
 *
 * @ORMTable(name="user")
 * @ORMEntity(repositoryClass="AppBundleRepositoryUserRepository")
 */
class User implements UserInterface, Serializable, EquatableInterface
{
    private $id;
    // and so on

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

    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->email,
            $this->password,
        ) = unserialize($serialized);
    }

    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }

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

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

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

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

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

        return true;
    }

}

backend controller:

class BackendController extends AbstractController
{
    public function loginAction(AuthenticationUtils $authenticationUtils)
    {
        return $this->render('AppBundle:Backend:page.html.twig', array(
            'email' => $authenticationUtils->getLastUsername(),
            'error' => $authenticationUtils->getLastAuthenticationError()
        ));
    }

    public function logoutAction()
    {
        $this->container->get('security.token_storage')->setToken(null);
        $this->container->get('request')->getSession()->invalidate();
    }

    public function showAction(Request $request, $page, $entry)
    {
        $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');

        // some logic
    }
}

LoginFormAuthentication.php

looks like the same in the example and it works. It successfully reaches the onAuthenticationSuccess() and redirects to the admin area.

dev.log

request.INFO: Matched route "app_login". {"route":"app_login"..}
security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"AppBundle\Security\LoginFormAuthenticator"} []
security.DEBUG: Calling getCredentials() on guard authenticator. {"firewall_key":"main","authenticator":"AppBundle\Security\LoginFormAuthenticator"} []
security.DEBUG: Passing guard token information to the GuardAuthenticationProvider {"firewall_key":"main","authenticator":"AppBundle\Security\LoginFormAuthenticator"} []
doctrine.DEBUG: SELECT t0.* FROM user t0 WHERE t0.email = ? LIMIT 1 ["email@me.com"] []
security.INFO: Guard authentication successful! {"token":"[object] (Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken: PostAuthenticationGuardToken(user="email@me.com", authenticated=true, roles="ROLE_ADMIN"))","authenticator":"AppBundle\Security\LoginFormAuthenticator"} []
security.DEBUG: Guard authenticator set success response. Redirect response
security.DEBUG: Remember me skipped: it is not configured for the firewall.
security.DEBUG: The "AppBundleSecurityLoginFormAuthenticator" authenticator set the response. Any later authenticator will not be called {"authenticator":"AppBundle\Security\LoginFormAuthenticator"} []

after the redirection:

request.INFO: Matched route "app_admin". {"route":"app_admin" ..}
security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"AppBundle\Security\LoginFormAuthenticator"} []
security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"AppBundle\Security\LoginFormAuthenticator"} []
security.INFO: Populated the TokenStorage with an anonymous Token. [] []
security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point.
security.DEBUG: Calling Authentication entry point. [] []

Answer

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:
        AppBundleEntityUser:
            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;
    }
}


Source: stackoverflow