Kamil Kiełczewski
Kamil Kiełczewski

Reputation: 92347

How to make JWT cookie authentication in Laravel

I want to have JWT authentication in Laravel >=5.2, using this (Tymon JWT-auth) library but I want to put JWT token into HttpOnly Cookies - to protect JWT token from steal from XSS attack.

  1. I set up Tymon library and... in project: app/Providers/RouteServiceProvider@mapWebRoutes i deactivate execution 'web' middelware group for all requests (which is default laravel behavior - you can see it by php artisan route:list) by remove 'middleware' => 'web' (If I don't do it, i will see CSRF problem with post request).
  2. in routes.php i write:
Route::group(['middleware' =>'api', 'prefix' => '/api/v1', 'namespace' => 'Api\V1'], function () {
    Route::post('/login', 'Auth\AuthController@postLogin');
    ...
    Route::get('/projects', 'ProjectsController@getProjects');
}
  1. In may Api\V1\Auth\AuthController@postLogin i generate token and send it back as httpOnly cookie:

    ...
    try
    {
        $user = User::where('email','=',$credentials['email'])->first();
    
        if ( !($user && Hash::check($credentials['password'], $user->password) ))
        {
            return response()->json(['error' => 'invalid_credentials'], 401);
        }
    
        $customClaims = ['sub' => $user->id, 'role'=> $user->role, 'csrf-token' => str_random(32) ];
        $payload = JWTFactory::make($customClaims);
        $token = JWTAuth::encode($payload);
    } catch(...) {...}
    return response()->json($payload->toArray())->withCookie('token', $token, config('jwt.ttl'), "/", null, false, true); 
    
  2. And, yeah here question starts. I would like to do something (may be modifiy laravel Auth class) on each request:

    • get coookie from request
    • decode it
    • check is right (if not trhow 401)
    • get user from DB
    • and make that method Auth::user() works every where like in usual way in laravel (so i can use it in each Controller for example)

Any ideas how to do point 4 ?

UPDATE

I also add here protection for CSRF attack - csrf-token is in JWT, and it is also return in body of response for login request (so JS have acces to this csrf-token) (i return only public part of JWT token in login response, whole JWT is return only in cookie, so it is XSS safe) - then front JS must copy csrf-token into header of each request. Then the middelware JWTAuthentiacate (in my answer below) compare csrf-token header with csrf-token field in JWT payload - if they are similar then request pass csrf test.

Upvotes: 7

Views: 13897

Answers (3)

hogan
hogan

Reputation: 1551

Actually you can put every route that needs authentication within a route group and add the middleware like this:

Route::group(['middleware' => ['jwt.auth']], function () {
    Route::patch('/profile', 'UserController@update');
});

The middleware already does what you wanted so there is no need to write additional logic. Don't use an additional handle method.

Within your i.e. UserController you can then i.e.

$user = \Auth::user();

And i.e. depending what you need...

// assign fields
$user->save();

return 'success'; // or whatever you need

Don't reinvent the wheel and keep things DRY.

Upvotes: -1

Kamil Kiełczewski
Kamil Kiełczewski

Reputation: 92347

I implement @ŁukaszKuczmaja idea in this way an it works! :) . So i create file in app/Http/Middleware/JWTAuthenticate.php :

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;
use JWTAuth;
use Tymon\JWTAuth\Token;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Illuminate\Session\TokenMismatchException;

class JWTAuthenticate
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        try {
            if(!$request->headers->has('csrf-token')) throw new TokenMismatchException();
            $rawToken = $request->cookie('token');
            $token = new Token($rawToken);
            $payload = JWTAuth::decode($token);
            if($payload['csrf-token'] != $request->headers->get('csrf-token')) throw new TokenMismatchException();
            Auth::loginUsingId($payload['sub']);
        } catch(\Exception $e) {
            if( $e instanceof TokenExpiredException) {
                // TODO token refresh here
            }
            return response('Unauthorized.', 401);
        }

        return $next($request);
    }
}

In app\Http\Kernel.php@$routeMiddelware I add line:

'jwt.auth'    => \App\Http\Middleware\JWTAuthenticate::class,

My routing file looks like this now:

Route::group(['middleware' =>'api', 'prefix' => '/api/v1', 'namespace' => 'Api\V1'], function () {

    Route::post('/login', 'Auth\AuthController@postLogin');

    Route::group(['middleware' =>'jwt.auth'], function () {
        Route::post('/projects', 'ProjectsController@postProjects');
        Route::get('/projects', 'ProjectsController@getProjects');
        Route::put('/projects/{project}', 'ProjectsController@putProjects');
        Route::delete('/projects/{project}', 'ProjectsController@deleteProjects');
    });

});

And for instance in app/Http/Controllers/Api/V1/ProjectsController.php i have:

public function getProjects() {
    $uid = Auth::user()->id;
    return Project::where('user_id','=',$uid)->get();
}

Upvotes: 2

Łukasz Kuczmaja
Łukasz Kuczmaja

Reputation: 1227

You can do it simple by creating middleware.

In handle() method just get cookie from request, decode it and login a user using id with this Laravel method:

Auth::loginUsingId($userIdFromToken);

Upvotes: 3

Related Questions