jwt-auth
jwt-auth copied to clipboard
Laravel 5.5 + Vue.js 2 + JWT Auth 1.0.0-rc.1
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.
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?
I guess dev-develop
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"
},
@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 see in config/auth.php the providers['users'] array.
@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 put here your config/auth.php file
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
'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()).
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))
use the guard on your routes
You mean like this
Route::middleware('jwt:api')->get('/user', function (Request $request) { return ['name' => 'test']; });
still no luck :-(
@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
@Rafeethu I think the guard must be called like this
Route::get('/user', function (Request $request) ['middleware' => 'auth:api']);
@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
@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.
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
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 well remembered, I updated my answer
@philliperosario , thanks for tutorial. however, what about the signing up?
@zhekaus sorry, I did not implement the sign up
@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 , 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 please check if you are using the right facade inside AuthController it should be use Tymon\JWTAuth\Facades\JWTAuth;
yes, I am
Your User model must implement Tymon\JWTAuth\Contracts\JWTSubject interface
@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.
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.
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?
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.