jwt-auth icon indicating copy to clipboard operation
jwt-auth copied to clipboard

When I do refresh token?

Open punnawat opened this issue 7 years ago • 32 comments

My back-end is Laravel framwork with jwt-auth and front-end is mobile application with React Native. When I call to API (back-end), I must refresh token every time or not?

If I refresh a token when it expired, it is secure? Do you have an automate refresh token function?

punnawat avatar Oct 13 '16 17:10 punnawat

Its not necessary to refresh the token every time but only when its almost expiring/expired, You can't use an expired token. refresh it before use at its already been blacklisted You can use the following to refresh the token

$token = JWTAuth::getToken();
$new_token = JWTAuth::refresh($token);

However if you want the token to refresh on every request(discouraged) add the jwt.refresh middleware in your app\Http\Kernel.php to be

protected $routeMiddleware = [
    ...
    'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
    'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
];

then add the jwt.refresh middleware in your routes

knox2 avatar Oct 14 '16 17:10 knox2

If I don't refresh a token every time, how to check the expire token?

punnawat avatar Oct 15 '16 04:10 punnawat

You can simply make another request with respective (old) token. The jwt.auth middleware will tell you, that the token is expired. Then you need to re-login in order to get a new token.

Not the best way, but works ;)

johannesschobel avatar Oct 20 '16 06:10 johannesschobel

Hello, this is custom middleware that I use, maybe could help... When the token is expired, the refreshed token is added to the response headers. The app just needs to search if the response has this, if so, update the saved token.

public function handle($request, Closure $next)
    {
        // caching the next action
        $response = $next($request);

        try
        {
            if (! $user = JWTAuth::parseToken()->authenticate() )
            {
                return ApiHelpers::ApiResponse(101, null);
            }
        }
        catch (TokenExpiredException $e)
        {
            // If the token is expired, then it will be refreshed and added to the headers
            try
            {
                $refreshed = JWTAuth::refresh(JWTAuth::getToken());
                $response->header('Authorization', 'Bearer ' . $refreshed);
            }
            catch (JWTException $e)
            {
                return ApiHelpers::ApiResponse(103, null);
            }
            $user = JWTAuth::setToken($refreshed)->toUser();
        }
        catch (JWTException $e)
        {
            return ApiHelpers::ApiResponse(101, null);
        }

        // Login the user instance for global usage
        Auth::login($user, false);

        return $response;
    }

cristianpontes avatar Oct 21 '16 20:10 cristianpontes

@cristianpontes Thanks for solution!!It should work.

I have one query regarding above code .

What is "ApiHelpers::ApiResponse(101, null) ;" in above code ?

plgautam avatar Oct 27 '16 08:10 plgautam

@plgautam the "ApiHelpers::ApiResponse(101, null) ;" is just global helper that I use for simplify the responses in my API, the response schema is always the same. Let me show you how it will work without this helper.

public function handle($request, Closure $next)
    {
        try
        {
            if (! $user = JWTAuth::parseToken()->authenticate() )
            {
                 return response()->json([
                   'code'   => 101 // means auth error in the api,
                   'response' => null // nothing to show 
                 ]);
            }
        }
        catch (TokenExpiredException $e)
        {
            // If the token is expired, then it will be refreshed and added to the headers
            try
            {
                $refreshed = JWTAuth::refresh(JWTAuth::getToken());
                $user = JWTAuth::setToken($refreshed)->toUser();
                header('Authorization: Bearer ' . $refreshed);
            }
            catch (JWTException $e)
            {
                 return response()->json([
                   'code'   => 103 // means not refreshable 
                   'response' => null // nothing to show 
                 ]);
            }
        }
        catch (JWTException $e)
        {
            return response()->json([
                   'code'   => 101 // means auth error in the api,
                   'response' => null // nothing to show 
            ]);
        }

        // Login the user instance for global usage
        Auth::login($user, false);

        return  $next($request);
    }

BTW, if you have some problems setting headers in the middleware, use this update.

cristianpontes avatar Oct 27 '16 11:10 cristianpontes

@cristianpontes thanks for reply. This solve my query. Great work friend!!!

plgautam avatar Oct 27 '16 11:10 plgautam

@cristianpontes Thanks so much.

punnawat avatar Nov 19 '16 08:11 punnawat

FYI: When generating a token add another field along with token for expiration unix timestamp. That way your frontend framework knows when to resend for refresh token.

prionkor avatar Jun 11 '17 20:06 prionkor

@cristianpontes I'm looking for this. Thanks.

Testing my API in Postman, the URL return {"code":103,"response":null} after token expires.

marcelo2605 avatar Aug 23 '17 19:08 marcelo2605

@cristianpontes shouldn't we ask the user to re-login if the token is expired? if we refresh the token automatically, we might end up giving the user a never expiring page, isn't it? Please clarify me. Thanks.

dhayanithims avatar Oct 08 '17 12:10 dhayanithims

I want to save the new jwt token in redux store how can I do it

zhangwei900808 avatar Oct 14 '17 08:10 zhangwei900808

@dhayanithims the refreshed token is created only if the expired token have a expiration time less than refresh_ttl minutes. The refresh_ttl value is defined on path "config/jwt.php".

` /* |--------------------------------------------------------------------------

Refresh time to live
Specify the length of time (in minutes) that the token can be refreshed
within. I.E. The user can refresh their token within a 2 week window of
the original token being created until they must re-authenticate.
Defaults to 2 weeks
*/

'refresh_ttl' => 20160,`

philliperosario avatar Oct 17 '17 23:10 philliperosario

I have tried @cristianpontes solution but after the token expires I get the code 101 which means there api an auth error in the api? I used the code from the Laravel 5 in the wiki (https://github.com/tymondesigns/jwt-auth/wiki/Creating-Tokens). What could cause this?

Also I am using Vue so I did an interceptor to get every response. Do I just reassign the token there to localStorage and the header?

//Check the request headers for a bad token

window.axios.interceptors.response.use(function (response) {

    // Do something with response data
    if( response.headers.authorization ){
        //Assign token?
    }

    return response;

}, function (error) {

    console.log(error);

    return Promise.reject(error);

});

packytagliaferro avatar Oct 20 '17 20:10 packytagliaferro

@packytagliaferro

import { get } from 'lodash'
import axios from 'axios'
import store from 'vuex-store'

export const connection = (options = {}) => {
  options.headers = { Authorization: store.getters.auth.getToken() }
  const instance = axios.create(options)

  instance.interceptors.response.use(function (response) {
    const newtoken = get(response, 'headers.authorization')
    if (newtoken) store.dispatch('login', newtoken)
    return response
  }, function (error) {
    switch (error.response.status) {
      case 401:
        store.dispatch('logoff')
        break
      default:
        console.log(error.response)
    }
    return Promise.reject(error)
  })

  return instance
}

philliperosario avatar Oct 20 '17 21:10 philliperosario

@philliperosario Thats perfect! Solved two of my issues. I will just extend the token time a little. Once thing I did notice is my axios.interceptors.response doesnt catch the error on the first call, but if I refresh and it makes the call again, it works and logs my user out.

packytagliaferro avatar Oct 20 '17 21:10 packytagliaferro

@packytagliaferro Since you've solved two of your problems, I'm going to give you a third problem. Try submit simultaneously two requests to server.

philliperosario avatar Oct 20 '17 22:10 philliperosario

@philliperosario Hmmm, sorry I am not sure what you mean. Shouldnt the interceptor run on every call? Here is my code

auth.js

import {store} from './store';


//Set the token
let token = window.hopbak.jwt_token;


//Check the request headers for a bad token

window.axios.interceptors.response.use(function (response) {

    console.log(response);
    
    return response;

}, function (error) {

    if( error.response.status == 401 ){
        store.commit('logout');
    }

   return Promise.reject(error);

});


//If we have a token, send it with the request
if (token) {

    window.axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;

} else {
    console.log('no token');
}

packytagliaferro avatar Oct 20 '17 23:10 packytagliaferro

@packytagliaferro First, do not set the token statically at the beginning of the code, it will always be changing and being replaced by refreshed_token. So use a function that returns the token (so I call a getter of vuex) otherwise the axios will always send the old token.

@packytagliaferro Since you've solved two of your problems, I'm going to give you a third problem. Try submit simultaneously two requests to server.

I meant that there is a hidden problem in this whole discussion. If your client submits two requests at the same time to the server, the token of the first request will be updated and the second request will fail. If you are using a tymon/jwt-auth "1.0.0-rc.1" version, this issue is resolved using BLACKLIST_GRACE_PERIOD.

When multiple concurrent requests are made with the same JWT, it is possible that some of them fail, due token regeneration on every request. Set grace period in seconds to prevent parallel request failure.

But there is another problem, the token will be updated twice and your client will receive two new tokens, when only one is valid, but the client will not know which one is correct. This problem I am trying to solve at the moment.

philliperosario avatar Oct 21 '17 00:10 philliperosario

#1355

philliperosario avatar Oct 21 '17 23:10 philliperosario

Had some issues with the response coming back with "Token has been blacklisted". Seems like it's because the header wasn't set properly before it meet the next middleware.

Anyway changing: header('Authorization: Bearer ' . $refreshed);

to: $response->headers->set('Authorization', 'Bearer '.$refreshed);

fixed my problem. In case anyone else runs into the same issue.

Pederytter avatar Oct 31 '17 14:10 Pederytter

The actual application to refresh the token, there will be two problems: one is the current request requires the use of token to obtain user information, but this token has actually been added to the blacklist, at least in the controller which can not be used, so the need to transform middleware The first request under the current request header; The second problem is the problem of concurrency, the above friends have also proposed that I solve this problem with redis, the old token as a key, the new token as a value stored, and set an expired Time, to ensure that in the case of concurrency, will be added to the blacklist token can be used within 30 seconds

<?php

namespace App\Http\Middleware;

use Closure;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Illuminate\Support\Facades\Redis;

class GetUserFromToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $newToken = null;
        $auth = JWTAuth::parseToken();
        if (! $token = $auth->setRequest($request)->getToken()) {
            return response()->json([
                'code' => '2',
                'msg' => '无参数token',
                'data' => '',
            ]);
        }

        try {
            $user = $auth->authenticate($token);
            if (! $user) {
                return response()->json([
                    'code' => '2',
                    'msg' => '未查询到该用户信息',
                    'data' => '',
                 ]);
            }
            $request->headers->set('Authorization','Bearer '.$token);
        } catch (TokenExpiredException $e) {
            try {
                $newToken = JWTAuth::refresh($token);
                $request->headers->set('Authorization','Bearer '.$newToken); // 给当前的请求设置性的token,以备在本次请求中需要调用用户信息
                // 将旧token存储在redis中,30秒内再次请求是有效的
                Redis::setex('token_blacklist:'.$token,30,$newToken);
            } catch (JWTException $e) {
                // 在黑名单的有效期,放行
                if($newToken = Redis::get('token_blacklist:'.$token)){
                    $request->headers->set('Authorization','Bearer '.$newToken); // 给当前的请求设置性的token,以备在本次请求中需要调用用户信息
                    return $next($request);
                }
                // 过期用户
                return response()->json([
                    'code' => '2',
                    'msg' => '账号信息过期了,请重新登录',
                ]);
            }
        } catch (JWTException $e) {
            return response()->json([
                'code' => '2',
                'msg' => '无效token',
                'data' => '',
             ]);
        }
        $response = $next($request);

        if ($newToken) {
            $response->headers->set('Authorization', 'Bearer '.$newToken);
        }

        return $response;
    }
}

Tamn9208 avatar Feb 05 '18 10:02 Tamn9208

@cristianpontes, i've implemented your middleware solution, but i'm getting "The token has been blacklisted" in response to every request after token expiration time. None of the suggestions listed above doesn't solve the issue. Do you have any ideas how I can fix it?

tom2drum avatar Mar 10 '18 02:03 tom2drum

@tom-aglow I've been recently implementing JWT refresh functionality on my API and I think I might have a clue what it might be as I encountered the same problem myself.

  • Library version: 1.0.0-rc2
  • Laravel version: 5.5

My issue was that my Authentication ("auth") middleware was executing before the RefreshToken middleware when hitting a route like this (below) with a JWT which was expired, but still within its refresh period. Auth middleware would see the expired token and throw an exception which gets rendered and the RefreshToken middleware never gets a chance to execute.

The behaviour I wanted was that a request with an expired, but refreshable JWT, should pass and return a new JWT once before, subsequent requests with the original JWT will be allowed until the blacklist grace period is reached.

Route::get('jwt', function() {
    return response()->json(['instanceOf' => get_class(auth()->user())]);
})->middleware('auth:api', 'jwt.refresh');

I did two things to fix this.

  1. I modified my App\Http\Kernel class with a constructor to insert the RefreshToken middleware before the Authenticate middleware in the $middlewarePriority attribute to ensure that it will be executed first.
public function __construct(Application $app, Router $router)
{
    $this->reorderMiddlewarePriority();
    parent::__construct($app, $router);
}

/**
 * Reorders the parent classes $middlewarePriority attribute which decides which middleware
 * should be executed before others.
 *
 * In the case of a refresh of a JWT we need to ensure that the Refresh middleware acts before
 * the Authentication middleware.
 */
private function reorderMiddlewarePriority()
{
    $middlewarePriority = $this->middlewarePriority;

    // Ensure that RefreshToken middleware executes, before Authenticate middleware.
    $insert = \App\Http\Middleware\RefreshToken::class;
    // Find the index of the middleware to be inserted before
    $before = array_search(\Illuminate\Auth\Middleware\Authenticate::class, $middlewarePriority);
    // Insert the new middleware
    array_splice($middlewarePriority, $before, 0, [$insert]);

    $this->middlewarePriority = $middlewarePriority;
}
  1. I used the RefreshToken middleware supplied by @philliperosario at https://github.com/tymondesigns/jwt-auth/issues/1355, but modified a little with the following to ensure that subsequent middleware will use the refreshed token.
...
if(isset($newtoken)){
    // For any subsequent middleware, set the new refreshed token so the request will pass as authenticated and not expired.
    $request->headers->set('Authorization','Bearer '.$newtoken);
}
$response = $next($request); // Token refreshed and continue.
return $this->setAuthenticationHeader($response, $newtoken); // Response with new token on header Authorization.

Hope it helps someone!

HSkogmo avatar Apr 20 '18 09:04 HSkogmo

Hi @cristianpontes, I found your solution very logic but when I try to implement it in app\http\Middleware\Authenticate I get the following error Method 'parseToken' not found in \JWTAuth. JWTAuth is an alias for Tymon\JWTAuth\Facades\JWTAuth. AFAIK this Facade should bring me access to methods of the JWTAuth class, so what I´m doing wrong here?

rmachado33 avatar Jul 24 '18 14:07 rmachado33

Thanks @HSkogmo, that helped me a lot. One note I have is to rename the middleware to something other than jwt.refresh because that already exists in this jwt-auth library and appears to overwrite the new custom middleware even if you add 'jwt.refresh' => \App\Http\Middleware\RefreshToken::class to Kernel.php, so the token is renewed on every request as per the original middleware. Other than that it all works perfectly!

kamaln7 avatar Aug 02 '18 10:08 kamaln7

I used a jwt-auth middleware and it is called for all the routes in api.php.

<?php
namespace App\Http\Middleware;
use Closure;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class VerifyJWTToken
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $success_status = 200;
        try{
            $user = JWTAuth::toUser($request->header('Authorization'));
        }catch (JWTException $e) {
            if($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException || $e instanceof \Tymon\JWTAuth\Exceptions\TokenBlacklistedException) {
                try {
                    $new_token  = JWTAuth::refresh($request->header('Authorization'));
                    $status     = 200;
                    // set new token in to the request header
                    $request->headers->set('Authorization',$new_token);
                    $response          = $next($request);
                    $original          = $response->getOriginalContent();
                    $original['token'] = $new_token;
                    // set response - token as a common parameter
                    $response->setContent(json_encode($original));
                    return $response;
                } catch (TokenExpiredException $e) {
                    $status     = 401;
                    $message    = 'Please Login Again. Your Session Timed Out';
                    return response()->json(compact('status','message'),$success_status);
                }
                catch (JWTException $e) {
                    $status     = 401;
                    $message    = 'Please Login Again. Refresh token time expired';
                    return response()->json(compact('status','message'),$success_status);
                }          
            }else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) {
                $status     = 401;
                $message    = 'This token is invalid. Please Login';
                return response()->json(compact('status','message'),$success_status);
            }else{
                $status     = 404;
                $message    = 'Token is required';
                return response()->json(compact('status','message'),$success_status);
                //return response()->json(['error'=>'Token is required']);
            }
        }
       return $next($request);
    }
}

If the access token validity is timed out, then a refresh token is generated which us then set as header for the current request. As the api response i send the token back to front end with every api resposne. This need to be replaced with the old token which is present in the local storage. This need to be done in the interceptor(angular 6).

yadurvt avatar Mar 13 '19 04:03 yadurvt

can someone give me a hint? Where is the difference or advantages between the jwt.refresh middleware and a token TTL of infinity? I just don't understand.

jansgescheit avatar Aug 08 '20 16:08 jansgescheit

can someone give me a hint? Where is the difference or advantages between the jwt.refresh middleware and a token TTL of infinity? I just don't understand.

@Jannnnnn:

You cannot easily revoke an infinitely valid token without checking some kind of database on each request, ie. if the user has been deactivated. However doing so kinda defeats the purpose of a stateless token like JWT. Having a token with a shorter lifespan solves this as we can check if the user is still allowed when it comes time to dispense a new token for them. When the token is being refreshed you may also add or modify roles or scopes embedded in the token should you happen to use that for access control. A system like this isn't instant and is bound to the refresh cycle unless the user requests a new token prematurely.

Hope that helps.

HSkogmo avatar Aug 10 '20 00:08 HSkogmo

What happens if the refresh_ttl expires while user logged in before or after he makes the last JWTAuth::refresh(expired_access_token)?

Bobach22 avatar Nov 02 '20 18:11 Bobach22