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. [] []
Advertisement
Answer
my colleague figured out what is the problem. Actually there are multiple problems with the code above.
- using GuardAuthenticator inteface has been removed from sf4: https://github.com/symfony/symfony/blob/4.4/UPGRADE-4.0.md#security
- logout_on_user_change is not necessary
- no need of LoginFormAuthenticator.
- 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
- 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; } }