1
votes

I am using jwtAuth package from Tymon to handle Auth from my laravel backend to vue spa front end, I am creating AuthController that pretty much I take from the documentation and just tweak a little of it to meet my needs. And everything works fine from login to logout and when token expires.

The question is I do see there is a token refresh function on that controller that if my guess is right, it is to refresh the current token that the client already has. But how to do that? how do I handle that refresh token on my front end? Since it is quite annoying that every 60 minutes (by default the token lifetime) then it will throw 401.

What I want is maybe every time the user doing request to backend then it will refresh the token or increase the lifetime of the token. So the token will only expire if the user idle for the entire 60 minutes.

Can we do that? and is it the best practice? I am quite new on the entire jwt and token thing, in the past, I only rely on laravel token to expire since I am not working with spa, but a blade front-end so mostly don't need to mess around with the way laravel authenticate user.

For the added information here is every file that i think have anything to do with the entire auth thing.

Here is my authcontroller

<?php

namespace App\Http\Controllers;

use DB;
use Hash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Input;
use App\Http\Controllers\Controller;
use App\User;
use Response;

class Authcontroller extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['username', 'password']);

        if (! $token = auth('api')->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth('api')->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth('api')->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        $id = auth('api')->user()->getId();
        $kelas = User::with('pus','cu')->findOrFail($id);

        return response()->json([
            'access_token' => $token,
            'user' => $kelas,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }

    public function guard()
    {
        return Auth::Guard('api');
    }

}

Here is my API route

Route::group(['prefix' => 'auth'],function($router){
    Route::post('/login', 'AuthController@login');
    Route::post('/logout', 'AuthController@logout');
    Route::post('/refresh', 'AuthController@refresh');
    Route::get('/me', 'AuthController@me');
});

And here is on my vue general.js file that handles route and also give the header to Axios

export function initialize(store, router) {
  router.beforeEach((to, from, next) => {
      const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
      const currentUser = store.state.auth.currentUser;

      if(requiresAuth && !currentUser) {
          next('/login');
      } else if(to.path == '/login' && currentUser) {
          next('/');
      } else {
          next();
      }
  });

  axios.interceptors.response.use(null, (error) => {
      if (error.response.status == 401) {
          store.dispatch('auth/logout');
          router.push('/login');
      }

      return Promise.reject(error);
  });

  if (store.state.auth.currentUser) {
      setAuthorization(store.state.auth.currentUser.token);
  }
}

export function setAuthorization(token) {
  axios.defaults.headers.common["Authorization"] = `Bearer ${token}`
}

And here is auth.js that handle login

import { setAuthorization } from "./general";

export function login(credentials){
  return new Promise((res,rej) => {
    axios.post('/api/auth/login', credentials)
      .then((response) => {
        setAuthorization(response.data.access_token);
        res(response.data);
      })
      .catch((err) => {
        rej("Username atau password salah");
      })
  })
}
1

1 Answers

1
votes

You could tweak the token expiry (from .env as JWT_TTL) and refresh times (JWT_REFRESH_TTL) to suit your needs. And check if the token is valid and/or needs to be refreshed in the middleware so the token is refreshed soon as it needs to be.

As to whether this is a good practice, see the comments to JWT_REFRESH_TTL in your Laravel project's config/jwt.php.

The solution that has worked well for me was to use a custom Middleware that extends Tymon\JWTAuth\Http\Middleware\BaseMiddleware. The boilerplate would look something like this:

Class TryTokenRefresh extends BaseMiddleware
{
    public function handle($request, Closure $next)
    {
        $newToken = $this->tryRefresh($request);
        if ($newToken) {
           // in case there's anything further to be done with the token
           // we want that code to have a valid one
           $request->headers->set('Authorization', 'Bearer ' . $newToken);
        }

        ...
        ...
        $response = $next($request);
        ...
        ...

        if ($newToken) {
            // send new token back to frontend
            $response->headers->set('Authorization', $newToken);
        }

        return $response;
    }

    // Refresh the token
    protected function tryRefresh()
    {
        try {
            $token = $this->auth->parseToken()->refresh();
            return $token;
        } catch (JWTException $e) {
            // token expired? force logout on frontend
            throw new AuthenticationException();
        }

        return null;
    }

On the frontend, it's as simple as look for a Authorization header in the response:

// check for the `Authorization` header in each response - refresh on frontend if found
axios.interceptors.response.use((response) => {
  let headers = response.headers

  // your 401 check here
  // token refresh - update client session
  if (headers.authorization !== undefined) {
   setAuthorization(headers.authorization);
  }

  return response
})

Hope this helps.