Laravel-vue jwt token refresh - laravel

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");
})
})
}

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.

Related

Laravel JWT makes new request insted of sending status

I'm making an API in laravel with tymon/jwt-auth package. based on https://jwt-auth.readthedocs.io/en/develop/
Login works fine.
Sending GET request with right token works fine.
BUT when I send a GET request with incorrect/no token it redirects me to POST login (wants me to login) rather than send a 401
GET /api/reviews with no Auth:
The GET method is not supported for this route. Supported methods: POST.
api.php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::group([
'prefix' => 'auth',
], function ($router) {
Route::post('login', [AuthController::class, 'login'])->name('login');
});
Route::get('reviews',[ReviewController::class, 'index']);
AuthController.php
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(['email', 'password']);
if (!$token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return response()->json([
"user" => auth()->user(),
"access_token" => $token,
]);
}
/**
* Get the authenticated User.
*
* #return \Illuminate\Http\JsonResponse
*/
public function me()
{
return response()->json(auth()->user());
}
/**
* Log the user out (Invalidate the token).
*
* #return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* #return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken(auth()->refresh());
}
}
ReviewController.php
class ReviewController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
}
public function index()
{
$reviews = Review::all();
return response()->json([
'status' => 200,
'reviews' => $reviews
]);
}
}
Should I remove the auth middleware and do it manually?

Throttle middleware is not working for unauthenticated users when used with auth:sanctum middleware

I would like to protect my Laravel API from brute force attacks of unauthenticated users trying to guess the authentication token. I used the throttle middleware but I can't make it work.
First I tried this to allow 5 guest request per minute on the resource but it is not working:
Route::middleware(['auth:sanctum','throttle:5|60,1'])->group(function () {
Route::apiResource('company.vehicles', VehicleController::class);
});
Then I tried having the throttle middleware first, but it is also not working
Route::middleware(['throttle:5|60,1'])->group(function () {
Route::middleware(['auth:sanctum','throttle:5|60,1'])->group(function () {
Route::apiResource('company.vehicles', VehicleController::class);
});
});
What should I do to make it work?
Maybe using the RateLimiter in your Authenticate middleware is an option?
In app/Http/Middleware/Authenticate.php
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Facades\RateLimiter;
class Authenticate extends Middleware
{
protected function authenticate($request, array $guards)
{
// Request guarded by sanctum?
$guardedBySanctum = in_array('sanctum', $guards);
// If the request is guarded by sanctum
// and there were to many attempts (5)
// throw an exception
if ($guardedBySanctum && RateLimiter::tooManyAttempts('api', 5)) {
throw new AuthenticationException(
'Too many requests. Retry in ' . RateLimiter::availableIn('api') . ' seconds'
);
}
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
// User is unauthenticated
// If the request is guarded by sanctum...
if ($guardedBySanctum) {
// ...hit the rate limiter
RateLimiter::hit('api');
}
$this->unauthenticated($request, $guards);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}

Livewire session returns NULL in Laravel Auth Middleware

I have a livewire component that needs to send requests using JWT.
As the livewire GET request is not sending the token by default, I decided to modify the Laravel's Authentication middleware in order to send the token that I put in session in the livewire component, via bearer in Laravel Auth middleware.
That the variable that I added to the session in my livewire component, returns NULL when I try to retrieve it in the Authentication middleware.
1.Livewire component
<?php
namespace App\Http\Livewire;
class HelloWorld extends Component
{
public function mount(TransactionCheckoutRequest $request)
{
session(['x__livewire__token' => 'put token in session' ]);
}
}
2.Laravel auth middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Cookie;
class Authenticate extends Middleware
{
public function handle($request, Closure $next, ...$guards)
{
# Check if request is made using LIVEWIRE
if ( request()->hasHeader('x-livewire') ) {
/**
* get token from session
*
*
* But, this $token return NULL
* instead token value.
*/
$token = session()->get('x__livewire__token');
$request->headers->set('Accept', 'application/json');
$request->headers->set('Content-Type', 'application/json');
// # Send token to request header
$request->headers->set('Authorization', 'Bearer ' . $token);
}
# After that, authenticate livewire request
$this->authenticate($request, $guards);
return $next($request);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* #param \Illuminate\Http\Request $request
* #return string|null
*/
protected function redirectTo($request)
{
if ( !$request->expectsJson() ) {
return route('user.login');
}
}
}
3.Web route file
<?php
Route::group(['prefix' => 'v1'], function() {
Route::group(['middleware' => ['auth:api', 'jwt.verifyer', 'hostname.access.verifyer']], function() {
# Here is LIVEWIRE component
Route::get('/processing', HelloWorld::class)->name('transaction.checkout');
});
});
Can somebody explain how should I extend the middleware to use the token?
Livewire uses its own request when making requests. if you want to send additional data. You can send it using addHeaders.
<script type="text/javascript">
document.addEventListener("livewire:load", function(event) {
Livewire.addHeaders({
'Authorization': 'Bearer {{ $token }}',
})
});
</script>

How to test logout while using tymon/jwt-auth in Laravel?

I'm trying to make a logout test for my api with tymon/jwt-auth package. Here I have defined the api routes, controller, and a unit test.
In api.php:
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function ($router) {
Route::post('login', 'AuthController#login');
Route::post('logout', 'AuthController#logout');
Route::post('refresh', 'AuthController#refresh');
Route::post('me', 'AuthController#me');
Route::post('me/profile', 'AuthController#profile');
});
In AuthController.php:
/**
* Log the user out (Invalidate the token).
*
* #return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
In tests/Unit/AuthenticationTest.php:
/**
* Test if user can login trough internal api.
*
* #return void
*/
public function testLogin()
{
$response = $this->post('api/auth/login', [
'email' => 'admin#xscriptconnect.com',
'password' => 'password'
]);
$response->assertStatus(200)
->assertJsonStructure(['access_token', 'token_type', 'expires_in']);
$this->assertAuthenticated('api');
}
/**
* Test if user can logout trough internal api.
*
* #return void
*/
public function testLogout()
{
$user = User::first();
$user = $this->actingAs($user, 'api');
$user->post('api/auth/logout')
->assertStatus(200)
->assertJsonStructure(['message']);
$this->assertUnauthenticatedAs($user, 'api');
}
The login test works fine but when it starts the logout test, the assertion fails. It shows me this error:
There was 1 failure:
1) Tests\Unit\AuthenticationTest::testLogout
Expected status code 200 but received 500.
Failed asserting that false is true.
And when I tested it using this method:
public function testLogout()
{
$user = User::first();
$this->actingAs($user, 'api');
$response = auth()->logout();
$response->assertStatus(200);
$response->assertJsonStructure(['message']);
}
I got this error:
There was 1 error:
1) Tests\Unit\AuthenticationTest::testLogout
Tymon\JWTAuth\Exceptions\JWTException: Token could not be parsed from the request
What is the proper way to test a logout trough this package? Please help.
According to the this comment in it's github page, I have found the solution for this problem. I changed my script like this and it works.
/**
* Test if user can logout trough internal api.
*
* #return void
*/
public function testLogout()
{
$user = User::first();
$token = \JWTAuth::fromUser($user);
$this->post('api/auth/logout?token=' . $token)
->assertStatus(200)
->assertJsonStructure(['message']);
$this->assertGuest('api');
}
Please feel free to post another answer regarding to this question if any. Thank you very much.
Override the method be() in your TestCase to set the authorization header when use actingAs() aka be() method
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
abstract class TestCase extends BaseTestCase
{
public function be(UserContract $user, $driver = null)
{
$token = auth()->fromUser($user);
return parent::be($user, $driver)->withHeader('Authorization', "Bearer {$token}");
}
}

Make session expiration redirect back to login?

When user logs in and is authenticated, I use Auth::user()->username; to show username of user on dashboard. However, for some reason when session expires the class Auth doesn't seem to work and dashboard page throws error as trying to get property of non-object for Auth::user()->username;. How can I redirect the user back to the login page when he clicks any link or refreshes the page after the session has expired?
I tried the Authenticate.php middleware but it always redirects back to login page,whatever you put the credentials either correct or incorrect.However,when I don't use this middleware it logins the user.Am I missing something?
Route.php
<?php
/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the controller to call when that URI is requested.
|
*/
/*
Actions Handled By Resource Controller
Verb Path Action Route Name
GET /photo index photo.index
GET /photo/create create photo.create
POST /photo store photo.store
GET /photo/{photo} show photo.show
GET /photo/{photo}/edit edit photo.edit
PUT/PATCH /photo/{photo} update photo.update
DELETE /photo/{photo} destroy photo.destroy
Adding Additional Routes To Resource Controllers
If it becomes necessary to add additional routes to a resource controller beyond the default resource routes, you should define those routes before your call to Route::resource:
Route::get('photos/popular', 'PhotoController#method');
Route::resource('photos', 'PhotoController');
*/
// Display all SQL executed in Eloquent
// Event::listen('illuminate.query', function($query)
// {
// var_dump($query);
// });
define('ADMIN','admin');
define('SITE','site');
Route::group(['namespace' => ADMIN], function () {
Route::get('/','UserController#showLogin');
});
////////////////////////////////////Routes for backend///////////////////////////////////////////////////
Route::group(['prefix' => ADMIN,'middleware' => 'auth'], function () {
Route::group(['namespace' => ADMIN], function () {
//Route::get('/','EshopController#products');
//sumit routes for user registration
//Route::resource('users','UserController');
Route::get('/users/destroy/{id}','UserController#destroy');
Route::get('UserProf','UserController#userProf');
Route::get('users','UserController#index');
Route::get('/users/create','UserController#create');
Route::get('/users/adminEdit/{id}','UserController#adminEdit');
Route::post('/users/adminUpdate','UserController#adminUpdate');
Route::post('/users/store','UserController#store');
Route::get('/users/edit/{id}','UserController#edit');
Route::post('/users/update/{id}','UserController#update');
//airlines route
Route::get('airlines','AirlinesController#index');
Route::get('/airlines/create','AirlinesController#create');
Route::post('/airlines/store','AirlinesController#store');
Route::get('/airlines/edit/{id}','AirlinesController#edit');
Route::post('/airlines/update','AirlinesController#update');
Route::get('/airlines/destroy/{id}','AirlinesController#destroy');
//end sumit routes
//flight routes
Route::get('flights','FlightController#index');
Route::get('showFlightBook','FlightController#showFlightBook');
Route::get('flights/create','FlightController#create');
Route::post('flights/store','FlightController#store');
Route::get('flights/book','FlightController#book');
Route::get('flights/edit/{id}','FlightController#edit');
Route::post('flights/update','FlightController#update');
Route::get('flights/destroy/{id}','FlightController#destroy');
//Route::resource('flight','FlightController');
//hotels route
Route::get('hotels','HotelsController#index');
Route::get('/hotels/create','HotelsController#create');
Route::post('/hotels/store','HotelsController#store');
Route::get('/hotels/edit/{id}','HotelsController#edit');
Route::post('/hotels/update','HotelsController#update');
Route::get('/hotels/destroy/{id}','HotelsController#destroy');
//end sumit routes
//book-hotel routes
Route::get('hotel-book','HotelBookController#index');
Route::get('showHotelBook','HotelBookController#showHotelBook');
Route::get('hotel-book/create','HotelBookController#create');
Route::post('hotel-book/store','HotelBookController#store');
Route::get('hotel-book/book','HotelBookController#book');
Route::get('hotel-book/edit/{id}','HotelBookController#edit');
Route::post('hotel-book/update','HotelBookController#update');
Route::get('hotel-book/destroy/{id}','HotelBookController#destroy');
//Route::resource('hotel','HotelController');
//close flight routes
//for admin login
//Route::get('initlogin','UserController#lgnPage');
Route::get('login','UserController#showLogin');
// Route::get('privilegeLogin','UserController#privilegeLogin');
// Route::post('privilegeCheck','UserController#privilegeCheck');
Route::post('login','UserController#doLogin');
Route::get('/dashboard','DashController#index');
Route::get('logout','UserController#doLogout');
//user login
//Route::get('userLogin','UserController#showUserLogin');
//Route::post('userLogin','UserController#doUserLogin');
Route::get('/userDashboard','DashController#userIndex');
Route::get('Logout','UserController#doUserLogout');
//password reset
Route::get('forget-pass','UserController#showReset');
//Route::get('home', 'PassResetEmailController#index');
});
});
Route::controllers([
'auth' => 'Auth\AuthController',
'password' => 'Auth\PasswordController',
]);
Authenticate.php:
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate {
/**
* The Guard implementation.
*
* #var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* #param Guard $auth
* #return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest())
{
if ($request->ajax())
{
return response('Unauthorized.', 401);
}
else
{
// return redirect()->guest('auth/login');
return redirect()->guest('/');
}
}
return $next($request);
}
}
All you have to do is just put this constructor at the top of the controller for your dashboard. It seems Laravel has a middleware that handles this already. At least I can confirm from 5.4 and up.
public function __construct()
{
$this->middleware('auth');
}
If the session expires then you can redirect to log in like as
open this file app/Exceptions/Handler.php add this code
public function render($request, Exception $exception)
{
if ($exception instanceof \Illuminate\Session\TokenMismatchException) {
return redirect('/login');
}
return parent::render($request, $exception);
}
If you want a middleware to be run during every HTTP request to your application, simply list the middleware class in the $middleware property of your app/Http/Kernel.php class.
So, to protect every route from being accessed without authentication do this
protected $middleware = [
'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
'Illuminate\Cookie\Middleware\EncryptCookies',
'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
'Illuminate\Session\Middleware\StartSession',
'Illuminate\View\Middleware\ShareErrorsFromSession',
'App\Http\Middleware\VerifyCsrfToken',
'App\Http\Middleware\Authenticate',// add this line according to your namespace
];
it will redirect the user if not logged in. UPDATE Keep in mind that adding auth middleware as global will create redirect loop so avoid it.
Or if you want specific routes to be protected then attach the middleware auth to that route
Route::get('admin/profile', ['middleware' => 'auth', function () {
//
}]);
I think you are not attaching the auth middleware to your routes.
Create a middleware like this
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class Authenticate
{
/**
* The Guard implementation.
*
* #var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* #param Guard $auth
* #return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('login');
}
}
return $next($request);
}
}
Then Group the routes and protect them like this
Route::group(['middleware' => 'auth'], function()
{
Route::get();
Route::get();
Route::get();
Route::get();
}
Offcourse, in the routes you have to specify your links etc, it will only allow the user when he is authenticated and if not then login page will be shown
To make session redirect to your login just add ->middleware('auth') in your router files as shown below I am using laravel 5.3
Ex:
Route::post('controllerName','folderName\fileName#fnNmae')->middleware('auth');
Or visit https://laravel.com/docs/5.3/authentication

Resources