tymondesigns / jwt-auth

🔐 JSON Web Token Authentication for Laravel & Lumen
https://jwt-auth.com
MIT License
11.3k stars 1.54k forks source link

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

Open philliperosario opened 7 years ago

philliperosario commented 7 years ago

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:

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.

ralbear commented 7 years ago

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

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

Kyslik commented 7 years ago

I guess dev-develop

ralbear commented 7 years ago

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

Problem 1

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"
    },
Rafeethu commented 7 years ago

@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.

philliperosario commented 7 years ago

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

Rafeethu commented 7 years ago

@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.

philliperosario commented 7 years ago

@Rafeethu put here your config/auth.php file

Rafeethu commented 7 years ago

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,
    ],
],

];

`

philliperosario commented 7 years ago

@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()).

Rafeethu commented 7 years ago

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))

philliperosario commented 7 years ago

use the guard on your routes

Rafeethu commented 7 years ago

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

still no luck :-(

ralbear commented 7 years ago

@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 commented 7 years ago

@Rafeethu I think the guard must be called like this

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

@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

ralbear commented 7 years ago

@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.

janva255 commented 6 years ago

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

core01 commented 6 years ago

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'],
...
philliperosario commented 6 years ago

@core01 well remembered, I updated my answer

zhekaus commented 6 years ago

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

philliperosario commented 6 years ago

@zhekaus sorry, I did not implement the sign up

core01 commented 6 years ago

@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"]);
    }
zhekaus commented 6 years ago

@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.

core01 commented 6 years ago

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

zhekaus commented 6 years ago

yes, I am

Frondor commented 6 years ago

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

zhekaus commented 6 years ago

@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 commented 6 years ago

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.

pbvarsok commented 6 years ago

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

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

jass-trix commented 6 years ago

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.

rafaberaldo commented 6 years ago

@philliperosario maybe you can clear my mind on something, I also have a setup very much like yours (with vue).

I need to keep track of my user's active tokens (in case I have to blacklist them), so I have a database table having every user ID and their active tokens, every time the user access a protected route I have to update my database to edit the token? Is there a better way to do it (even not being JWT related)? I'm kinda in the dark for this.

philliperosario commented 6 years ago

@rafaelpimpa you can create a table on database with "user_id" and "logged_until". When a new token is generated for user you UPDATE_OR_CREATE a registry with user id and the timestamp of expiry date. Users actives = logged_until > now.

rafaberaldo commented 6 years ago

@philliperosario makes sense, but you think should be better if I set the token's ttl to a higher number and do not refresh them, or just use like your setup and update database for every request? I do have some sensitive data, not sure if the first option is secure enough.

Thanks for your insight :)

Frondor commented 6 years ago

The idea behind JWTokens relies on its payload. If you need to query a DB to validate/invalidate a token, then you probably have to reconsider your authentication mechanism. Short-lived tokens and an in-memory cache'd blacklist should be enough.

Seaony commented 6 years ago

thanks! :)

josemiguelq commented 6 years ago

I has some troubles to implement to, but I solved them. 1 - Exactly the same @philliperosario 2 - I create a App\Http\Middleware\BaseJWTMiddleware.php with

`<?php

/*

namespace TradeAppOne\Http\Middleware;

use Tymon\JWTAuth\JWTAuth; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Routing\ResponseFactory;

abstract class BaseJWTMiddleware { /**

3 - Then the RefrshToken.php, same path `<?php

namespace TradeAppOne\Http\Middleware;

use Tymon\JWTAuth\Exceptions\JWTException; use Tymon\JWTAuth\Exceptions\TokenExpiredException; use Tymon\JWTAuth\Facades\JWTAuth;

class RefreshToken extends BaseJWTMiddleware {

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

    if (!$token = $this->auth->setRequest($request)->getToken()) {
        return $this->respond('tymon.jwt.absent', 'token_not_provided', 400);
    }

    try {
        $user = $this->auth->authenticate($token);
    } catch (TokenExpiredException $e) {
        try {
            $newToken = $this->auth->setRequest($request)->parseToken()->refresh();
        } catch (TokenExpiredException $e) {
            return $this->respond('tymon.jwt.expired', 'token_expired',
                500, [$e]);
        } catch (JWTException $e) {
            return $this->respond('tymon.jwt.invalid', 'token_invalid',
                500, [$e]);
        }

        header('Authorization: Bearer ' . $newToken);

        JWTAuth::setToken($newToken)->toUser();

        $user = $this->auth->authenticate($newToken);

    } catch (JWTException $e) {
        return $this->respond('tymon.jwt.invalid', 'token_invalid', 500,
            [$e]);
    }

    if (!$user=JWTAuth::parseToken($token)) {

        return $this->respond('tymon.jwt.user_not_found', 'user_not_found', 404);
    }

    $this->events->fire('tymon.jwt.valid', $user);

    return $next($request);
}

}`

ghostvar commented 6 years ago

for Lumen walkthrough ?

stefanheimann commented 6 years ago

@ralbear I came across the same problem, the solution to this is to set minimum-stability in composer.json file.

{ "minimum-stability": "dev", "prefer-stable": true }

gcjbr commented 6 years ago

I wish I could slowly kiss you on the lips!

Thanks for this great contribution, sir!

Akumzy commented 6 years ago

Am really frustrated right now if anyone can help
Route::any('{all}', function () { return view('index'); }) ->where(['all' => '.*']); I've this route in route/web.php which except all path and return the home page but all my site.com/api/* requests are still going the web instead of api am I missing anything here I have changed my default guard to api still they're still going to web

gcjbr commented 6 years ago

@Akumzy , your api calls should go to the routes defined on api.php by default. You shouldn't have to mess with web.php routes at all.

Akumzy commented 6 years ago

Am handling my routing with vue-router

KazukiSadasue commented 6 years ago

Hello.There is something I don't know. Maybe my program is running to add 'routes\api.php' step. But, I don't know add 'Authenticate method' and 'Axios interceptor' step. I want an example on a simple login vue. Is this example using vuex?

core01 commented 6 years ago

@KazukiSadasue login.vue - https://gist.github.com/core01/e10ded8de504d8bb919797c0d536c53f; Axios interceptor's step - https://gist.github.com/core01/9cb3c292576049e3be5cca0889ed3e52

KazukiSadasue commented 6 years ago

@core01 Thank you! What is store file imported in connection.js? I don't have such a file.

core01 commented 6 years ago

@KazukiSadasue this is my vuex store:

import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)
export const store = new Vuex.Store({
  state: { ... },
  getters: { ... }
...
})
export default store
KazukiSadasue commented 6 years ago

@core01 Thank you ! I will try !

gcjbr commented 6 years ago

@philliperosario how do you avoid axios from creating an instance without the token when it's first loaded (assuming the user is not logged in yet)

alexeycrystal commented 6 years ago

@philliperosario, awesome, thanks! All is working fine. Already tested this on "1.0.0-rc.2" version. Very usefull solution!

victorrss commented 6 years ago

I am using

"php": ">=7.0.0",
"fideloper/proxy": "~3.3",
"laravel/framework": "5.5.*",
"laravel/tinker": "~1.0",
"tymon/jwt-auth": "1.0.0-rc.1"

it worked here Add the following code to the render method within app/Exceptions/Handler.php

public function render($request, Exception $e)
    {
        if($e instanceof \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException){
            return response()->json([$e->getMessage()], $e->getStatusCode());
        }
        return parent::render($request, $e);
    }