laravel/dusk

Dusk won't store cookies on SSL site with self-signed certs

phpguru opened this issue · 8 comments

  • Dusk Version: 7.1.1
  • Laravel Version: 9.43.0
  • PHP Version: 8.1.11
  • Database Driver & Version: mysqli 8 pdo

Description:

  • I have a legacy site on which I am attempting to use Dusk for browser testing.
  • I have implemented self-signed certificates for the local domain.
  • I have implemented a RootCA which I used to sign the local domain's SSL certificate.
  • I have imported the RootCA.crt to my laptop's Keychain and set it to Always Trust.
  • The website's SSL is now working/accepted/green even in Chrome. It is recognized as a valid SSL cert.
  • When I test the site using the SSL domain, Dusk refuses to accept cookies, so my login fails.
  • When I test the site using http://localhost, Dusk accepts the cookies and I can login without issue.

Steps To Reproduce:

  1. Run a site under http://localhost and write a test that forces a cookie to be written.
  2. Generate a self-signed certificate for the site, such as https://sample.local and add the CRT to your trust store.
  3. Verify the site's SSL is accepted by Chrome.
  4. Run the dusk test. It will refuse to accept the cookie.
  5. If you comment out --headless in DuskTestCase.php you can see the site comes up with the certificate un-accepted
  6. Adding --ignore-certificate-errors, ignore-ssl-errors and ->setCapability('acceptInsecureCerts', true) have no effect on cookies, they're simply ignored regardless of these configuration options

Question:

Does anyone know where Chrome Web Driver looks for trusted certificates? It appears to ignore the RootCA set to Always Trust in Keychain access.

Goal:

As a user of Dusk on a site with a self-signed certificate, I should be able to test the site with SSL, but as long as Dusk doesn't accept cookies while it refuses to accept my trusted RootCA, I am forced to use http://localhost to do Dusk testing.

Thank you for reporting this issue!

As Laravel is an open source project, we rely on the community to help us diagnose and fix issues as it is not possible to research and fix every issue reported to us via GitHub.

If possible, please make a pull request fixing the issue you have described, along with corresponding tests. All pull requests are promptly reviewed by the Laravel team.

Thank you!

As a side note, many possibly-related threads can be found on StackOverflow with search terms like chrome webdriver selenium self signed certificate secure cookies, but nearly all of them deal with getting Chrome to accept secure certs and I already have that working. Secondarily, this could be an issue with my system or with Chromium. I am posting here to see if the community has insight. I will be updating this issue with any discoveries I find, and potentially a PR if applicable.

Cross-posted to ChromeDriver community here.

Hi @phpguru,

  1. Run a site under http://localhost and write a test that forces a cookie to be written.
  2. Generate a self-signed certificate for the site, such as https://sample.local and add the CRT to your trust store.

How are you serving these local domains? Are you using something like Valet, Sail, Homestead etc.?

Adding --ignore-certificate-errors, ignore-ssl-errors and ->setCapability('acceptInsecureCerts', true) have no effect on cookies, they're simply ignored regardless of these configuration options

Can you share the exact code you used in these cases?

Thanks for the reply @staudenmeir -- the site I am testing is running in Docker - it's a legacy PHP 7.4 app running under Apache2 with mod-php on port 80, and Traefik is acting as a reverse proxy and SSL termination. The SSL certs are self-signed by rootCA that is imported to Keychain Access. I managed to get Chrome to actually accept it, but when running in webdriver, Dusk ignores it.

I have tried the SSL-related settings in the base class, as in the example below:

<?php
// DuskTestCase.php

namespace Tests;

use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Laravel\Dusk\TestCase as BaseTestCase;

abstract class DuskTestCase extends BaseTestCase
{
    use CreatesApplication;

    /**
     * Prepare for Dusk test execution.
     *
     * @beforeClass
     * @return void
     */
    public static function prepare()
    {
        if (! static::runningInSail()) {
            static::startChromeDriver();
        }
    }

    /**
     * Create the RemoteWebDriver instance.
     *
     * @return \Facebook\WebDriver\Remote\RemoteWebDriver
     */
    protected function driver()
    {
        $options = (new ChromeOptions)->addArguments(collect([
            $this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
        ])->unless($this->hasHeadlessDisabled(), function ($items) {
            return $items->merge([
                '--disable-gpu',
                '--headless',
            ]);
        })->all());

        return RemoteWebDriver::create(
            $_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515',
            DesiredCapabilities::chrome()
                ->setCapability(ChromeOptions::CAPABILITY, $options)
                ->setCapability('acceptInsecureCerts', true)
        );
    }

    /**
     * Determine whether the Dusk command has disabled headless mode.
     *
     * @return bool
     */
    protected function hasHeadlessDisabled()
    {
        return isset($_SERVER['DUSK_HEADLESS_DISABLED']) ||
               isset($_ENV['DUSK_HEADLESS_DISABLED']);
    }

    /**
     * Determine if the browser window should start maximized.
     *
     * @return bool
     */
    protected function shouldStartMaximized()
    {
        return isset($_SERVER['DUSK_START_MAXIMIZED']) ||
               isset($_ENV['DUSK_START_MAXIMIZED']);
    }
}

The Dusk test I made as a POC performs login against it:

<?php
// LoginTest.php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Illuminate\Support\Facades\Config;
use Throwable;

class LoginTest extends DuskTestCase
{
    /**
     * A Dusk test example.
     *
     * @return void
     * @throws Throwable
     */
    public function testLogin(): void
    {
        $user = new \stdClass();
        $user->username = '<USERNAME_REDACTED>';
        $user->password = '<PASSWORD_REDACTED>';
        $this->browse(function ($browser) use ($user) {
            $browser
                ->visit(Config::get('dusk.base.url').'/login.php')
                ->waitForText('Username')
                ->assertSee('Password')
                ->type('input#username', $user->username)
                ->type('input#password', $user->password)
                ->press('input[name=login]')
                ->assertPathIs('/dashboard/')
                ;
        });
    }
}

My test is failing when running with the SSL certificate and having https://my.local.app as the Dusk base URL. I've taken a couple screenshots to confirm this and when I commented out --headless I could see the SSL certificate was unaccepted, and the cookie was refused. If I ignore SSL, the Traefik config, start the legacy app on port 80, and set the Dusk base URL to http://localhost, the test passes.

having https://my.local.app/ as the Dusk base URL.

Is that the same URL you generated the certificate for and visit in the "normal" browser?

Can you add something like ->pause(60_000) to the test and inspect the certificate issue while the browser is open?

  • Does the server actually deliver your custom certificate?
  • If so, what does Chrome say why it's not valid (e.g. NET::ERR_CERT_AUTHORITY_INVALID)?

Yes, the domain my.local.app is the self-signed certificate I made, along with a root CA which I used to self-sign the cert for the domain (along with a variety of others). In Chrome desktop, this cert is recognized as valid by setting "Always Trust" on the imported rootCA.crt into Keychain Access.

I have tried to replicate the issue after pivoting away from this issue, using http://localhost - in any event, when I tried to replicate the issue, I am now getting this error in Chrome when opening the page:

NET::ERR_CERT_VALIDITY_TOO_LONG

which is odd. I don't think this is the same error I was getting before. I am going to regenerate self-signed certificates with 1 year validity and see if that fixes the isssue.

Confirmed:

I regenerated my rootCA.crt with --days 365 and then recreated my.local.app.crt with --days 365 and everything works.