laravel / sanctum

Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.
https://laravel.com/docs/sanctum
MIT License
2.76k stars 298 forks source link

`assertCredentials` is throwing an error #268

Closed sebdesign closed 3 years ago

sebdesign commented 3 years ago

Description:

I'm using Sanctum for SPA authentication on an existing project, and while testing a new feature, I noticed that $this->assertCredentials() throws an error when accessing a route with the auth:sanctum middleware.

The reason of this error is that the default guard is sanctum, which doesn't have a provider by default.

   Error
  Call to a member function retrieveByCredentials() on null

  at vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php:147
    143▕     protected function hasCredentials(array $credentials, $guard = null)
    144▕     {
    145▕         $provider = $this->app->make('auth')->guard($guard)->getProvider();
    146▕
  ➜ 147▕         $user = $provider->retrieveByCredentials($credentials);
    148▕
    149▕         return $user && $provider->validateCredentials($user, $credentials);
    150▕     }
    151▕ }

  1   vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithAuthentication.php:114
      Illuminate\Foundation\Testing\TestCase::hasCredentials()

  2   tests/Feature/Http/Controllers/Api/UsersControllerTest.php:52
      Illuminate\Foundation\Testing\TestCase::assertCredentials()

I've tried calling $this->actingAs($user, 'sanctum') and $this->assertCredentials([..], 'sanctum') and that still fails. The only way that works is $this->assertCredentials([...], 'web').

Another way to fix this is to add this to config/auth.php:

'guards' => [
    'sanctum' => [
        'provider' => 'users',
    ],
],

I remember fixing some related issue in #225 , but that's still not enough. In this comment https://github.com/laravel/sanctum/pull/225#issuecomment-732241569 I mentioned that the root of the problem is #149, which is not implemented correctly. That implementation works when configuring custom guards, but not with a vanilla Sanctum installation.

Steps To Reproduce:

// routes/api.php
Route::resource('user', UsersController::class)->middleware('auth:sanctum');

// tests/Feature/UsersControllerTest.php

$response = $this->actingAs($user)
    ->handleValidationExceptions()
    ->postJson(route('user.store'), [
        'email' => 'info@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

$response->assertJsonMissingValidationErrors()->assertStatus(200);

$this->assertCredentials(['email' => 'info@example.com', 'password' => 'password']);
driesvints commented 3 years ago

Since Sanctum works token based, this method is incompatible with Sanctum.

sebdesign commented 3 years ago

Sanctum is not only token-based, it relies on the user provider of the default guard for authenticating/fetching users for SPA. You can even specify a user provider in config/auth.php.

Even on token-based authentication, it checks the provider (if specified): https://github.com/laravel/sanctum/blob/2.x/src/Guard.php#L71

driesvints commented 3 years ago

Heya, thanks for reporting.

I'll need more info and/or code to debug this further. Can you please create a repository with the command below, commit the code that reproduces the issue and share the repository here? Please make sure that you have the latest version of the Laravel installer in order to run this command. Please also make sure you have both Git & the GitHub CLI tool properly set up.

laravel new sanctum-issue-268 --github="--public"

After you've posted the repository, I'll try to reproduce the issue.

Thanks!

sebdesign commented 3 years ago

Hey @driesvints, I've created a repo for reproducing this issue: https://github.com/sebdesign/sanctum-issue-268

After the installation, I've installed Sanctum without any customization, in a separate commit. The latest commit includes the routes and two tests, one using SPA authentication and one using API tokens.

I'm using an in-memory SQLite database, so after installing this you can run php artisan test. Each test has many assertions for sanity checks, but the assertCredentials assertion that throws the error is at the end.

I hope that's not confusing and that you can understand the process. The key here is that the sanctum guard is missing something to make it work well with the underlying user provider.

driesvints commented 3 years ago

I have to postpone looking into this a bit. I tried setting up the project locally but it causes PhpStorm for me to freeze and crash all the time. It's specific to this project and my colleagues aren't experiencing the same thing so it's specific to my device. I've contacted JetBrains in the hope to figure that out. Please do not remove your test repo for the time being so they can debug themselves.

In the meantime: I noticed you're sending multiple HTTP calls in a single test. This isn't officially support and can lead to edge cases with state management. You're also using an in-memory sqlite database which can be a culprit in combination with this.

Please try the following to see if that helps in resolving to the problem:

Please do not commit these changes to the test repo so JetBrains can use it in its current state to debug the crashing issue.

sebdesign commented 3 years ago

Thanks for the explanation! I'm using Visual Studio code with Ubuntu 18.04 in WSL2, so I don't know what might cause this.

The only reason I'm doing all these HTTP calls is for sanity checks, as a way to describe a more realistic flow. But in my own projects, I only to $this->actingAs($admin) and then $this->post('/api/users') once (also I'm using MySQL 5.7).

I don't believe this is a database issue, because the reason the user provider is null is because the sanctum guard does not have a user provider in the configuration or during the booting of its service provider.

driesvints commented 3 years ago

I think I understand what's going on. It's not so much the sanctum guard that you need to target but the web guard. Try this:

        $this->assertCredentials([
            'email' => 'info@example.com',
            'password' => 'password',
        ], 'web');

Your tests will pass. Also see notes like this in the docs:

Screenshot 2021-04-19 at 14 43 04