Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

SteveRaines's avatar

Dusk with multi-tenancy and custom subdomains

I work for a company that has an app that uses multi-tenancy to serve several customers. Let's call it something like... Really Great Company. Say we've got a customer whose organization is abbreviated as FOO and another that goes by BAR. The first company would visit https://foo.reallygreatcompany.com, and the second https://bar.reallygreatcompany.com. When working on our local machines we use valet to register test sites that we visit in the browser with URLs like http://foo.test and http://bar.test.

I had previously written some Dusk tests before and had them all working, but lately I keep getting an error saying "The user is not authenticated." Here is what one of my tests looks like:

        $this->browse(function (Browser $browser) use ($userId) {
            $browser->loginAs($userId)
                ->assertAuthenticated()
                ->visit(new PublicationsPage())
                ->openPublicationForm('journal article')
                ->addNewAuthor([
                    'first_name' => 'Testy',
                    'last_name' => 'McTester'
                ])
                ->fillOutFormAndSave([
                    'title' => 'Test New Contact Journal Article'
                ])
                ->waitUntilMissing('@subformContainer')
                ->waitForText('Test New Contact Journal Article');
        });

All these tests were working before, but now it seems to be failing with the exception Illuminate\Auth\SessionGuard::login(): Argument #1 ($user) must be of type Illuminate\Contracts\Auth\Authenticatable, null given, called in /Users/steve/code/app/vendor/laravel/dusk/src/Http/Controllers/UserController.php on line 48.

I have seen this Laracasts question that seems related, and I've seen the answer suggested by the OP, but he mentions in his answer

This is assuming you haven't changed the default 'domain' => env('SESSION_DOMAIN'), in your session.php config file.

I tried their solution and nothing changed. Guess whose session config file has been altered that way... That's right, mine just looks like this: 'domain' => null, Git annotations shows the commit message on that line

use redis for sessions

Using xdebug I can see that the loginAs method appears to be working. Then the test proceeds to the assertAuthenticated method where Dusk visits the _dusk/user route. In the user method of vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php I can see by walking through with xdebug that $this->user is null, as is the variable $id, so the code attempts to decrypt the "recaller" cookie to get the user object.

    public function user()
    {
        if ($this->loggedOut) {
            return;
        }

        // If we've already retrieved the user for the current request we can just
        // return it back immediately. We do not want to fetch the user data on
        // every call to this method because that would be tremendously slow.
        if (! is_null($this->user)) {
            return $this->user;
        }

        $id = $this->session->get($this->getName());

        // First we will try to load the user using the identifier in the session if
        // one exists. Otherwise we will check for a "remember me" cookie in this
        // request, and if one exists, attempt to retrieve the user using that.
        if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
            $this->fireAuthenticatedEvent($this->user);
        }

        // If the user is null, but we decrypt a "recaller" cookie we can attempt to
        // pull the user data on that cookie which serves as a remember cookie on
        // the application. Once we have a user we can return it to the caller.
        if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
            $this->user = $this->userFromRecaller($recaller);

            if ($this->user) {
                $this->updateSession($this->user->getAuthIdentifier());

                $this->fireLoginEvent($this->user, true);
            }
        }

        return $this->user;
    }

! is_null($recaller = $this->recaller()) in that final if statement is where it appears to be failing. $recaller is null, as is $this->user, causing the user function to return null, resulting in the exception message that I mentioned previously: Argument #1 ($user) must be of type Illuminate\Contracts\Auth\Authenticatable, null given

Does anyone have any ideas on how to fix this? It sounds like maybe I need to change something with redis? I've never really messed with redis before so I'm kind of in the dark here.

0 likes
2 replies
LaryAI's avatar
Level 58

The issue seems to be related to the session configuration. Since the app uses multi-tenancy with custom subdomains, the session domain needs to be set dynamically based on the subdomain. One solution is to set the session domain in the middleware based on the current subdomain. Here's an example:

// app/Http/Middleware/SetSessionDomain.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetSessionDomain
{
    public function handle(Request $request, Closure $next)
    {
        $subdomain = $request->route('subdomain');
        if ($subdomain) {
            config(['session.domain' => $subdomain . '.reallygreatcompany.com']);
        }
        return $next($request);
    }
}

Then, add this middleware to the web middleware group in app/Http/Kernel.php:

// app/Http/Kernel.php

protected $middlewareGroups = [
    'web' => [
        // ...
        \App\Http\Middleware\SetSessionDomain::class,
    ],
    // ...
];

Finally, update the Dusk test to include the subdomain in the URL:

$this->browse(function (Browser $browser) use ($userId) {
    $browser->loginAs($userId)
        ->visit('http://foo.test') // replace with the appropriate subdomain
        ->assertAuthenticated()
        ->visit(new PublicationsPage())
        // ...
});

Note that this solution assumes that the subdomain is passed as a route parameter. If it's included in the URL, you'll need to modify the middleware accordingly.

SteveRaines's avatar

I wish I had more details for anyone else who might run into this problem, but after talking with a coworker I ran php artisan dusk:install to reinstall the Chrome drivers, then I ran composer dump-autoload, and finally I copied my coworker's .env.dusk file in place of my own and ran php artisan optimize:clear and ran my tests again. That seems to have resolved the issue.

EDIT: Okay, this WAS working for a few minutes, and then I ran a Dusk test through the little green play button next to the method header in phpstorm, as opposed to how I would normally run it (iTerm). Now I'm getting the "user cannot be authenticated" error again. Really strange.

I've also noticed that I can't seem to use Chrome. I've been attempting to toubleshoot an unrelated error on a different ticket by using the same browser that the user used (Chrome, obviously) when they had the error. I keep trying to open Chrome and nothing comes up. I wonder if it's a chrome webdriver issue that's causing all these problems?

Please or to participate in this conversation.