Laravel JWT tokens are Invalid after refresh them in a authentication JWT approach

16,894

When I get this issue, the solution that I found to get my project working was to generate a new token with data from older token on each new request.

My solution, that works for me, is bad, ugly, and can generate more issues if you have many async requests and your API(or business core) server is slow.

For now is working, but I will investigate more this issue, cause after 0.5.3 version the issue continues.

E.g:

Request 1 (GET /login):

Some guest data on token

Request 2 (POST /login response):

User data merged with guest data on old token generating a new token

Procedural code example(you can do better =) ), you can run this on routes.php out of routes, I say that is ugly haha:

// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
    if($authToken === null) {
         $authToken = JWTAuth::parseToken();
    }
    return $authToken;
};

$getLoggedUser = function() use ($getAuthToken) {
    return $getAuthToken()->authenticate();
};

$getAuthPayload = function() use ($getAuthToken) {
    try {
        return $getAuthToken()->getPayload();
    } catch (Exception $e) {
        return [];
    }
};

$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
    $currentPayload = [];
    try {
        $currentAuthPayload = $getAuthPayload();
        if(count($currentAuthPayload)) {
            $currentPayload = $currentAuthPayload->toArray();
        }
        try {
            if($user = $getLoggedUser()) {
                $currentPayload['user'] = $user;
            }
            $currentPayload['isGuest'] = false;
        } catch (Exception $e) {
            // is guest
        }
    } catch(Exception $e) {
        // Impossible to parse token
    }

    foreach ($customPayload as $key => $value) {
        $currentPayload[$key] = $value;
    }

    return $currentPayload;
};

// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
    $getLoggedUser();
    $payload = ['isGuest' => false];
} catch (Exception $e) {
    $payload = ['isGuest' => true];
}

try {
    $payload = $mountAuthPayload($payload);
} catch (Exception $e) {
    // Make nothing cause token is invalid, expired, etc., or not exists.
    // Like a guest session. Create a token without user data.
}

Some route(simple example to save user mobile device):

Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
    Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
        $Response = new \Illuminate\Http\Response();
        $user = $getLoggedUser();

        // code to save on database the user device from current "session"...

        $payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
        $token = JWTAuth::encode($payload);
        $Response->header('Authorization', 'Bearer ' . $token);

        $responseContent = ['setted' => 'true'];

        $Response->setContent($responseContent);
        return $Response;
    });
});
Share:
16,894
Maykonn
Author by

Maykonn

Solid knowledge on: I'm a web backend software architect 10+ years working with programing problems, especially on distributed cloud solutions Backend, APIs, microservices and cloud infrastructure specialist Architecture and development of RESTful APIs Complex and critical problem solving GoF Patterns, DDD, SOLID Programming concise, clean, without coupling, easy maintainability, safe and performance. Laravel, Symfony, Zend, Yii, and much more Redis and Memcached as cache or DB Apache, Nginx, Node.js Relational and NoSQL databases UML, analisys, management, engineering and software architecture Agile Development and Classics Methodologies Versioning, GIT, SVN https://github.com/maykonn

Updated on June 14, 2022

Comments

  • Maykonn
    Maykonn almost 2 years

    EDIT:

    Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83

    MY ORIGINAL QUESTION:

    I'm implement with jwt-auth my protected resources that require an authenticated user with bellow code:

    Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() {
        // Protected routes
    });
    

    When user 'sign in' on API an Authorization token is created, and sent on response Authorization header to client application that call the resource. So, client applications when intercept a Authorization token on header of any response, set a variable/session/whatever with this token value, to send again to API on next request.

    The first request for a protected resource after 'login' works fine, but the next client application request to API with a refreshed token, gives the following error (API mount all responses in json format):

    {
        "error": "token_invalid"
    }
    

    What can be happen with refreshed tokens? My refresh token implementation (set as a after middleware) is wrong? Or isn't necessary to manually refresh all Authorization token that come with client apps requests?

    UPDATE:

    I update the jwt-auth RefreshToken middleware as propose here, but the token_invalid persist.

    BUG:

    I guess that I found what happens. Note that in the refresh method, old token is added to blacklist cache case enabled:

    // Tymon\JWTAuth\JWTManager
    public function refresh(Token $token)
    {
        $payload = $this->decode($token);
    
        if ($this->blacklistEnabled) {
            // invalidate old token
            $this->blacklist->add($payload);
        }
    
        // return the new token
        return $this->encode(
            $this->payloadFactory->setRefreshFlow()->make([
                'sub' => $payload['sub'],
                'iat' => $payload['iat']
            ])
        );
    }
    

    And note that in add to blacklist method the key is the jti param from old token payload:

    // Tymon\JWTAuth\Blacklist
    public function add(Payload $payload)
    {
        $exp = Utils::timestamp($payload['exp']);
    
        // there is no need to add the token to the blacklist
        // if the token has already expired
        if ($exp->isPast()) {
            return false;
        }
    
        // add a minute to abate potential overlap
        $minutes = $exp->diffInMinutes(Utils::now()->subMinute());
    
        $this->storage->add($payload['jti'], [], $minutes);
    
        return true;
    }
    

    Thus, when has on blacklist method is called, the old token jti param is the same that the new, so the new token is in blacklist:

    // Tymon\JWTAuth\Blacklist
    public function has(Payload $payload)
    {
        return $this->storage->has($payload['jti']);
    }
    

    If you don't need the blacklist functionality just set to false on jwt.php configuration file. But I can't say if it expose to some security vulnerability.

    Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83