#StackBounty: #symfony #authentication #symfony4 Symfony 4 login form : authenticating successfully, but authentication immediately los…

Bounty: 100

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 "SymfonyComponentSecurityCoreEncoderBCryptPasswordEncoder" class is deprecated since Symfony 4.3, use "SymfonyComponentSecurityCoreEncoderNativePasswordEncoder" 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 FoundrnCache-Control: no-cache, privaternDate:          Fri, 05 Jul 2019 10:28:46 GMTrnLocation:      /backofficernrn<!DOCTYPE html>n<html>n    <head>n        <meta charset="UTF-8" />n        <meta http-equiv="refresh" content="0;url=/backoffice" />nn        <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 "AppSecurityLoginFormAuthenticator" 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:
        AppEntityUser:
            algorithm: bcrypt
    providers:
        app_user_provider:
            entity:
                class: AppEntityUser
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            guard:
                authenticators:
                    - AppSecurityLoginFormAuthenticator
            logout:
                path: app_logout
    access_control:
        - { path: ^/backoffice, roles: ROLE_EDITOR} # requires_channel: https

routes.yaml

admin:
  path: /backoffice
  controller: EasyCorpBundleEasyAdminBundleControllerEasyAdminController

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 SymfonyComponentHttpFoundationRedirectResponse
     */
    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:
    AppEntityUser:
        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: AppEntityUser
            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:
                - AppSecurityLoginFormAuthenticator
            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: {  }

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:"SymfonyComponentSecurityGuardTokenPostAuthenticationGuardToken":718:{a:2:{i:0;s:4:"main";i:1;a:5:{i:0;O:15:"AppEntityUser":6:{s:19:"^@AppEntityUser^@id";i:1;s:22:"^@AppEntityUser^@email";s:21:"user_email@gmail.com";s:22:"^@AppEntityUser^@roles";a:1:{i:0;s:11:"ROLE_EDITOR";}s:25:"^@AppEntityUser^@password";s:60:"$2y$13$cXaR7Ss.kTH1U.T/Rzi6m.ALsKwWCLDcO5/OIeRDAq02iylmf4us6";s:21:"^@AppEntityUser^@name";s:7:"Thierry";s:13:"^@*^@resetToken";N;}i:1;b:1;i:2;a:2:{i:0;O:41:"SymfonyComponentSecurityCoreRoleRole":1:{s:47:"^@SymfonyComponentSecurityCoreRoleRole^@role";s:11:"ROLE_EDITOR";}i:1;O:41:"SymfonyComponentSecurityCoreRoleRole":1:{s:47:"^@SymfonyComponentSecurityCoreRoleRole^@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";}
    

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.


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.