I built a login form following this form login setup doc.
This is working fine on localhost but not on the production server.
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"} []
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"} []
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:
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
admin:
path: /backoffice
controller: EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController
// 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');
}
}
// 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
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: null
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
manager_name: null
firewalls:
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: true
access_control:
-
path: ^/backoffice
roles:
- ROLE_EDITOR
requires_channel: null
host: null
port: null
ips: { }
methods: { }
allow_if: null
access_decision_manager:
strategy: affirmative
allow_if_all_abstain: false
allow_if_equal_granted_denied: true
access_denied_url: null
session_fixation_strategy: migrate
hide_user_not_found: true
always_authenticate_before_granting: false
erase_credentials: true
role_hierarchy: { }
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.
So now sessions are saved into var/sessions/prod.
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";}
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;
}*/
}
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.
So here are my comments in a more structured way, so that it might help someone else having problems with authentication in Symfony.
By default, each session is saved as a file with the name sess_<id>
in <project_dir>/var/cache/<env>/sessions
or as defined by save_path
in your php.ini
if framework.session.handler
is set to null
. Configure your session directory explicitly and make sure a session file is created when you log in. If not, check the permissions for that folder.
# app/config/config.yml (Symfony 3)
# config/packages/framework.yaml (Symfony 4)
framework:
session:
handler_id: 'session.handler.native_file'
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
Cf. https://symfony.com/doc/current/session.html#configuration
When you login, a session with a new ID should be created. Check the content of the file. It should contain your serialized user under the firewall name (e.g. main), including your identifier (e.g. email) and your user role(s) (e.g. ROLE_USER). A problem here could be caused by faulty authentication, security config, or serialization.
_sf2_attributes|a:3:{s:18:"_csrf/authenticate";s:43:"n2oap401u4P4O7m_IhPODZ6Bz7EHl-DDsHxBEl-fhxc";s:23:"_security.last_username";s:10:"foo@bar.de";s:14:"_security_main";s:545:"C:67:"Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken":464:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"App\Entity\User":4:{s:19:"App\Entity\Userid";i:1;s:22:"App\Entity\Useremail";s:10:"foo@bar.de";s:22:"App\Entity\Userroles";a:0:{}s:25:"App\Entity\Userpassword";s:60:"$2y$13$qwbtasafa58lPonX6B5a9eV4lziF7EZWP8NFLAe3blpCJVhQgPVOS";}i:1;b:1;i:2;a:1:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:1:{i:0;s:9:"ROLE_USER";}}}}";}_sf2_meta|a:3:{s:1:"u";i:1563015142;s:1:"c";i:1563015142;s:1:"l";s:1:"0";}
Check that a cookie with the same ID is set in your browser on login. The name of the cookie is defined by session.name
in your php.ini
, by default it is PHPSESSID
. It should be sent with every request you make (e.g. Cookie: PHPSESSID=lpcf79ff8jdv2iigsgvepnr9bb
). If the correct session exists, but you have a different cookie in your browser, you might have been immediately logged out after a success redirect.
The session ID should only change when your user changes (e.g. on login and logout). If it changes after normal requests (e.g. you are immediately logged out) or your session seems to be ignored, the problem might be that Symfony considers your user changed. This can be caused by faulty (de)serialization or comparison.
By default, Symfony uses the serialized results of getPassword()
, getUsername()
, and getSalt()
from the session to compare against the user provided by the user provider (e.g. the database). If any of those values changes, you are logged out (cf. https://symfony.com/doc/current/security/user_provider.html#understanding-how-users-are-refreshed-from-the-session).
Thus, you should make sure that the user provided by e.g. your database is correct and matches the deserialized user from the session. Make sure the relevant fields are properly serialized. If you implement the Serializable
interface, make sure your serialize()
method matches your unserialize()
. If you implement EquatableInterface
, make sure your isEqualTo()
method works correctly. Both of those interfaces are optional though, so you might consider to remove them for debugging purposes.