Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Panther with Authentication #283

Open
Sethbass opened this issue Jan 16, 2020 · 42 comments
Open

Panther with Authentication #283

Sethbass opened this issue Jan 16, 2020 · 42 comments

Comments

@Sethbass
Copy link

Sethbass commented Jan 16, 2020

Hello,

First I would like to thank you to improve this part of symfony and helping us to get rid of Behat.

I have created a symfony app which needs authentication. This means that / is protected by the firewall main and every URL needs an authenticated user.
On top of that I have custom authenticator which extends AbstractGuardAuthenticator. The authentication process is based on CAS (not my choice I have to cope with it).

I have tried to start my functional tests using Panther and followed the documentation here :
https://github.com/symfony/panther

So far so good :) Then I have to deal immediately with the authentication, so I found this tutorial :
https://symfony.com/doc/4.3/testing/http_authentication.html

First thing I have discovered is that I need to visit a page on my website otherwise I get this error :

Facebook\WebDriver\Exception\InvalidCookieDomainException: invalid cookie domain
  (Session info: chrome=79.0.3945.117)
  (Driver info: chromedriver=78.0.3904.70 (edb9c9f3de0247fd912a77b7f6cae7447f6d3ad5-refs/branch-heads/3904@{#800}),platform=Mac OS X 10.15.2 x86_64)

Ok then I have created a test page accessible anonymously and got rid of the error. Not sure this is a best practice.

Then I was able to run the following code :

<?php

namespace App\Tests;

use App\Entity\User;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;

class HomeControllerTest extends PantherTestCase
{
    private $client = null;

    public function setUp()
    {
        $this->client = static::createPantherClient();
    }

    public function testHome()
    {

        $this->logIn();
        $crawler = $this->client->request('GET', '/test');

        sleep(5);
        $user = self::$container->get('security.helper')->getUser();
        echo ('The user is :'.$user);
        $crawler = $this->client->request('GET', '/');

    }

    private function logIn()
    {

        $doctrine = self::$container->get('doctrine.orm.default_entity_manager');

        /** @var User $user */
        $user = $doctrine->getRepository(User::class)->findOneBy(['email' => 'sebastien@code-it.fr']);

        $session = self::$container->get('session');


        // 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, 'main', $user->getRoles());
        $session->set('_security_main', serialize($token));
        $session->save();
        $cookie = new Cookie($session->getName(), $session->getId());
        $this->client->getCookieJar()->set($cookie);
    }

}

The problem is that I am still redirected to the CAS authentication page calling $crawler = $this->client->request('GET', '/');
My user is always NULL which means I am not authenticated.

I thought Session Storage Mock file could be a suspect but actually all my tests were unsuccessful.

Help :)

Thanks a lot for reading

@ioniks
Copy link

ioniks commented Jan 23, 2020

I have the same issue.

@Sethbass
Copy link
Author

Sethbass commented Jan 31, 2020

Hello,

If anyone is getting the same error about user authentication in functional tests implementing the token, I found the issue.

This was not related to Panther but to the security component, indeed I did not notice the logs telling me that the token needed to be refresh and then the user was automatically deauthenticated => cannot refresh the token because the user has changed. This means that symfony was not able to compare properly the user with the authentication system which I guess is by default comparing username password and salt. But in the case of CAS authentication you do not have password or salt but only the username.

In order to solve this, you need to implement Symfony\Component\Security\Core\User\EquatableInterface and the related method isEqualTo.

public function isEqualTo(UserInterface $user)
{
        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
}

So when the security component will check if it needs a refresh, it will work properly.

I am now able to run a classic WebTestCase and to access my home page.

The only issue remaining is the invalid cookie domain which is preventing me to use Panther for now. I will investigate on this side and let you know in case I fund something.

Cheers

@Sethbass
Copy link
Author

Sethbass commented Feb 3, 2020

Hello,

I have tried many things on my side, and this is not possible to get rid of the "invalid cookie domain" error, unless you create a new page (simple page with nothing or an h1 is enough) to initiate the process. This is not related to Panther but to php webdriver. I found many entries about it on stackoverflow.

The real issue is that my code to log in is working with absolutely no issue doing a WebTestCase but is not working with Panther.

    private function logIn($email)
    {
        /** @var User $user */
        $user = self::$container->get('doctrine')->getRepository(User::class)->findOneBy(['email' => $email]);
        $session = self::$container->get('session');

        $firewallName = 'main';
        // if you don't define multiple connected firewalls, the context defaults to the firewall name
        // See https://symfony.com/doc/current/reference/configuration/security.html#firewall-context
        $firewallContext = 'main';

        // 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, ['ROLE_ADMIN']);
        $session->set('_security_'.$firewallContext, serialize($token));
        $session->save();


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

As far as I understand, despite I had the cookie to Panther Cookie Jar it is not used to authenticate and it keeps sending me to the CAS authentication page.

Here is the content of the cookie Jar of my Panther client:

array(1) {
  [0]=>
  object(Symfony\Component\BrowserKit\Cookie)#602 (9) {
    ["name":protected]=>
    string(10) "MOCKSESSID"
    ["value":protected]=>
    string(64) "a1c18866bc13ca0b2233feb351b02788a93961672793420bfa24dcfc25a93923"
    ["expires":protected]=>
    NULL
    ["path":protected]=>
    string(1) "/"
    ["domain":protected]=>
    string(9) "127.0.0.1"
    ["secure":protected]=>
    bool(false)
    ["httponly":protected]=>
    bool(true)
    ["rawValue":protected]=>
    string(64) "a1c18866bc13ca0b2233feb351b02788a93961672793420bfa24dcfc25a93923"
    ["samesite":"Symfony\Component\BrowserKit\Cookie":private]=>
    NULL
  }
}

Based on the log in my test environment I can see no entry from the Security component like using the WebTestCase. So my conclusion is that the cookie is not even considered and used for authentication in Symfony. PantherClient is just redirecting me to the authentication page.

Does anyone have a clue ?

Thanks a lot,

@andrescevp
Copy link

here same problem... some glue? thanks!

@gponty
Copy link
Contributor

gponty commented Mar 17, 2020

Same problem here... any idea ?

@Sethbass
Copy link
Author

@andrescevp , @gponty , I ended up using classic classic Web test case and the Crawler which are working perfectly with the code above.
I even found a way to add items to the collection without using JS. Maybe I will look into Panther once we have a solution...

@JohnstonCode
Copy link
Contributor

JohnstonCode commented May 13, 2020

This it what i had to throw together to get it working. I had to set readinessPath in the option to make sure the client wasn't redirected to the login page as I use SAML SSO. Then you need to make another request to your site before it will allow you to set a cookie. It doesn't matter if the page returns a 404 response.

class HomeControllerTest extends PantherTestCase
{
    private $client;

    protected function setUp()
    {
        $this->client = static::createPantherClient(['readinessPath' => '/error']);
    }

    public function testHome()
    {
		$this->logIn();

        $this->client->request('GET', '/');

        $this->assertPageTitleContains('Home Page');
    }

    private function logIn()
    {
        $this->client->request('GET', '/error');

        $doctrine = self::$container->get('doctrine.orm.default_entity_manager');

        /** @var User $user */
        $user = $doctrine->getRepository(User::class)->findOneBy(['email' => 'test@example.com']);

        $session = self::$container->get('session');

        // 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, 'main',$user->getRoles());
        $session->set('_security_main', serialize($token));
        $session->save();
        $cookie = new Cookie($session->getName(), $session->getId());
        $this->client->getCookieJar()->set($cookie);
    }
}

@Sethbass
Copy link
Author

Hi @JohnstonCode, you should check the upcoming changes on SF 5.1 :) There will be a new auth system for the tests. As far as I understood there will be no need for us anymore to generate this code.
https://symfony.com/blog/new-in-symfony-5-1-simpler-login-in-tests

Cheers :)

@JohnstonCode
Copy link
Contributor

Thanks, looks good hopefully this resolves our issue.

@antoine1003
Copy link

Same issue here :/

@bastien70
Copy link
Contributor

bastien70 commented Jul 27, 2020

Same here with :

        $client = static::createPantherClient();


        /** @var User $user */
        $user = $this->getSuperAdminAccount();

        $session = self::$container->get('session');

        $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
        $session->set('_security_main', serialize($token));
        $session->save();

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

Any solution ?

@nizarRebhi
Copy link

nizarRebhi commented Aug 16, 2020

For setting-up cookie's domain, you have to request firstly on the domain, then set-up the cookie...

$client->request('GET', '/');

@kolvin
Copy link

kolvin commented Aug 24, 2020

@nizarRebhi thanks for the comment, working a treat for me now

@shadowc
Copy link
Contributor

shadowc commented Feb 14, 2021

So I've been investigating the login process as well and it seems $client->loginUser is not present in the PantherClient. I'm not sure if this should be implemented or not, but if not implemented, I suppose the project should provide an equally simple and efficient method to authenticate.

Us cypress users are used to just send a POST request to the login Url that gets us logged in for the e2e tests without going through the UI login process. May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported) or provide a shortcut function like loginUser

@njsa04
Copy link

njsa04 commented Feb 15, 2021

@shadowc Have you been able to login yourself? I have tried methods outlined earlier - but continue to get CSRF errors.
The createClient and then ->loginUser() method works fine. But using the pantherClient and setting the cookies with a generated token value doesn't seem to work. I suspect it is because the test is running in test env but the URL it's hitting is in the dev environment, but I haven't figured out where to go from here.

@Sethbass have you found a solution to getting PantherClient to work with authentication?

@JohnstonCode did you say you got it working with PantherClient and authentication?

Would love to get this working.

So I've been investigating the login process as well and it seems $client->loginUser is not present in the PantherClient. I'm not sure if this should be implemented or not, but if not implemented, I suppose the project should provide an equally simple and efficient method to authenticate.

Us cypress users are used to just send a POST request to the login Url that gets us logged in for the e2e tests without going through the UI login process. May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported) or provide a shortcut function like loginUser

@drupol
Copy link

drupol commented Mar 8, 2021

Dear SethBass,

I'm trying to do functional tests on an app protected by CAS and I'm not able to pass the authentication yet.

I have a few questions, could you please help me?

  1. Could you show me your security.yaml ?
  2. Is there something else, besides implements EquatableInterface to do to get this working?
  3. Could you please tell me which CAS bundle you're using?

Thanks!

@madxmind
Copy link

Hi,

Has anyone found a solution to use :
static::createClient()->loginUser($myUser)
in a PanterClient :
static::createPantherClient()->loginUser($myUser)

PS : it doesn't work so (Error: Call to undefined method Symfony\Component\Panther\Client::loginUser())

Symfony 5.3
PphUnit 9.5

Thanks!

@hutnikau
Copy link

For me it was the issue with empty cookies when env switched to "test"

Try to comment out mock session storage factory:

#framework.yaml
when@test:
    framework:
        test: true
#        session:
#            storage_factory_id: session.storage.factory.mock_file

@holema
Copy link

holema commented Dec 19, 2021

I want to use panther to test an application which uses a SSO to authenticate. Unfortunantaly up to now it is not possible to test this application with panther because of the missing login fuction wich is implemented in the WebTestCase of the kernel application.
When I want to login into the application I get sveral redirects ant he redirect url has to be correct as well. so the effort is so huge to simulate it with a SSO provider.

I would be so great if here is an update planned.

@chadcasey
Copy link

I'm also experiencing this same issue.

// Assuming an existing user:
$user = iterator_to_array($userRepo->findByRole(User::ROLE_SUPER_ADMIN, 1))[0];

// Get the Panther client:
$client = static::createPantherClient();

// Get a new token:
$token = new OAuthToken('test', $user->getRoles());
$token->setUser($user);

// Assign it to the session:
$session = static::getContainer()->get('session');
$session->set('_security_main', serialize($token));
$session->save();

// Add cookies to the client:
$client->request(Request::METHOD_GET, '/'); // Need to hit the domain first before we can set cookies
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
$client->request(Request::METHOD_GET, '/'); // <-- Still redirected back to the login page

Previously, I tried creating the panther client with the hostname and port configured so that I can use PhpStorm breakpoints with traffic coming from the Panther browser, and I could see that the cookies are being set as expected. But the $_SESSION global doesn't have any of the security token information that I'm setting as shown above. Is there a better way to set session properties that will be used by the Panther browser? Could that be the issue preventing SSO authentication?

@alexandre-mace
Copy link
Contributor

Hi,

Has anyone found a solution to use : static::createClient()->loginUser($myUser) in a PanterClient : static::createPantherClient()->loginUser($myUser)

PS : it doesn't work so (Error: Call to undefined method Symfony\Component\Panther\Client::loginUser())

Symfony 5.3 PphUnit 9.5

Thanks!

Still facing this problem, any solution ?

@arderyp
Copy link
Contributor

arderyp commented May 17, 2022

I can authenticate in Panther with CAS, but I am not using a bundle but rather my own Guard implementation.

I can navigate to pages that require authentication, and click through things, but I have hit a wall where my Ajax POST requests are not working. They get triggered but return 404 not found. Posting to json endpoints provided 404, but to HTML endpoints gives a 302 redirect to my login page.

More info on my POST issue and my authentication implementation here: #547

While I am not using a CAS bundle, I based by Guard implementation on this (its a few years old so probably a bit out of date, but the same basic idea): https://github.com/PRayno/CasAuthBundle/blob/master/Security/CasAuthenticator.php

P.S. I don't implement EquatableInterface, and I do have to hit / before setting my login token. I've tested implementing EquatableInterface just now and it does not solve my POST issue.

@arderyp
Copy link
Contributor

arderyp commented May 17, 2022

@shadowc, you said

May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported)

Where is this documented?

@drupol
Copy link

drupol commented May 17, 2022

I can authenticate in Panther with CAS, but I am not using a bundle but rather my own Guard implementation.

I can navigate to pages that require authentication, and click through things, but I have hit a wall where my Ajax POST requests are not working. They get triggered but return 404 not found. Posting to json endpoints provided 404, but to HTML endpoints gives a 302 redirect to my login page.

More info on my POST issue and my authentication implementation here: #547

While I am not using a CAS bundle, I based by Guard implementation on this (its a few years old so probably a bit out of date, but the same basic idea): https://github.com/PRayno/CasAuthBundle/blob/master/Security/CasAuthenticator.php

P.S. I don't implement EquatableInterface, and I do have to hit / before setting my login token. I've tested implementing EquatableInterface just now and it does not solve my POST issue.

How about using a bundle for CAS ? Wouldn't it be a bit more practical?

@arderyp
Copy link
Contributor

arderyp commented May 17, 2022

Interestingly, other ajax posts seem to work, so maybe there is something about this specific implementation that's wonky. I will test further and report back.

@arderyp
Copy link
Contributor

arderyp commented May 17, 2022

@drupol no, I've found it is not. The CAS protocol is very simple and easy to implement (at least on the consumer side). My coworkers who are depending on phpCas and various CAS bundles have experienced breakage from updates to our CAS servers, due to their package implementations. I, on the other hand, have not :)

In addition, if something breaks or changes (hasn't happened in 4 years), its much easier/quicker for me to fix it myself rather than wait on third party developers to acknowledge and fix the issue or accept my pull request. In general, programming has become way too reliant on dependencies for simple functionality that can be implemented/controlled in house. This comes with all sorts of problems. I can't say my software if free from this problem altogether, not by a long shot. But, thankfully, it is in the context of CAS.

If CAS were more complex, I'd reach for a library, but its very simple XML parsing.

Out of curiosity, which CAS bundle do you recommend?

P.S. I've resolved my ajax POST mystery, and it turned out authentication was not the problem.

@drupol
Copy link

drupol commented May 17, 2022

@drupol no, I've found it is not. The CAS protocol is very simple and easy to implement (at least on the consumer side). My coworkers who are depending on phpCas and various CAS bundles have experienced breakage from updates to our CAS servers, due to their package implementations. I, on the other hand, have not :)

In addition, if something breaks or changes (hasn't happened in 4 years), its much easier/quicker for me to fix it myself rather than wait on third party developers to acknowledge and fix the issue or accept my pull request. In general, programming has become way too reliant on dependencies for simple functionality that can be implemented/controlled in house. This comes with all sorts of problems. I can't say my software if free from this problem altogether, not by a long shot. But, thankfully, it is in the context of CAS.

If CAS were more complex, I'd reach for a library, but its very simple XML parsing.

Out of curiosity, which CAS bundle do you recommend?

P.S. I've resolved my ajax POST mystery, and it turned out authentication was not the problem.

Try this one: https://github.com/ecphp/cas-bundle

Sorry for the brevity, replying from the smartphone.

@arderyp
Copy link
Contributor

arderyp commented May 17, 2022

all good, thanks for the recommendation

EDIT: I see you're the primary contributor, nice! Thanks for the FOSS offering.

@shadowc
Copy link
Contributor

shadowc commented May 17, 2022

@shadowc, you said

May be panther can allow for POST calls in their client (I've tried this and it just plainly said POST calls are not supported)

Where is this documented?

Not documented. I believe I was referring to an error message that came out in the console. This message is a year old so things could have changed since!

@drupol
Copy link

drupol commented May 17, 2022

all good, thanks for the recommendation

EDIT: I see you're the primary contributor, nice! Thanks for the FOSS offering.

You're welcome! I developed this bundle for the European Commission and we are using everyday in every Symfony app.
Feel free to let me know if anything goes wrong, we're quite responsive!

@bastoune
Copy link

bastoune commented Jul 8, 2022

Hi,
Has anyone found a solution to use : static::createClient()->loginUser($myUser) in a PanterClient : static::createPantherClient()->loginUser($myUser)
PS : it doesn't work so (Error: Call to undefined method Symfony\Component\Panther\Client::loginUser())
Symfony 5.3 PphUnit 9.5
Thanks!

Still facing this problem, any solution ?

Hi, did anybody has update about this ?

Using Symfony 5.4 and still facing the issue.

@mhujer
Copy link

mhujer commented Jul 8, 2022

Hi! I'm using the following base class for our E2E tests (it is based on previous comments in this discussion and code in KernelBrowser::loginUser) and it works fine on SF 6.1.

//...
use Symfony\Component\Panther\Client as PantherClient;
//...

abstract class PantherTestCase extends KernelTestCase
{

    use WebTestAssertionsTrait;

    /**
     * @param string[] $options An array of options to pass to the createKernel class
     * @param string[] $kernelOptions
     * @param string[] $managerOptions
     */
    protected static function createAuthenticatedPantherClient(
        User $user,
        array $options = [],
        array $kernelOptions = [],
        array $managerOptions = []
    ): PantherClient
    {
        $client = self::createPantherClient();

        // without this request, the session cookie could not be set
        $client->request('GET', '/');

        // Inspired by \Symfony\Bundle\FrameworkBundle\KernelBrowser::loginUser()
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $container = self::getContainer();
        $container->get('security.untracked_token_storage')->setToken($token);

        $session = $container->get('session.factory')->createSession();
        $session->set('_security_main', serialize($token));
        $session->save();

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

        return $client;
    }

@tacman
Copy link

tacman commented Jul 14, 2022

Thanks, @mhujer , this looks very promising.

When I make the first call after getting the client, I get


Testing /home/tac/survos/demos/symfony-demo/tests

Facebook\WebDriver\Exception\InvalidCookieDomainException : invalid cookie domain
  (Session info: headless chrome=103.0.5060.53)

With the $client->request('GET', '/'); line, I made some progress, but the authenticated route returns the login page.

With the
I tried @hutnikau idea and got rid of the session in framework when test:

when@test:
    framework:
        test: true
#        session:
#            storage_factory_id: session.storage.factory.mock_file

And got the curious error:


Testing /home/tac/survos/demos/symfony-demo/tests

RuntimeException : Failed to start the session because headers have already been sent by "/home/tac/survos/demos/symfony-demo/vendor/bin/.phpunit/phpunit-9.5-0/src/Util/Printer.php" at line 104.
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:135
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorage.php:296
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Session.php:258
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Session.php:278
 /home/tac/survos/demos/symfony-demo/vendor/symfony/http-foundation/Session/Session.php:86
 /home/tac/survos/demos/symfony-demo/tests/E2eTest.php:41
 /home/tac/survos/demos/symfony-demo/tests/E2eTest.php:58

The test itself is using the latest (6.1) symfony demo.

use App\Entity\User;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Panther\Client;
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class E2eTest extends PantherTestCase
{
    /**
     * @param string[] $options An array of options to pass to the createKernel class
     * @param string[] $kernelOptions
     * @param string[] $managerOptions
     */
    protected static function createAuthenticatedPantherClient(
//        User $user,
        ?string $username=null,
        array $options = [],
        array $kernelOptions = [],
        array $managerOptions = []
    ): Client
    {
        $client = self::createPantherClient();
        if ($username) {

          $client->request('GET', '/'); // removing this line shows a different error

            $container = self::getContainer();
            $user = $container->get('doctrine')->getRepository(User::class)->findOneBy(['username' => $username]);
            assert($user, "Invalid user $username, not in user database");

            // Inspired by \Symfony\Bundle\FrameworkBundle\KernelBrowser::loginUser()
            $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

            $container = self::getContainer();
            $container->get('security.untracked_token_storage')->setToken($token);

            $session = $container->get('session.factory')->createSession();
            $session->set('_security_main', serialize($token));
            $session->save();

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

        }


        return $client;
    }
    public function testMyApp(): void
    {
//        static $client = static::createPantherClient(); // Your app is automatically started using the built-in web server
        $client = $this->createAuthenticatedPantherClient('jane_admin');
        $client->request('GET', '/');

You mentioned it was working for you -- can you post the entire test in a gist or repo somewhere? Or here? Thanks.

@shadydealer
Copy link

@tacman This no longer works since symfony/symfony#45662 . Unfortunately I cannot find ANY workaround, since that change. I can't even login a user using the login form. For some reason it submits it but nothing happens... I literally cannot get Authentication to work with panther

@arderyp
Copy link
Contributor

arderyp commented Sep 24, 2022

@shadydealer I've worked around this in 5.4. Here are my related links.

#547

symfony/symfony#46961 (comment)

I made a PR to allow sessions in tests for persistent interactions, but no one at Symfony has chimed in:

symfony/symfony#47001

My implementation may need work, but if people want this feature, they should comment on and upvote the PR.

@shadydealer
Copy link

shadydealer commented Sep 27, 2022

@arderyp thanks for the reply.

I managed to get my login form to work. What I had missed and am now realizing is not really part of the documentation and probably should be is that I hadn't set up my panther environment correctly. I had to:
add PANTHER_APP_ENV=panther to my .env.test environment. What this does is start the panther server using a .env.panther environment that had to be set up, but I had missed for some reason. Otherwise it will use your .env.local which, in my case, caused my panther environment using my dev database and my test environment using my test database, so when I tried logging in a user using the login form it always returned incorrect credentials. Furthermore, you have to create a config/packages/panther/framework.yaml along with any other configurations you need (in my case I copied my config/packages/test files over. Once that's done your config/packages/panther/framework.yaml needs to not have the lines I've commented below (or just keep them commented like I've shown in the example):

framework:
    ...
    #
    # remove or comment these 2 lines below.
    #session:
    #  storage_factory_id: session.storage.factory.mock_file

why this is needed is explained in the following issue: #159

Once all of these changes are in place I can successfully log in a user using the login form, so at least that works. But I think it should be explained in the documentation a bit better.

I realize this solution is not ideal for your case (I think probably everybody would prefer not having to login using their login form) but at least it's something that others might find useful, I think.

Unfortunately I've abandoned trying to set panther up and can no longer be of help on this particular issue, but I did try to replicate the loginUser() method, like you did, but did not test it after finally setting my environment up correctly, so I don't know if that would've changed anything.

@plotbox-io
Copy link

Hi all. I've got a workable solution for Symfony 6.1. May not be perfect for everyone but maybe can be useful for some people.

  • Add panther environment if doesn't already exist
    • Add .env.panther file
    • Add config/packages/panther directory (copy files from config/packages/test, cheifly doctrine.yaml for me).
    • I use sqlite for doctrine when running tests and make/adjust fixtures during each test, so both my .env.test and .env.panther have the Doctrine var set to DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db
    • Ensure other testing webserver has the APP_ENV environment variable set to panther (I use another Apache/PHP-FPM container rather than the simple native server that is default to Panther as allows me to use Xdebug debugging which I find useful - I just add an env item to the container to do this. It may be automatically done with built in web server, not sure on that..)
  • In config/packages/framework.yaml ensure that both test and panther environments use 'mock session storage' (see example below)
when@test:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

when@panther:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

Mock session storage is the default mechanism when running tests to help avoid complications with native sessions. It essentially dumps the session to a flat file (i.e., "/app/var/cache/{APP_ENV}/sessions"). So by ensuring that both panther and test environments use the same file, we can make the automated authentication work as expected.

See below working example of helper method

protected function logInUser(Client $panther, User $user): void
{
    // Make a single request to avoid 'Invalid cookie domain' error
    $panther->request('GET', '/login');

    // This is the key part - I've just hardcoded the mock session file to be where I know the panther one will
    // be stored in the other (panther env) webserver
    $session = new Session(new MockFileSessionStorage('/app/var/cache/panther/sessions'));
    
    $token = new UsernamePasswordToken($user, 'main', $user->getRoles());
    $session->set('_security_main', serialize($token));
    $session->save();

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

@arderyp
Copy link
Contributor

arderyp commented Sep 30, 2022

@plotbox-io this is a very interesting alternative to my approach. Great write up and clarity. Your solution feels a little more "proper" than mine, so I may move over to your approach in the coming months

@shadydealer
Copy link

shadydealer commented Oct 4, 2022

In config/packages/framework.yaml ensure that both test and panther environments use 'mock session storage' (see example below)

when@test:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

when@panther:
    framework:
        test: true
        session:
            storage_factory_id: session.storage.factory.mock_file

Mock session storage is the default mechanism when running tests to help avoid complications with native sessions. It essentially dumps the session to a flat file (i.e., "/app/var/cache/{APP_ENV}/sessions"). So by ensuring that both panther and test environments use the same file, we can make the automated authentication work as expected.

I think this was exactly the piece that I was missing in order to get this to work, nice work @plotbox-io

@shadydealer
Copy link

@plotbox-io maybe you can create a pull request and try to get the loginUser method merged?

I would also suggest the following changes:

protected function logInUser(object $user, String $firewallContext): void
{
    if (!interface_exists(UserInterface::class)) {
        throw new \LogicException(sprintf('"%s" requires symfony/security-core to be installed.', __METHOD__));
    }

    if (!$user instanceof UserInterface) {
        throw new \LogicException(sprintf('The first argument of "%s" must be instance of "%s", "%s" provided.', __METHOD__, UserInterface::class, get_debug_type($user)));
    }
    // Make a single request to avoid 'Invalid cookie domain' error
    $this->request('GET', '/login');

    // This is the key part - I've just hardcoded the mock session file to be where I know the panther one will
    // be stored in the other (panther env) webserver
    $session = new Session(new MockFileSessionStorage('/app/var/cache/panther/sessions'));
    
    $token = new UsernamePasswordToken($user, $firewallContext, $user->getRoles());
    $session->set('_security_'.$firewallContext, serialize($token));
    $session->save();

    $cookie = new Cookie($session->getName(), $session->getId());
    $this->getCookieJar()->set($cookie);
}
  1. Using $this instead of Client $panther so it could be baked into the Panther Client
  2. Using $firewallContext
  3. Adding some of the checks for the $user and making it an object, because not all project might have a User entity defined.

(2 and 3 are taken from here)

@plotbox-io
Copy link

@shadydealer Thanks for the updated code. Probably would want to inject SessionStorageFactoryInterface as well rather than hardcode. A client of the library could feasibly use a database, redis, or some other method to share the session between the test/panther environment.

I've made a PR here. Let's see what the Symfony team thinks.

Regards,
Richard

@arderyp
Copy link
Contributor

arderyp commented Dec 14, 2022

I like these ideas. Likewise, it would be nice to have an optional string $loginPath = null argument to logInUser() for those of us who don't use /login.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests