1

I have a Guard Authentication in my Symfony Web Application. I would like to perform some unit tests. I'm unable to simulate an authentification in my tests. The token stays null when calling $tokenStorage->getToken().

Note:

  • The login authentification is working under dev and prod environnement.
  • I saw quite a lot of related topics without success and the doc.
  • Symfony version: 3.4.

Reproduce: you can reproduce the error from this repo (symfony project). This repo defined one entity User with a custom constraint validator ExampleValidator. In this constraint, I need to have the current logged user.

Code sample:

After manually creating an User, the login function used in tests:

private function logIn($firewallName = 'main'){
   // dummy call to bypass the hasPreviousSession check
   $crawler = $this->client->request('GET', '/');
   $session = $this->client->getContainer()->get('session');

   /** @var User $user */
   $user = $this->entityManager->getRepository(User::class)
       ->findOneBy(['email' => '[email protected]']);

   // you may need to use a different token class depending on your application.
   // for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
   $token = new PostAuthenticationGuardToken($user, $firewallName, [new Role('ROLE_CLIENT')]);
        self::$kernel->getContainer()->get('security.token_storage')->setToken($token);

   $session->set('_security_'.$firewallName, serialize($token));
   $session->save();

   $cookie = new Cookie($session->getName(), $session->getId());
   $this->client->getCookieJar()->set($cookie);
}

The User call from tokenStorage (from service function):

class ExampleValidator extends ConstraintValidator{
    protected $requestStack;
    protected $em;
    protected $user_id;

    public function __construct(RequestStack $request,
                                EntityManager $em,
                                TokenStorage $tokenStorage){
        $this->requestStack = $request;
        $this->em = $em;

        /** @var User $user */
        // Token is always null
        $user = $tokenStorage->getToken()->getUser();
        $this->user_id = $user != "anon." ? $user->getId() : null;
    }

    /**
     * @param $value
     * @param Constraint $constraint
     */
    public function validate($value, Constraint $constraint)
    {
        // validation rules ...
    }
}

LoginFormAuthenticator.php

<?php

namespace AppBundle\Security;


use AppBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator{
    use TargetPathTrait;

    private $entityManager;
    private $urlGenerator;
    private $csrfTokenManager;
    private $passwordEncoder;
    private $loginAttemptRepository;


    public function __construct(EntityManagerInterface $entityManager,
                                UrlGeneratorInterface $urlGenerator,
                                CsrfTokenManagerInterface $csrfTokenManager,
                                UserPasswordEncoderInterface $passwordEncoder){
        $this->entityManager = $entityManager;
        $this->urlGenerator = $urlGenerator;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
    }

    /**
     * @param Request $request
     * @return bool
     */
    public function supports(Request $request){
        return $request->getPathInfo() == '/login_check' &&
            $request->isMethod('POST') &&
            $request->request->get('_password') !== null;
    }


    /**
     * @param Request $request
     * @return array|mixed|void|null
     */
    public function getCredentials(Request $request){
        $isLoginSubmit = $request->getPathInfo() == '/login_check' &&
            $request->isMethod('POST') &&
            $request->request->get('_password') !== null;
        $isCaptcha = $request->request->get('captcha_set');

        if ($isCaptcha == 1 && $request->request->get('_password') !== null) {
            $secret = ...;
            if($_POST['g-recaptcha-response'] !== null){
                // Paramètre renvoyé par le recaptcha
                $response = $_POST['g-recaptcha-response'];
                $remoteip = $_SERVER['REMOTE_ADDR'];

                $api_url = "https://www.google.com/recaptcha/api/siteverify?secret="
                    . $secret
                    . "&response=" . $response
                    . "&remoteip=" . $remoteip ;

                $decode = json_decode(file_get_contents($api_url), true);

                if ($decode['success'] == true) {
                    $username = $request->request->get('_username');
                    $password = $request->request->get('_password');
                    $csrfToken = $request->request->get('_csrf_token');

                    if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) {
                        throw new InvalidCsrfTokenException('Invalid CSRF token.');
                    }

                    $request->getSession()->set(
                        Security::LAST_USERNAME,
                        $username
                    );

                    return [
                        'username' => $username,
                        'password' => $password,
                    ];
                }
                else{
                    throw new CustomUserMessageAuthenticationException('Captcha invalids.');
                }
            }
            else{
                throw new CustomUserMessageAuthenticationException('Captcha invalids.');
            }
        }
        else {
            if (!$isLoginSubmit) {
                // skip authentication
                return;
            }

            $username = $request->request->get('_username');
            $password = $request->request->get('_password');
            $csrfToken = $request->request->get('_csrf_token');

            if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) {
                throw new InvalidCsrfTokenException('Invalid CSRF token.');
            }

            $request->getSession()->set(
                Security::LAST_USERNAME,
                $username
            );

            return [
                'username' => $username,
                'password' => $password,
            ];
        }
    }

    /**
     * @param mixed $credentials
     * @param UserProviderInterface $userProvider
     * @return User|object|UserInterface|null
     */
    public function getUser($credentials, UserProviderInterface $userProvider){
        $username = $credentials["username"];
        $user = $this->entityManager->getRepository(User::class)
            ->findOneBy(['username' => $username]);
        return $user;
    }


    /**
     * @param mixed $credentials
     * @param UserInterface $user
     * @return bool
     */
    public function checkCredentials($credentials, UserInterface $user){
        $password = $credentials["password"];
        $rep = false;
        if ($this->passwordEncoder->isPasswordValid($user, $password)){
            $rep = true;
        }
        return $rep;
    }

    /**
     * @param Request $request
     * @param TokenInterface $token
     * @param string $providerKey
     * @return RedirectResponse
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey){
        $targetPath = null;
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }
        return new RedirectResponse($this->urlGenerator->generate('map'));
    }

    /**
     * @return string
     */
    protected function getLoginUrl(){
        return $this->urlGenerator->generate('fos_user_security_login');
    }
}
4
  • What's ExampleValidator doing in there? How is it relevant? Commented Jan 22, 2020 at 15:12
  • @yivi This isn't relevant here. I defined the validator ExampleValidator as an example constraint for the User class. I have an entity with custom constraints I want to test. On some of these constraints, I need to have the current logged user. Commented Jan 22, 2020 at 17:18
  • If it's not relevant here, remove it. It induces to confusion. Leave only what's relevant to the problem at hand. Commented Jan 22, 2020 at 17:23
  • Hum, I'm sorry not to be clear. The ExampleValidator is actually the problem, I mean the content of the above "reproductible" example isn't relevant. It's only a dummy example. Commented Jan 22, 2020 at 17:25

1 Answer 1

4
+50

I believe the root of your problem is that you are using multiple container instances. In particular, your logIn() function works on the container of the client, but the validator is from a different container that you boot up during setUp(). Thus, the changes you make in logIn() to the client container do not affect the validator you are actually testing.

Using the same container everywhere, e.g. the one from the client, should solve this. The following changes to your repository make the test pass:

diff --git a/tests/AppBundle/Validator/UserTest.php b/tests/AppBundle/Validator/UserTest.php
index f15c854..603e566 100644
--- a/tests/AppBundle/Validator/UserTest.php
+++ b/tests/AppBundle/Validator/UserTest.php
@@ -44,10 +44,7 @@ class UserTest extends WebTestCase{
         $this->container = $this->client->getContainer();
         $this->entityManager = $this->container->get('doctrine.orm.entity_manager');

-        // Set validator
-        $kernel = $this->createKernel();
-        $kernel->boot();
-        $this->validator = $kernel->getContainer()->get('validator');
+        $this->validator = $this->client->getContainer()->get('validator');

         // Create one user
         $this->createOneUser();
@@ -100,7 +97,7 @@ class UserTest extends WebTestCase{
         // you may need to use a different token class depending on your application.
         // for example, when using Guard authentication you must instantiate PostAuthenticationGuardToken
         $token = new PostAuthenticationGuardToken($user, $firewallName, [new Role('ROLE_CLIENT')]);
-        self::$kernel->getContainer()->get('security.token_storage')->setToken($token);
+        $this->client->getContainer()->get('security.token_storage')->setToken($token);

         $session->set('_security_'.$firewallName, serialize($token));
         $session->save();
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.