Open punnawat opened 7 years ago
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
If I don't refresh a token every time, how to check the expire token?
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 ;)
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 Thanks for solution!!It should work.
I have one query regarding above code .
What is "ApiHelpers::ApiResponse(101, null) ;" in above code ?
@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 thanks for reply. This solve my query. Great work friend!!!
@cristianpontes Thanks so much.
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.
@cristianpontes I'm looking for this. Thanks.
Testing my API in Postman, the URL return {"code":103,"response":null}
after token expires.
@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.
I want to save the new jwt token in redux store how can I do it
@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,`
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
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 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 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 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 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.
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.
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;
}
}
@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?
@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.
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.
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);
}
/**
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; }
2. 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.
```php
...
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!
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?
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!
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).
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.
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.
What happens if the refresh_ttl expires while user logged in before or after he makes the last JWTAuth::refresh(expired_access_token)?
Can someone explain, why this library doesn't provide refresh token?
I found a solution implemented by myself
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?