Jwt-auth: Another Token blacklisted issue using Laravel 5.4

Created on 8 May 2017  路  15Comments  路  Source: tymondesigns/jwt-auth

Using Laravel 5.4 with "tymon/jwt-auth": "dev-master"

I have a /refresh route setup as follows:

Route::get('/refresh', 'AuthenticateController@refreshToken');

When a user has logged in, I get back a token so that all works fine.

However, when the token expires, my refresh endpoint is called but I get back a TokenBlacklistedException.

My controller refresh is as follows:

public function __construct()
{
    $this->middleware('jwt.refresh', [
        'only' => [
            'refreshToken'
        ]
    ]);
}

public function refreshToken(Request $request)
{
    $token = JWTAuth::getToken();

    try {
        $token = JWTAuth::refresh($token);
    } catch (JWTException $exception) {
        return $this->setStatusCode(500)->respondWithError([
            'token_issue' => 'Could not refresh token',
            'exception' => $exception
        ]);
    }

    return $this->respondWithData(compact('token'));
}

I've spent hours reading over everyone else's issue and Googling for solutions but I still cannot find a fix.

Please help.

Most helpful comment

@rickshawhobo @huy-tran

It was nothing to do with the refresh_ttl values.

My issue was related to how I was using the end point. I guess I misunderstood how refresh tokens worked and after lots of reading, I finally got my head around it and was able to resolve my issue.

So I have this line in my routes/api.php file:

Route::get('/authenticate/refresh', 'AuthenticateController@refreshToken');

In my AuthenticateController I have this:

$this->middleware('jwt.refresh', [
    'only' => [
        'refreshToken'
    ]
]);
/**
 * Returns a refreshed token.
 *
 * @param   Request     $request
 * @return  Response
 */
public function refreshToken(Request $request) {
    return response($request->getContent(), 200, $this->headers);
}

That's it!

The middleware will automatically refresh the token in the request and send it back as an authorization header in the response. Take a look at Tymon\JWTAuth\Middleware\RefreshToken. You'll see a new token getting set as part of the response.

This is why we don't need to grab the token, refresh it and pass it back as per some "solutions" doing the rounds Google, StackOverflow et al. The middleware does all the hard work.

Clients requesting new tokens using this endpoint just need to grab the authorization header from the response and update the token stored client side.

Also, see https://laracasts.com/discuss/channels/general-discussion/how-to-refreshing-jwt-token/replies/101539

Hope it makes sense.

All 15 comments

@seandelaney Check your refresh_ttl config it may be un-refreshable if it's too short.

My refresh_ttl is default value:

'refresh_ttl' => 20160,

I'm getting the same issue. Any resolution for this?

@seanclowdy @rickshawhobo
How did you store your JWT Token? In localStorage and request via client ajax?

I am thinking of you haven't updated your JWT Token, so it still requested with the old token which is blacklisted after refreshed...

@rickshawhobo @huy-tran

It was nothing to do with the refresh_ttl values.

My issue was related to how I was using the end point. I guess I misunderstood how refresh tokens worked and after lots of reading, I finally got my head around it and was able to resolve my issue.

So I have this line in my routes/api.php file:

Route::get('/authenticate/refresh', 'AuthenticateController@refreshToken');

In my AuthenticateController I have this:

$this->middleware('jwt.refresh', [
    'only' => [
        'refreshToken'
    ]
]);
/**
 * Returns a refreshed token.
 *
 * @param   Request     $request
 * @return  Response
 */
public function refreshToken(Request $request) {
    return response($request->getContent(), 200, $this->headers);
}

That's it!

The middleware will automatically refresh the token in the request and send it back as an authorization header in the response. Take a look at Tymon\JWTAuth\Middleware\RefreshToken. You'll see a new token getting set as part of the response.

This is why we don't need to grab the token, refresh it and pass it back as per some "solutions" doing the rounds Google, StackOverflow et al. The middleware does all the hard work.

Clients requesting new tokens using this endpoint just need to grab the authorization header from the response and update the token stored client side.

Also, see https://laracasts.com/discuss/channels/general-discussion/how-to-refreshing-jwt-token/replies/101539

Hope it makes sense.

Hey @seandelaney. This is the most simple solution I have found.

But I'm having one problem: /authenticate/refresh is returning

Undefined property: App\HttpControllers\ApiController::$headers

public function __construct()
{
    $this->middleware('jwt.refresh')->only('refreshToken');
}

...other methods...

public function refreshToken(Request $request)
{
    return response($request->getContent(), 200, $this->headers);
}

@marcelo2605 Have you tried with something like this?:

return response($request->getContent(), 200) ->header('Content-Type', $type) ->header('X-Header-One', 'Header Value') ->header('X-Header-Two', 'Header Value');

@LexBel-co

I'm using just return response($request->getContent(), 200);. This code return form me the new token in the HTTP header. So I don't known what $this->headers do.

@marcelo2605

Sorry for the confusion - the snippet I posted above was from a client project where I had defined headers in the constructor like this:

$this->headers = [
    'Access-Control-Allow-Origin' => '*',
    'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT, DELETE',
    'Access-Control-Allow-Headers' => 'Content-Type, Accept, Authorization, X-Company, X-User-Id, X-Requested-With, Application, Origin',
];

Obviously, this was specific to a project I was working so you can either remove $this->headers, replace $this->headers with an empty array or do what @LexBel-co has suggested.

Glad you found my solution easy to follow.

Thanks @seandelaney !

@seandelaney finally i could figured out how this is work!
But anyway how did the refresh link work? should we do a request to the refresh link
because i'm a little bit confuse, i don't think that link will be triggered automatically.

@jass-trix

Yes you will need to call the refresh endpoint.

@jass-trix

More info to help you out...

In your code where you check for a token but your authenticated call returns false, we know the token has expired. Let the response continue and let your HTTP interceptor catch the 401 and call the refresh token endpoint. The endpoint will send back a new token, you grab it, save it, set authenticated to true and allow the user continue to the page they were wanting to view.

Hope this makes sense?

@seandelaney sorry for the late reply! Well that makes more sense. But however everytime i request a new refreshed token. My Old Token always already blacklisted.
Should i just turn off the blacklist feature?

Let me share my implementation, this intercepts the response, and if there is a new token in the Authorization Header, the interceptor take it and store it in localStorage, in case you need to refresh the token, you can do it in the error.status == 401

`import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(
private router: Router
) {}

intercept(
    request: HttpRequest<any>,
    next: HttpHandler
): Observable<HttpEvent<any>> {
    return next.handle(request).map((event: HttpEvent<HttpResponse<any>>) => {
        if (event instanceof HttpResponse) {
            if (event.headers.get('Authorization')) {
                let token = event.headers.get('Authorization').split(' ');
                localStorage.setItem('id_token', token[1])
            }
        }
        return event;
    }).catch ((error: any) => {
        if (error.headers.get('Authorization')) {
            let token = error.headers.get('Authorization').split(' ');
            localStorage.setItem('id_token', token[1])
        }
        if (error instanceof HttpErrorResponse) {
            switch (error.status) {
                case 401:
                    localStorage.removeItem('id_token');
                    this.router.navigate(['/pages/login']);
                    // Here you can call your refresh token method
                    return Observable.throw(error.error);
                case 403:
                    this.router.navigate(['/pages/403']);
                    return Observable.throw(error.error);
                case 404:
                    this.router.navigate(['/pages/404']);
                    return Observable.throw(error.error);
                default:
                    return Observable.throw(error.error);
            }
        }
    });
}

}`

Was this page helpful?
0 / 5 - 0 ratings