Quantcast
Channel: Active questions tagged symfony4 - Stack Overflow
Viewing all articles
Browse latest Browse all 3924

Symfony 4 login form : authenticating successfully, but authentication immediately lost after redirect

$
0
0

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

  1. Guard authentication successful
  2. Guard authenticator set success response
  3. Stored the security token in the session
  4. 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.

  1. I clean the dir : sudo rm -r var/sessions/prod/sess_*
  2. 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";}
  3. 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";}
  4. 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.


Viewing all articles
Browse latest Browse all 3924

Latest Images

Trending Articles



Latest Images

<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>