Reputation: 2918
I'm use jwt-auth to make a RESTful auth resource in my API. When a client app calls the login resource, case user is logged, your current token must be invalidated and so a new token generated.
But case the current token is blacklisted a TokenBlacklistedException
is thrown.
How to verify if a token is blacklisted? Or how to correct implement an user "logout"? I try to found on jwt-auth API source but not exists a getToken()->isBlacklisted()
or parseToken()->isBlacklisted()
or some validator to implement it.
Ever token is invalid parseToken() throws a TokenBlacklistedException, so an isBlacklisted method is a good way to verify if token is valid before invalidate a token.
INFO:
The bellow code verify if payload is invalid, thrown the TokenBlacklistedException
if is invalid:
if(
false === \Tymon\JWTAuth\Blacklist::has(
\Tymon\JWTAuth\Facades\JWTAuth::getPayload($token)
)
) {
\Tymon\JWTAuth\Facades\JWTAuth::parseToken()->invalidate();
}
How to verify like:
if(false ===\Tymon\JWTAuth\Facades\JWTAuth::parseToken()->isBlacklisted()) {
// invalidate...
}
Upvotes: 13
Views: 37270
Reputation: 5116
You can simply destroy the session on the client side when they logout, and "invalidate" the token on the backend (which's just another term for "blacklist", at least in JWT).
Technically destroying the token on the client side will be enough, but for session hijacking, invalidating it on the backend is a good idea too.
If you are invalidating, you'll need to destroy/ forget the frontend's token after you get your response from Laravel.
// Maybe set below to `false`,
// else cache may take too much storage.
$forever = true;
// Both loads and blacklists
// (the token, if it's set, else may raise exception).
JWTAuth::parseToken()->invalidate( $forever );
OR
JWTAuth::getToken(); // Ensures token is already loaded.
JWTAuth::invalidate($forever);
OR
$token = \JWTAuth::parseToken();
\JWTAuth::manager()->invalidate(
new \Tymon\JWTAuth\Token($token->token),
$forever
);
Note that Laravel's cache-driver should not be
array
, as blacklist is stored as cache.See stackoverflow.com/Which cache driver to use?
Then on angular side:
function logout()
{
UserService.logout().$promise.then(function() {
$cookieStore.remove('userToken');
// redirect or whatever
});
}
One way you can handle JWT exceptions is to setup an EventServiceProvider
in laravel, here is what mine looks like:
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider {
/**
* The event handler mappings for the application.
*
* @var array
*/
protected $listen = [
'tymon.jwt.valid' => [
'App\Events\JWTEvents@valid',
],
'tymon.jwt.user_not_found' => [
'App\Events\JWTEvents@notFound'
],
'tymon.jwt.invalid' => [
'App\Events\JWTEvents@invalid'
],
'tymon.jwt.expired' => [
'App\Events\JWTEvents@expired'
],
'tymon.jwt.absent' => [
'App\Events\JWTEvents@missing'
]
];
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
{
parent::boot($events);
//
}
}
You'll register that in your app.php.
Then I implement the JWTEvents class with methods for each event.
class JWTEvents extends Event {
// Other methods
public function invalid()
{
return response()->json(['error' => 'Token Invalid'], 401);
die();
}
}
Important thing to note is that we are catching the JWT exceptions and returning a json response with a specific status code.
On the angular side, I have in my httpInterceptor class, catches for these http status codes.
angular.module('ngApp')
.factory('httpInterceptor', function($q, $log, $cookieStore, $rootScope, Response) {
return {
request: function(config) {
// Where you add the token to each request
},
responseError: function(response) {
// Check if response code is 401 (or whatever)
if (response.status === 401) {
// Do something to log user out & redirect.
$rootScope.$broadcast('invalid.token');
}
}
}
});
Upvotes: 21
Reputation: 635
This works for me.
public function logout( Request $request ) {
// No need to get token, as "parseToken()" does that itself.
//$token = $request->header( 'Authorization' );
try {
// Adds token to blacklist.
$forever = true;
JWTAuth::parseToken()->invalidate( $forever );
return response()->json( [
'error' => false,
'message' => trans( 'auth.logged_out' )
] );
} catch ( TokenExpiredException $exception ) {
return response()->json( [
'error' => true,
'message' => trans( 'auth.token.expired' )
], 401 );
} catch ( TokenInvalidException $exception ) {
return response()->json( [
'error' => true,
'message' => trans( 'auth.token.invalid' )
], 401 );
} catch ( JWTException $exception ) {
return response()->json( [
'error' => true,
'message' => trans( 'auth.token.missing' )
], 500 );
}
}
Upvotes: 8
Reputation: 41
As far as I understand, one thing that nobody stressed is the 'jwt.refresh' (aka RefreshTokenMiddleware) used to refresh the token.
Now, if anyone who wants to perform a logout action wraps the controller method in a route like
Route::group(['middleware' => ['jwt.auth', 'jwt.refresh']], function()...
for sure will get a new token in logout response hence the client will be able to perform new requests.
Hope this can help clarify this issue.
Upvotes: 4