skip to Main Content
  • Sanctum Version: ^2.11.2
  • Laravel Version: 8.54
  • PHP Version: 7.3|^8.0
  • Database Driver & Version:

Description:

deleting current user token works fine
but in test it throw exception

Steps To Reproduce:

routes/api.php


Route::prefix('auth')->middleware('auth:sanctum')->group(function () {
    Route::delete('/sign_out', [AuthController::class, 'signOut']);
   /// other routes
});

appHttpControllersApiAuthAuthController.php

//  * delete the current access token
    public function signOut(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return  response()->json(
            [
                'message' => 'auth.signed_out_successfully'
            ],
        );
    }

the test

  public function test_when_signed_out_the_token_gets_deleted()
    {

        // * create new user
        User::factory()->create(['email' => '[email protected]']);

        // * sign in the user
        $response = $this->withHeaders(
            ['accept' => 'Application/json']
        )->postJson('/api/auth/sign_in', [
            'email' => '[email protected]',
            'password' => 'top_secret',
            'device_name' => 'test_device'
        ]);
        $response->assertStatus(200);
        $object = $response->getData();
        $array = json_decode(json_encode($object), true);
        $token = $array['data']['token'];


        /// get current tokens count from the server
        $queryResult  = DB::select('select Count(*) from personal_access_tokens')[0];
        $array = json_decode(json_encode($queryResult), true);
        $tokensCount = $array["Count(*)"];
        $this->assertEquals("1", $tokensCount);


        // * sign out request
        $response = $this->withHeaders(
            [
                'accept' => 'Application/json',
                'Authorization' => 'Bearer ' . $token,
            ]
        )->delete('/api/auth/sign_out');
        dd($response->getData());
        $response->assertStatus(200);


        /// get current tokens count from the server
        $queryResult  = DB::select('select Count(*) from personal_access_tokens')[0];
        $array = json_decode(json_encode($queryResult), true);
        $tokensCount = $array["Count(*)"];
        $this->assertEquals("0", $tokensCount);
    }

stack trace

{
  "message": "Call to undefined method LaravelSanctumTransientToken::delete()",
  "exception": "Error",
  "file": "<path>appHttpControllersApiAuthAuthController.php",
}

2

Answers


  1. I’m pretty sure the problem is with your login API endpoint.

    Do not use Auth or auth() methods to implement your login. For example, DO NOT implement with auth()->attempt($credentials).

    Your login API should be:

    • Find the user record (ex. User::where('email', request()->email)->first());
    • Check the password
      with Hash::check(request()->password, $user->password);
    • Return a
      token with $user->createToken('something')->plainTextToken.

    If you use Auth for your login, you are using the "web" guard, even if not explicitly defined. The web guard is made for cookie-based authentication, not for tokens.

    Also, explicitly use the sanctum guard on all Auth calls. Ex. auth('sanctum')->user().

    Why this exception happens?

    This exception means currentAccessToken() is returning a TransientToken object instead of PersonalAccessToken.

    The TransientToken is returned when Sanctum detects a cookie-based authentication. It is not a database record and doesn’t have a delete() method.

    The PersonalAccessToken is returned on token-based authentication. It is an Eloquent record and can be deleted.

    Why does it only happens in tests?

    The way Sanctum detects if it is a cookie or token authentication is by checking auth('web')->user().

    If there is a user in the "web" guard, it is a cookie-based authentication. If there’s no user, it is a token-based authentication.

    The "web" guard is made to be used with cookies and session middlewares. It doesn’t work properly without them and shouldn’t be used on APIs.

    Your code works in a server because auth('web')->user() returns null. But in testing, because your login API call sets a user to auth('web')->user(), the next API call will have the user loaded, and Sanctum treats it as a cookie-based authorization.

    Alternative solution for login in tests

    No matter what, you should never use the "web" guard in a code that is token-based. Make sure you explicitly specify the "sanctum" (or whatever non-session guard) on auth calls. Ex.: auth('sanctum')->user().

    That said, you have an alternative for login in tests.

    In tests, you can replace your login calls with Sanctum::actingAs($user) or actingAs($user, 'sanctum'). Just make sure to specify the Sanctum guard.

    If you are testing tokens, I recommend making an API call to login instead of authenticating with "actingAs". Otherwise, you won’t be testing in the same way as in real life and may not catch some errors.

    Login or Signup to reply.
  2. I face the same problem, application work fine but failed in test. I resolve this by editing config/sanctum.php:

    origin:

    [
        'guard' => ['web'],
    ]
    

    change to following:

    [
        'guard' => [],  // clear this config array
    ]
    

    As other answers say, the default guard is web, but we use Sanctum as api token. The point is don’t use web guard.

    The following text is from config/sanctum.php comments:

    If none of these guards are able to authenticate the request, Sanctum will use the bearer token that’s present on an incoming request for authentication.

    Therefore, just remove the guard and leave it empty.


    BTW, in other test should use actingAs() is correct, but login test, should simulate how user act. I agree the test content of this post.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search