I built a login form following this form login setup doc.
This is working fine on localhost but not on the production server.
On both localhost and prod, authentication begins successfully
- Guard authentication successful
- Guard authenticator set success response
- Stored the security token in the session
Matched route "easyadmin
### var/log/prod.log output with info level[2019-07-05 10:28:46] request.INFO: Matched route "app_login". {"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"https://example.com/login","method":"POST"} [][2019-07-05 10:28:46] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [][2019-07-05 10:28:46] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: Calling getCredentials() on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: Passing guard token information to the GuardAuthenticationProvider {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] php.INFO: User Deprecated: The "Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder" class is deprecated since Symfony 4.3, use "Symfony\Component\Security\Core\Encoder\NativePasswordEncoder" instead. {"exception":"[object] (ErrorException(code: 0): User Deprecated: The \"Symfony\\Component\\Security\\Core\\Encoder\\BCryptPasswordEncoder\" class is deprecated since Symfony 4.3, use \"Symfony\\Component\\Security\\Core\\Encoder\\NativePasswordEncoder\" instead. at /var/www/clients/client0/web4/web/vendor/symfony/security-core/Encoder/BCryptPasswordEncoder.php:14)"} [][2019-07-05 10:28:46] security.INFO: Guard authentication successful! {"token":"[object] (Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken: PostAuthenticationGuardToken(user=\"myemail@gmail.com\", authenticated=true, roles=\"ROLE_EDITOR, ROLE_USER\"))","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: Guard authenticator set success response. {"response":"[object] (Symfony\\Component\\HttpFoundation\\RedirectResponse: HTTP/1.0 302 Found\r\nCache-Control: no-cache, private\r\nDate: Fri, 05 Jul 2019 10:28:46 GMT\r\nLocation: /backoffice\r\n\r\n<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n <meta http-equiv=\"refresh\" content=\"0;url=/backoffice\" />\n\n <title>Redirecting to /backoffice</title>\n </head>\n <body>\n Redirecting to <a href=\"/backoffice\">/backoffice</a>.\n </body>\n</html>)","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: Remember me skipped: it is not configured for the firewall. {"authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: The "App\Security\LoginFormAuthenticator" authenticator set the response. Any later authenticator will not be called {"authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: Stored the security token in the session. {"key":"_security_main"} [][2019-07-05 10:28:46] request.INFO: Matched route "easyadmin". {"route":"easyadmin","route_parameters":{"_controller":"Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction","path":"/backoffice/","permanent":true,"scheme":null,"httpPort":80,"httpsPort":443,"_route":"easyadmin"},"request_uri":"https://example.com/backoffice","method":"GET"} []
But while in localhost, I am correctly redirected to the backoffice :
- Read existing security token from the session
User was reloaded from a user provider
### var/log/prod.log (following lines, localhost) [2019-07-05 10:19:29] security.DEBUG: Read existing security token from the session. {"key":"_security_main","token_class":"Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken"} [][2019-07-05 10:19:29] security.DEBUG: User was reloaded from a user provider. {"provider":"Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider","username":"raoux.thierry@free.fr"} [][2019-07-05 10:19:29] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [][2019-07-05 10:19:29] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:19:29] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:19:29] cache.INFO: Lock acquired, now computing item "easyadmin.processed_config" {"key":"easyadmin.processed_config"} []
In prod environment, instead :
- it skips step : reading existing security token
- does not refresh user as expected
- instead it populates the TokenStorage with an anonymous Token
Acces denied and back to login url
### var/log/prod.log (same following lines, but from production server) [2019-07-05 10:28:46] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} [][2019-07-05 10:28:46] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} [][2019-07-05 10:28:46] security.INFO: Populated the TokenStorage with an anonymous Token. [] [][2019-07-05 10:28:46] security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at /var/www/clients/client0/web4/web/vendor/symfony/security-http/Firewall/AccessListener.php:72)"} [][2019-07-05 10:28:46] security.DEBUG: Calling Authentication entry point. [] [][2019-07-05 10:28:46] request.INFO: Matched route "app_login". {"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"https://example.com/login","method":"GET"} []
security.yaml
security: encoders: App\Entity\User: algorithm: bcrypt providers: app_user_provider: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: anonymous: true guard: authenticators: - App\Security\LoginFormAuthenticator logout: path: app_logout access_control: - { path: ^/backoffice, roles: ROLE_EDITOR} # requires_channel: https
routes.yaml
admin: path: /backoffice controller: EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController
LoginFormAuthenticator
// use...class LoginFormAuthenticator extends AbstractFormLoginAuthenticator{ use TargetPathTrait; private $entityManager; private $urlGenerator; private $csrfTokenManager; private $passwordEncoder; public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) { $this->entityManager = $entityManager; $this->urlGenerator = $urlGenerator; $this->csrfTokenManager = $csrfTokenManager; $this->passwordEncoder = $passwordEncoder; } public function supports(Request $request) { return 'app_login' === $request->attributes->get('_route')&& $request->isMethod('POST'); } public function getCredentials(Request $request) { $credentials = ['email' => $request->request->get('email'),'password' => $request->request->get('password'),'csrf_token' => $request->request->get('_csrf_token'), ]; $request->getSession()->set( Security::LAST_USERNAME, $credentials['email'] ); return $credentials; } public function getUser($credentials, UserProviderInterface $userProvider) { $token = new CsrfToken('authenticate', $credentials['csrf_token']); if (!$this->csrfTokenManager->isTokenValid($token)) { throw new InvalidCsrfTokenException(); } $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); if (!$user) { // fail authentication with a custom error throw new CustomUserMessageAuthenticationException('Email could not be found.'); } return $user; } public function checkCredentials($credentials, UserInterface $user) { return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { return new RedirectResponse($targetPath); } return new RedirectResponse($this->urlGenerator->generate('admin')); } protected function getLoginUrl() { return $this->urlGenerator->generate('app_login'); }}
Security controller
// use...class SecurityController extends AbstractController{ /** * @Route("/login", name="app_login") */ public function login(AuthenticationUtils $authenticationUtils): Response { // get the login error if there is one $error = $authenticationUtils->getLastAuthenticationError(); // last username entered by the user $lastUsername = $authenticationUtils->getLastUsername(); return $this->render('security/login.html.twig', ['last_username' => $lastUsername,'error' => $error, ] ); } /** * @Route("/logout", name="app_logout") * @return \Symfony\Component\HttpFoundation\RedirectResponse */ public function logout() { return $this->redirectToRoute('home'); }}//... skipped forgottenPassword and resetPassword methods
EDIT:
php bin/console debug:config security
output
Current configuration for extension with alias "security"=========================================================security:encoders: App\Entity\User: algorithm: bcrypt hash_algorithm: sha512 key_length: 40 ignore_case: false encode_as_base64: true iterations: 5000 cost: null memory_cost: null time_cost: null threads: nullproviders: app_user_provider: entity: class: App\Entity\User property: email manager_name: nullfirewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false methods: { } user_checker: security.user_checker stateless: false logout_on_user_change: true main: anonymous: secret: null guard: authenticators: - App\Security\LoginFormAuthenticator entry_point: null logout: path: app_logout csrf_parameter: _csrf_token csrf_token_id: logout target: / invalidate_session: true delete_cookies: { } handlers: { } methods: { } security: true user_checker: security.user_checker stateless: false logout_on_user_change: trueaccess_control: - path: ^/backoffice roles: - ROLE_EDITOR requires_channel: null host: null port: null ips: { } methods: { } allow_if: nullaccess_decision_manager: strategy: affirmative allow_if_all_abstain: false allow_if_equal_granted_denied: trueaccess_denied_url: nullsession_fixation_strategy: migratehide_user_not_found: truealways_authenticate_before_granting: falseerase_credentials: truerole_hierarchy: { }
EDIT 2
AS @Arno commented, I edited framework.yaml
to save sessions in var/
directory and I can check that this step works without permissions issues, each time I hit the login form, a sess_ file is written.
Worth saying that if I comment :
access_control: - { path: ^/odelices_admin, roles: ROLE_USER}
I can access backoffice.
EDIT 3 : session behavior
So now sessions are saved into var/sessions/prod.
- I clean the dir :
sudo rm -r var/sessions/prod/sess_*
I open Chrome and the url, it sets a PHPSSID cookie with the same value as a first sess_xyz file :
_sf2_attributes|a:2:{s:19:"_csrf/https-contact";s:43:"Oq-QpN21bI_BUDcVbv0ocyrYsTzQo3aJr80QAk2AR7w";s:19:"_csrf/https-booking";s:43:"z_L4TG7Wg0jydwl5VabfJMx0NBhQgeasuAiqxksLvD8";}_sf2_meta|a:3:{s:1:"u";i:1562668584;s:1:"c";i:1562668584;s:1:"l";s:1:"0";}
I go to login page. New PHPSSID value associated with a new sess_xyz file :
_sf2_attributes|a:1:{s:24:"_csrf/https-authenticate";s:43:"erWMU-irtptcZodr8UOjFtxiuyE23LbAeFHRnXgcNdc";}_sf2_meta|a:3:{s:1:"u";i:1562668662;s:1:"c";i:1562668662;s:1:"l";s:1:"0";}
I log in with correct values. This creates 3 new ssid_xyz files.
# 1st one shows user logged in with correct roles and so on_sf2_attributes|a:3:{s:24:"_csrf/https-authenticate";s:43:"erWMU-irtptcZodr8UOjFtxiuyE23LbAeFHRnXgcNdc";s:23:"_security.last_username";s:21:"user_email@gmail.com";s:14:"_security_main";s:799:"C:67:"Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken":718:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"App\Entity\User":6:{s:19:"^@App\Entity\User^@id";i:1;s:22:"^@App\Entity\User^@email";s:21:"user_email@gmail.com";s:22:"^@App\Entity\User^@roles";a:1:{i:0;s:11:"ROLE_EDITOR";}s:25:"^@App\Entity\User^@password";s:60:"$2y$13$cXaR7Ss.kTH1U.T/Rzi6m.ALsKwWCLDcO5/OIeRDAq02iylmf4us6";s:21:"^@App\Entity\User^@name";s:7:"Thierry";s:13:"^@*^@resetToken";N;}i:1;b:1;i:2;a:2:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"^@Symfony\Component\Security\Core\Role\Role^@role";s:11:"ROLE_EDITOR";}i:1;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"^@Symfony\Component\Security\Core\Role\Role^@role";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:2:{i:0;s:11:"ROLE_EDITOR";i:1;s:9:"ROLE_USER";}}}}";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";}# 2nd one ...is empty# 3rd one refers to backoffice url_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:42:"https://mywebsite.com/backoffice";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";}# last one is similar to point 3, before logging, only ssid value differs, and a corresponding cookie is set on Chrome_sf2_attributes|a:1:{s:24:"_csrf/https-authenticate";s:43:"3UC5dCRrahc2qhdZ167Jg4HKTJCexf8PFlefibTVpYk";}_sf2_meta|a:3:{s:1:"u";i:1562668713;s:1:"c";i:1562668713;s:1:"l";s:1:"0";}
EDIT 4 : User Entity
namespace App\Entity;use Doctrine\ORM\Mapping as ORM;// use Symfony\Component\Security\Core\User\EquatableInterface;use Symfony\Component\Security\Core\User\UserInterface;/** * @ORM\Entity(repositoryClass="App\Repository\UserRepository") */class User implements UserInterface # , EquatableInterface{ /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=180, unique=true) */ private $email; /** * @ORM\Column(type="json") */ private $roles = []; /** * @var string The hashed password * @ORM\Column(type="string") */ private $password; /** * @ORM\Column(type="string", length=255) */ private $name; /** * @var string le token qui servira lors de l'oubli de mot de passe * @ORM\Column(type="string", length=255, nullable=true) */ protected $resetToken; /*public function __construct($username, $password, array $roles) { $this->username = $username; $this->password = $password; $this->roles = $roles; }*/ public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): self { $this->email = $email; return $this; } /** * A visual identifier that represents this user. * * @see UserInterface */ public function getUsername(): string { return (string) $this->email; } /** * @see UserInterface */ public function getRoles(): array { $roles = $this->roles; // guarantee every user at least has ROLE_USER $roles[] = 'ROLE_USER'; return array_unique($roles); } public function setRoles(array $roles): self { $this->roles = $roles; return $this; } /** * @see UserInterface */ public function getPassword(): string { return (string) $this->password; } public function setPassword(string $password): self { $this->password = $password; return $this; } /** * @see UserInterface */ public function getSalt() { // not needed when using the "bcrypt" algorithm in security.yaml } /** * @see UserInterface */ public function eraseCredentials() { // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; } public function getName(): ?string { return $this->name; } public function setName(string $name): self { $this->name = $name; return $this; } /** * @return string */ public function getResetToken(): string { return $this->resetToken; } /** * @param string $resetToken */ public function setResetToken(?string $resetToken): void { $this->resetToken = $resetToken; } public function __toString() { return $this->getName() ; }/* public function isEqualTo(UserInterface $user) { if ($this->password !== $user->getPassword()) { return false; } if ($this->email !== $user->getUsername()) { return false; } return true; }*/}
Stack
Debian Stretch, Nginx + Varnish : Nginx handles 443 requests, pass them to Varnish as a cache proxy, which delivers cached objects or pass requests to nginx backend on 8083 port. This is working like a charm for another app with similar login logic (the lone difference is the buggy one redirects to easyadmin instead of a custom admin), so I don't think it is related to the stack.
vhost
server { # this block only redirects www to non www listen aaa.bbb.ccc.ddd:443 ssl; server_name www.somewebsite.com; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_certificate /var/www/clients/client0/web4/ssl/somewebsite.com-le.crt; ssl_certificate_key /var/www/clients/client0/web4/ssl/somewebsite.com-le.key; return 301 https://somewebsite.com$request_uri;}server { # this block redirects ssl requests to Varnish listen aaa.bbb.ccc.ddd:443 ssl; server_name somewebsite.com; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_certificate /var/www/clients/client0/web4/ssl/somewebsite.com-le.crt; ssl_certificate_key /var/www/clients/client0/web4/ssl/somewebsite.com-le.key; location / { # Pass the request on to Varnish. proxy_pass http://127.0.0.1; # Pass some headers to the downstream server, so it can identify the host. proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Tell any web apps that the session is HTTPS. proxy_set_header X-Forwarded-Proto https; proxy_redirect off; }}server { # now sent to backend listen aaa.bbb.ccc.ddd:8083; server_name somewebsite.com; root /var/www/somewebsite.com/web/public; location / { try_files $uri /index.php$is_args$args; } location ~ ^/index\.php(/|$) { fastcgi_pass 127.0.0.1:8998; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; internal; } location ~ \.php$ { return 404; } error_log /var/log/ispconfig/httpd/somewebsite.com/error.log; access_log /var/log/ispconfig/httpd/somewebsite.com/access.log combined; location ~ /\. { deny all; } location ^~ /.well-known/acme-challenge/ { access_log off; log_not_found off; root /usr/local/ispconfig/interface/acme/; autoindex off; try_files $uri $uri/ =404; } location = /favicon.ico { log_not_found off; access_log off; expires max; } location = /robots.txt { allow all; log_not_found off; access_log off; }}
Could this be related to permissions on some dir ? HTTPS ? EasyAdmin ? How can I make sure the security token was stored in the session, even it is logged as stored ? I also tried to change access_control to role ROLE_USER so that any authenticated user should access. No way.
Any help is really appreciated.