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

Laravel 5.5 + Vue.js 2 + JWT Auth 1.0.0-rc.1

Open philliperosario opened this issue 8 years ago • 70 comments

Since I lost tons of time doing tymon/jwt-auth work in my application, I decided to share my code in this walkthrough.

FOR LARAVEL:

Add "tymon/jwt-auth": "1.0.0-rc.1" to composer.json and run composer update

Add the service provider to the providers array in config\app.php: Tymon\JWTAuth\Providers\LaravelServiceProvider::class

Add the facades to the aliases array also in config/app.php:

'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class

Run: php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

Run: php artisan jwt:secret

If you are using CORS expose the response header "Authorization" to give JS allow getting header. Already if you are using Laravel CORS, then in config/cors.php specify expose the header:

...
'exposedHeaders' => ['Authorization'],
...

The 1.0.0-rc.1 version requires you to implement Tymon\JWTAuth\Contracts\JWTSubject on your user model too. You must then add the required methods, which are getJWTIdentifier() and getJWTCustomClaims() to app\User.php:

<?php

namespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject {
    
    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier() {
        return $this->getKey(); // Eloquent Model method
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims() {
        return [];
    }
}
?>

App\Http\Controllers\AuthController.php:

<?php

namespace App\Http\Controllers;

use App\Route;
use App\Legislature;
use App\Http\Controllers\Controller;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;

class AuthController extends Controller {

    public function authenticate(\Illuminate\Http\Request $request) { 
        $credentials = $request->only('email', 'password'); // grab credentials from the request
        try {
            if (!$token = JWTAuth::attempt($credentials)) { // attempt to verify the credentials and create a token for the user
                return response()->json(['error' => 'invalid_credentials'], 401);
            }
        } catch (JWTException $e) {
            return response()->json(['error' => 'could_not_create_token'], 500); // something went wrong whilst attempting to encode the token
        }

        return response()->json(['token' => "Bearer $token"]);
    }
}
?>

Add to .env file:

JWT_SECRET=[replace with your key]
JWT_TTL=60
JWT_REFRESH_TTL=21600
JWT_BLACKLIST_GRACE_PERIOD=30

Run: php artisan config:cache

I created my own middleware, which works like this:

  • The client sends the credentials (email and password) to Laravel and receives a token (JWT) in response. This token is valid for JWT_TTL minutes. During this time all requests with the header Authorization = "Bearer token" will be successful.
  • For a request made after JWT_TTL minutes, that is, with the token expired, two situations will occur: 1 - If there is less than JWT_REFRESH_TTL minutes since the creation of the token (the token carries within it the date of creation on claim IAT), then this token will be invalidated (blacklist) and a new token will be generated and sent as a response to the client. JWT_REFRESH_TTL defines how many minutes after creating the first token the new tokens can be created. For example, for JWT_REFRESH_TTL = 21600, new tokens will be generated for 15 days, after which time the user should reauthenticate. 2 - The request occurs after JWT_REFRESH_TTL minutes after the first token was created. In this case, it will not be possible to generate a new token for the client and it must authenticate again. A 401 error will be sent to the client.
  • When multiple concurrent requests are made with the same JWT, it is possible that some of them fail, due to token regeneration on every request. Set grace period in seconds to prevent parallel request failure, because the JWT will consider to be valid for JWT_BLACKLIST_GRACE_PERIOD seconds, even if it's on the blacklist.
  • VERY WORTH HERE. When multiple concurrent requests are made with the same EXPIRED JWT, the default tymon/jwt-auth middleware will response to each request with a different new token. But there is a serious mistake in that. Only one of the tokens sent to the client will be valid and the others will be on the blacklist. Responses do not reach the client necessarily in the order they were sent by the API, so the client can save an invalid token and use it to submit new requests. My middleware solves this by checking if in the last JWT_BLACKLIST_GRACE_PERIOD seconds some new token has been generated for the client and in case it prevents a new token from being sent to the client.

Create the file App\Http\Middleware\RefreshToken:

<?php

namespace App\Http\Middleware;

use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;

class RefreshToken extends BaseMiddleware {

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, \Closure $next) {

        $this->checkForToken($request); // Check presence of a token.

        try {
            if (!$this->auth->parseToken()->authenticate()) { // Check user not found. Check token has expired.
                throw new UnauthorizedHttpException('jwt-auth', 'User not found');
            }
            $payload = $this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray();
            return $next($request); // Token is valid. User logged. Response without any token.
        } catch (TokenExpiredException $t) { // Token expired. User not logged.
            $payload = $this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray();
            $key = 'block_refresh_token_for_user_' . $payload['sub'];
            $cachedBefore = (int) Cache::has($key);
            if ($cachedBefore) { // If a token alredy was refreshed and sent to the client in the last JWT_BLACKLIST_GRACE_PERIOD seconds.
                \Auth::onceUsingId($payload['sub']); // Log the user using id.
                return $next($request); // Token expired. Response without any token because in grace period.
            }
            try {
                $newtoken = $this->auth->refresh(); // Get new token.
                $gracePeriod = $this->auth->manager()->getBlacklist()->getGracePeriod();
                $expiresAt = Carbon::now()->addSeconds($gracePeriod);
                Cache::put($key, $newtoken, $expiresAt);
            } catch (JWTException $e) {
                throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), $e, $e->getCode());
            }
        }

        $response = $next($request); // Token refreshed and continue.

        return $this->setAuthenticationHeader($response, $newtoken); // Response with new token on header Authorization.
    }

}

Add to routeMiddleware array in App\Http\Kernel.php: 'jwt' => \App\Http\Middleware\RefreshToken::class

routes\api.php:

<?php
// Auth
Route::post('auth/signin', 'AuthController@authenticate');
Route::group(['middleware' => 'jwt'], function () {
   // Protected routes
  Route::resource('index', 'IndexController');
});

FOR VUE.JS:

Authenticate method:

  methods: {
    authenticate () {
      if (!this.isValid) return false
      Loading.show()
      axios.create(def).post('api/auth/signin', { email: this.user.email, password: this.user.password }).then((response) => {
        const arr = []
        arr.push(this.$store.dispatch('setToken', response.data.token))
        Promise.all(arr).then(() => {
          router.push('/')
          Loading.hide()
        })
      }, (error) => this.traitError(error))
    },
    traitError (ops) {
      Loading.hide()
      if (!ops.response) return
      let reason = ''
      switch (ops.response.status) {
        case 401:
          reason = 'Invalid credentials.'
          break
        default:
          reason = ops.response.data.message
      }
      Toast.create.negative(reason)
    }
  }

Axios interceptor for watch and save new tokens:

import { defaults, get } from 'lodash'
import axios from 'axios'
import store from 'vuex-store'
import def from './default'

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

  instance.interceptors.response.use(function (response) {
    const newtoken = get(response, 'headers.authorization')
    if (newtoken) store.dispatch('setToken', newtoken)
    console.log(response.data)
    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
}

I think that's it. Good luck. And I hope I have helped.

philliperosario avatar Oct 21 '17 23:10 philliperosario

When i set in composer the "tymon/jwt-auth": "1.0.0-rc.1" and go to the "composer update" i have this error:

Problem 1 - The requested package tymon/jwt-auth 1.0.0-rc.1 exists as tymon/jwt-auth[0.1.0, 0.2.0, 0.3.0, 0.3.1, 0.3.10, 0.3.11, 0.3.12, 0.3.2, 0.3.3, 0.3.4, 0.3.5, 0.3.6, 0.3.7, 0.3.8, 0.3.9, 0.4.0, 0.4.1, 0.4.2, 0.4.3, 0.5.0, 0.5.1, 0.5.2, 0.5.3, 0.5.4, 0.5.5, 0.5.6, 0.5.7, 0.5.8, 0.5.9, 1.0.0-alpha.2, 1.0.0-alpha1, dev-develop, 1.0.x-dev, dev-master, dev-refactor] but these are rejected by your constraint.

I have a fresh laravel 5.5.18 version installed, witch jwt-auth version must i choose?

ralbear avatar Oct 23 '17 09:10 ralbear

I guess dev-develop

Kyslik avatar Oct 23 '17 11:10 Kyslik

Yep i try dev-develop too and i have a big issue too

Problem 1

  • Installation request for laravel/framework 5.5.* -> satisfiable by laravel/framework[v5.5.18].
  • tymon/jwt-auth dev-develop requires illuminate/support 5.1.* || 5.2.* || 5.3.* -> satisfiable by illuminate/support[v5.1.1, v5.1.13, v5.1.16, v5.1.2, v5.1.20, v5.1.22, v5.1.25, v5.1.28, v5.1.30, v5.1.31, v5.1.41, v5.1.6, v5.1.8, v5.2.0, v5.2.19, v5.2.21, v5.2.24, v5.2.25, v5.2.26, v5.2.27, v5.2.28, v5.2.31, v5.2.32, v5.2.37, v5.2.43, v5.2.45, v5.2.6, v5.2.7, v5.3.0, v5.3.4].
    • don't install illuminate/support v5.1.1|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.13|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.16|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.2|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.20|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.22|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.25|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.28|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.30|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.31|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.41|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.6|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.1.8|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.0|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.19|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.21|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.24|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.25|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.26|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.27|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.28|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.31|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.32|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.37|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.43|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.45|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.6|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.2.7|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.3.0|remove laravel/framework v5.5.18
    • don't install illuminate/support v5.3.4|remove laravel/framework v5.5.18
    • Installation request for tymon/jwt-auth dev-develop -> satisfiable by tymon/jwt-auth[dev-develop].

This is the first part of my composer.json file

{
    "name": "laravel/laravel",
    "description": "The Laravel Framework.",
    "keywords": ["framework", "laravel"],
    "license": "MIT",
    "type": "project",
    "require": {
        "php": ">=7.0.0",
        "fideloper/proxy": "~3.3",
        "laravel/framework": "5.5.*",
        "laravel/tinker": "~1.0",
        "tymon/jwt-auth": "dev-develop"
    },

ralbear avatar Oct 23 '17 14:10 ralbear

@philliperosario how can i implement the same for a non User class. my issue is the the mobile API should be authenticated against the \App\Customer class (not user class). I'm using Laravel 5.4.

Rafeethu avatar Oct 24 '17 09:10 Rafeethu

@Rafeethu see in config/auth.php the providers['users'] array.

philliperosario avatar Oct 24 '17 12:10 philliperosario

@philliperosario my app has web module for internal users and api for mobile users (customers). So in config/app.php the guards array has paras for web and api separately (laravel 5.4 has this)

I need web section to use \App\User model and api section to use \App\Customer model. So I set the providers for api accordingly. But when calling through api route, it still use the web guard section (that's the user model).

What am I missing here.

Rafeethu avatar Oct 24 '17 13:10 Rafeethu

@Rafeethu put here your config/auth.php file

philliperosario avatar Oct 24 '17 16:10 philliperosario

This is my config/auth.php file `

return [

/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/

'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'token',
        'provider' => 'customers',
    ],
],

/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],
    'customers' => [
        'driver' => 'eloquent',
        'model' => App\Customer::class,
    ],

    // 'users' => [
    //     'driver' => 'database',
    //     'table' => 'users',
    // ],
],

/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/

'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_resets',
        'expire' => 60,
    ],
],

];

`

Rafeethu avatar Oct 25 '17 01:10 Rafeethu

@Rafeethu

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt',
        'provider' => 'customers',
    ],
],

and as said in this post:

You can choose which guard you're using to protect your routes by adding a colon and the guard name after auth in the middleware key (e.g. Route::get('whatever', ['middleware' => 'auth:api'])). You can choose which guard you're calling manually in your code by making guard('guardname') the first call of a fluent chain every time you use the Auth façade (e.g. Auth::guard('api')->check()).

philliperosario avatar Oct 25 '17 02:10 philliperosario

Yes, i tried $token = JWTAuth::guard('api')->attempt($credentials)

in login controller but it throws

(1/1) BadMethodCallExceptionMethod [guard] does not exist.

in JWT.php (line 399) at JWT->__call('guard', array('api'))in Facade.php (line 221) at JWTAuth->guard('api')in Facade.php (line 221) at Facade::__callStatic('guard', array('api'))in AuthController.php (line 38) at JWTAuth::guard('api')in AuthController.php (line 38) at AuthController->login(object(Request))

Rafeethu avatar Oct 25 '17 02:10 Rafeethu

use the guard on your routes

philliperosario avatar Oct 25 '17 02:10 philliperosario

You mean like this Route::middleware('jwt:api')->get('/user', function (Request $request) { return ['name' => 'test']; });

still no luck :-(

Rafeethu avatar Oct 25 '17 03:10 Rafeethu

@akkhan20 Because the master version is not compatible with laravel 5.5 at the moment, and the dev one looks like they have the laravel 5.5 compatibility issues solved

ralbear avatar Oct 26 '17 17:10 ralbear

@Rafeethu I think the guard must be called like this

Route::get('/user', function (Request $request) ['middleware' => 'auth:api']);

ralbear avatar Oct 26 '17 17:10 ralbear

@ralbear you can download 1.0.0-rc.1 and u can certainly find some docs for that and its compatible with L5.5 too

jampack avatar Oct 26 '17 18:10 jampack

@akkhan20 If you read my first comment, when i try with the 1.0.0-rc.1 version in composer i get an error, composer says that version is not available.

ralbear avatar Oct 26 '17 18:10 ralbear

php artisan vendor: publish --provider = "Tymon\JWTAuth\Providers\LaravelServiceProvider" should be php artisan vendor: publish --provider "Tymon\JWTAuth\Providers\LaravelServiceProvider" no =

also: remove spaces vendor:publish - php artisan jwt:secret

janva255 avatar Nov 11 '17 00:11 janva255

If you are using CORS - do not forget to expose response header "Authorization" to give JS allow getting header. If you are using Laravel CORS, then In config/cors.php specify expose header:

...
'exposedHeaders' => ['Authorization'],
...

core01 avatar Nov 12 '17 17:11 core01

@core01 well remembered, I updated my answer

philliperosario avatar Nov 13 '17 12:11 philliperosario

@philliperosario , thanks for tutorial. however, what about the signing up?

zhekaus avatar Nov 29 '17 07:11 zhekaus

@zhekaus sorry, I did not implement the sign up

philliperosario avatar Nov 29 '17 07:11 philliperosario

@zhekaus try this:

    public function register(Request $request)
    {
        $request->validate(
            [
                'email' => 'required|string|email|max:255|unique:users',
                'password' => 'required|string|min:6|confirmed',
                'password_confirmation' => 'required|string|min:6',
            ]
        );
        $user = new User();
        $user->email = $request->email;
        $user->password = bcrypt($request->password);
        $user->save();
        $token = JWTAuth::attempt($request->only('email', 'password'));
        return response()->json(['token' => "Bearer $token"]);
    }

core01 avatar Nov 29 '17 08:11 core01

@core01 , many thanks indeed! However, I've got this:

Type error: Argument 1 passed to Tymon\JWTAuth\JWT::fromUser() must implement interface Tymon\JWTAuth\Contracts\JWTSubject

at the line $token = JWTAuth::attempt($request->only('email', 'password'));

However, I did implemented it as described above.

zhekaus avatar Nov 29 '17 11:11 zhekaus

@zhekaus please check if you are using the right facade inside AuthController it should be use Tymon\JWTAuth\Facades\JWTAuth;

core01 avatar Nov 29 '17 11:11 core01

yes, I am

zhekaus avatar Nov 29 '17 11:11 zhekaus

Your User model must implement Tymon\JWTAuth\Contracts\JWTSubject interface

Frondor avatar Nov 29 '17 12:11 Frondor

@Frondor , As I said before, I did it. There is no problem with a fresh Laravel project. I just can't make it work with the real one.

zhekaus avatar Nov 30 '17 05:11 zhekaus

Finally I’ve solved my problem. Tracing led to wrong config. I had user provider driver set to 'database' before I cached configuration following the tutorial.

I changed it to 'eloquent' according to the docs, but hadn’t run php artisan config:clear since that. Caching step is definitely superfluous for this tutorial. :-)

Also you don’t need aliases for JWT’s facades.

zhekaus avatar Nov 30 '17 20:11 zhekaus

I have a question. I have several public pages. Those pages are in (laravel) a different group and I don't use the refresh middleware. That means that if the user doesn't call the routes that does have the refresh middleware, the used token will be the same... The token will change only if the user request for those "middlewared" routes... so.. if the token is expired, will be refreshed. If the user doesn't request for those routes the token could be refreshed until the JWT_REFRESH_TTL pass? For instance...

.env
JWT_TTL=1
JWT_REFRESH_TTL=20160
JWT_BLACKLIST_ENABLED=true
JWT_BLACKLIST_GRACE_PERIOD=180
  • The user login, return TokenA
  • Call /user/me (jwt middleware). Sends TokenA. Middleware Invalid TokenA, return TokenB
  • Call /posts (NOT JWT MIDDLEWARE). Sends TokenB.
  • Call /categories (NOT JWT MIDDLEWARE). Sends TokenB.
  • Pass 1 day. Call /posts (NOT JWT MIDDLEWARE). Sends TokenB.
  • Call /user/me (jwt middleware). Sends TokenB. Middleware Invalid TokenB, return TokenC.
  • Call /posts (NOT JWT MIDDLEWARE). Sends TokenC.
  • Pass 2 weeks
  • Call /user/me (jwt middleware). Sends TokenC. Return 401 because TokenC couldn't refresh (JWT_REFRESH_TTL).
  • User Login, return TokenX

This is correct? There is a better approach to this? refresh from the frontend every several time? or something like that?

pbvarsok avatar Dec 05 '17 17:12 pbvarsok

I'm following this setup for the middleware yet i always get blacklisted token.... i don't know why. I have already return the response back with the new token and give an interceptor if there is any new token.

It is always caught in exception when arrived at this line $this->auth->refresh(); Blacklisted Token.

jass-trix avatar Dec 14 '17 13:12 jass-trix