- 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
I’m pretty sure the problem is with your
login
API endpoint.Do not use
Auth
orauth()
methods to implement your login. For example, DO NOT implement withauth()->attempt($credentials)
.Your
login
API should be:User::where('email', request()->email)->first()
);with
Hash::check(request()->password, $user->password)
;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 allAuth
calls. Ex.auth('sanctum')->user()
.Why this exception happens?
This exception means
currentAccessToken()
is returning aTransientToken
object instead ofPersonalAccessToken
.The
TransientToken
is returned when Sanctum detects a cookie-based authentication. It is not a database record and doesn’t have adelete()
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 toauth('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)
oractingAs($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.
I face the same problem, application work fine but failed in test. I resolve this by editing
config/sanctum.php
:origin:
change to following:
As other answers say, the default guard is
web
, but we use Sanctum as api token. The point is don’t useweb
guard.The following text is from
config/sanctum.php
comments: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.