panther icon indicating copy to clipboard operation
panther copied to clipboard

Panther with Authentication

Open Sethbass opened this issue 5 years ago • 43 comments
trafficstars

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' => '[email protected]']);

        $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

Sethbass avatar Jan 16 '20 12:01 Sethbass

I have the same issue.

ioniks avatar Jan 23 '20 12:01 ioniks

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 avatar Jan 31 '20 19:01 Sethbass

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,

Sethbass avatar Feb 03 '20 10:02 Sethbass

here same problem... some glue? thanks!

andrescevp avatar Feb 04 '20 13:02 andrescevp

Same problem here... any idea ?

gponty avatar Mar 17 '20 13:03 gponty

@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...

Sethbass avatar Mar 17 '20 13:03 Sethbass

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' => '[email protected]']);

        $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);
    }
}

JohnstonCode avatar May 13 '20 08:05 JohnstonCode

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 :)

Sethbass avatar May 15 '20 06:05 Sethbass

Thanks, looks good hopefully this resolves our issue.

JohnstonCode avatar May 15 '20 08:05 JohnstonCode

Same issue here :/

antoine1003 avatar May 15 '20 19:05 antoine1003

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 ?

bastien70 avatar Jul 27 '20 15:07 bastien70

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

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

nizarRebhi avatar Aug 16 '20 13:08 nizarRebhi

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

kolvin avatar Aug 24 '20 10:08 kolvin

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

shadowc avatar Feb 14 '21 03:02 shadowc

@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

njsa04 avatar Feb 15 '21 16:02 njsa04

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!

drupol avatar Mar 08 '21 10:03 drupol

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!

madxmind avatar Jun 29 '21 14:06 madxmind

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

hutnikau avatar Aug 25 '21 20:08 hutnikau

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.

holema avatar Dec 19 '21 17:12 holema

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?

chadcasey avatar Dec 22 '21 17:12 chadcasey

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 ?

alexandre-mace avatar Apr 01 '22 15:04 alexandre-mace

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: https://github.com/symfony/panther/issues/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 avatar May 17 '22 01:05 arderyp

@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?

arderyp avatar May 17 '22 02:05 arderyp

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: https://github.com/symfony/panther/issues/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?

drupol avatar May 17 '22 03:05 drupol

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 avatar May 17 '22 03:05 arderyp

@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.

arderyp avatar May 17 '22 04:05 arderyp

@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.

drupol avatar May 17 '22 05:05 drupol

all good, thanks for the recommendation

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

arderyp avatar May 17 '22 05:05 arderyp

@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!

shadowc avatar May 17 '22 07:05 shadowc

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!

drupol avatar May 17 '22 07:05 drupol