Okay, I've got a bit of time, and need to set up a new project with Laravel 5.3 and JWTGuard. @tymondesigns I hope you don't mind this being in your issues similar to my other "guide" (#513), and if so I can try and find a different spot, but I don't write blogs or tutorials.
This is just a post of the steps to get the JWTGuard in place for an API using Laravel 5.3.x to possibly help with starting the documentation of this feature. Anything from #513 that is the same is still included so you don't need to browse through it unless I missed something. I've set this up twice now, and I found myself that it is just easier to do a clean install, and port. So this assumes you are doing a clean install, and currently are using version v5.3.11 of Laravel .
Also, jwt-auth 1.0 hasn't been released yet so this also assumes that you are pulling in the development branch "tymon/jwt-auth": "dev-develop", using composer.
1) config/app.php - add Tymon\JWTAuth\Providers\LaravelServiceProvider::class to providers
2) In your terminal publish the config file: php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider", and add it to your list service providers in app.php
3) In your terminal generate the secret: php artisan jwt:secret provided you have JWT_SECRET in your .env file
4) config/auth.php - set the default guard to api, and change the api driver to jwt
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
...
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
5) /routes/api.php - add a few basic authentication routes
Route::post('login', 'Auth\LoginController@login');
Route::group([
'prefix' => 'restricted',
'middleware' => 'auth:api',
], function () {
// Authentication Routes...
Route::get('logout', 'Auth\LoginController@logout');
Route::get('/test', function () {
return 'authenticated';
});
});
6) /app/http/Controller/auth/LoginController.php
Authentication appears to almost work out of the box using the JWTGuard. To maintain the existing throttling I copied the AuthenticateUsers::login into LoginController and removed the second parameter of the call to attempt from $request->has('remember'); to let it use the default in JWTGuard::attempt, and stored the $token for use, and passed it along to LoginController::sendLoginResponse
public function login(Request $request)
{
...
if ($token = $this->guard()->attempt($credentials)) {
return $this->sendLoginResponse($request, $token);
}
...
}
7) AuthenticatedUsers::sendLoginResponse needs to know about the $token so I pulled it up to the LoginController and removed $request->session()->regenerate(); since JWT is stateless.
protected function sendLoginResponse(Request $request, $throttles, string $token)
{
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user(), $token);
}
8) AuthenticateUsers::sendLoginResponse checks for a method authenticated, which I added to the LoginController to respond when authentication is successful
protected function authenticated(Request $request, $user, string $token)
{
return response()->json([
'token' => $token,
]);
}
9) AuthenticatesUsers::sendFailedLoginResponse can be pulled up to LoginController to respond with JSON instead of redirecting failed authentication attempts
protected function sendFailedLoginResponse(Request $request)
{
return response()->json([
'message' => Lang::get('auth.failed'),
], 401);
}
10) User model needs to implement Tymon\JWTAuth\Contracts\JWTSubject (see #260, and JWTSubject)
...
use Tymon\JWTAuth\Contracts\JWTSubject as AuthenticatableUserContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract,
AuthenticatableUserContract
{
use Authenticatable, Authorizable, CanResetPassword, Notifiable;
...
/**
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey(); // Eloquent model method
}
/**
* @return array
*/
public function getJWTCustomClaims()
{
return [
'user' => [
'id' => $this->id,
...
]
];
}
}
11) Test out whether authentication works by hitting the /login route with Postman using a set of valid credentials, which should return a token.
12) In order to test this setup hit the authenticated route /test without setting the token in the header, which should respond with an error message, and then hit /test again after setting the header to key: Authorization, value: Bearer TOKEN_STRING, which should respond with the string authenticated.
RegisterController contains RegistersUsers::register, which needs to be pulled up to the RegisterController and updated to return a response instead of a redirect.public function register(Request $request)
{
...
return response()->json([
'message' => trans(...),
]);
}
To logout invoke JWTGuard's logout method, which invalidates the token, blacklists the token (assuming you kept the config defaults), resets the user, and unsets the token.
public function logout()
{
$this->guard()->logout(); // pass true to blacklist forever
// ...
}
Thanks for the Tutorial. Right on time for me.
remark: if you don't set the 'auth.defaults.guard' to 'api' from 'web' in the auth.php - you're gonna have some true time :)
@spawn-guy did you remove the second parameter from attempt in login?
if($token = $this->guard()->attempt($credentials)) {
...
@mtpultz discard that..
i forgot to set the 'auth.defaults.guard' to 'api' from 'web'. my bad.
even though i don't really know why it is needed.
may i add my 5 cents, @mtpultz ? a quick-fix to make the response of the login endpoint to be "resembling" OAuth2 flows - edit response() lines to
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user, $token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => \Config::get('jwt.ttl') * 60 // config value is in minutes
]);
}
parameter names in response are specified in the RFC
@spawn-guy maybe I don't get your point but JWT has nothing to do with oauth2 as explained here:
http://www.seedbox.com/en/blog/2015/06/05/oauth-2-vs-json-web-tokens-comment-securiser-un-api/
@daeXecutoR if you use Bearer keyword in the Authorisation header - then how do you tell an api-client to prepend it to the plain token value that you got from the authentication endpoint? (/api/login in this case).
and when you are making this first step towards OAuth2 flows with Bearer why not make a second step? and make your /api/login endpoint output the 2 other fields that are in the OAuth2 RFC.
then why not make a step3 .. where you expose your /api/login endpoint as a /oauth/token endpoint?
and to finish the whole picture - check for "grant_type":"password" on the login endpoint (along with username and password). and that will make you 100% compatible with OAuth2 flows.
benefits are:
OAuth2 is not about JWT vs RandomizedTokens. It's about flows of the tokens themselves(doesn't matter the tech you made them with) and some standardization.
JWT is useful when you work with multitude of different services at the same time. That you(as a UserDB owner) grant permission to be used by someone else. and you don't actually need to have the whole userObject everywhere and/or read the full User from Original User database.
@spawn-guy In my case I'm using angularjs as Front-End for my API and I've now what to add to the header for the restricted ressources ( so I'm okay with just getting a "token: 234....." string)
If you take it further you would also need to setup refresh tokens because your target is to simulate oauth2 password grant except your not using any oauth2-plugins out there for your front-end.
I mean as long as you build everything by yourself you can go down the road as far as you like.
The new 5.3 feature is called passport and not passenger.
@daeXecutoR refresh_token's are totally optional ;)
@spawn-guy How do you handle your jwt tokens in terms of lifetime? I'm not yet sure which approach I should go for. I was thinking about using short lifed tokens like 60 minutes and create an wachter service in my front-end which requests an extension like 5 minutes prior to the expiration.
@daeXecutoR _most_ of ppl give access_tokens for an Hour. refresh_tokens for 2 weeks.
but if you don't have refresh_tokens, each time access_token expires - you need to ask for user's password(which you must NOT store in sessions).
there is an option to _refresh_ the access_tokens.
oh well.. if we are trying to mimic OAuth2 - you can place the same token in both access_token and refresh_token value. but the same time provide 2 expiration values: lower value for access_token and "original" value for refresh_token.
then, in the login function in LoginController, you'll need to distinguish different grant_type's: with "password" - you expect username and password; with "refresh_token" - you exchange the old valid token to a new one.
but _BEWARE_ of the JWT.. as the information can easily be decoded and read by anyone.
or.. hehe... you can generate 2 JWT tokens. one for access_token with 1hour expiration. the other one for refresh_token with a longer expiration period. and you don;t need to store any of them in the database :)
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user, $token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => (\Config::get('jwt.ttl') * 60) - 120 // client has a window of 2 minutes
'refresh_token' => $token,
'refresh_token_expires_in' => (\Config::get('jwt.ttl') * 60) // client has a window of 2 minutes to not to ask for a password
]);
}
@spawn-guy the reason I think about storing the token is that it gives me the possibility to sign them and with that improve security. (storing hash/verify and prevent manipulation)
Yet I'm looking for a complete example of laravel 5.3 + jwt + angularjs that covers all of the challenges we discused here ;-)
But thanks for your valuable inputs. I'm going over a new dev environment and try to achive my goals. :-)
I'm stuck at step 6. There is no /app/LoginController.php file and I even don't know where you edit or what to do with the other steps. Can someone help me out on that?
@daeXecutoR that is a typo the LoginController is in /app/http/controllers/auth
@mtpultz Thanks so much for your support. I got the file, but it is quit empty. Can you show me how the file should look at the end, and where you copied the code from? (I'm pretty new to laravel so sorry for that)
@mtpultz I guess I found the source file. May you want to add it for others:
\vendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.php
@mtpultz Where do you put the changes in step 10? I don't think I do change the file here:
vendor\laravel\framework\src\Illuminate\Foundation\Auth\User.php
I guess I have to create one in my app folder and apply the changes there, right? But where exactly?
@mtpultz In Step 5 you have to remove the API-group-prefix. Because your route is in the route/api.php it's allready added by Laravel. Your route is /api/api right now.
I can't get this to work. If I send username&password credentials I just got a white page. If I try to hit api/test I get "NotFoundHttpException in RouteCollection.php line 161:"
Think I will go with the oauth2 package instead.
@daeXecutoR it seems like you're having more issues with OOP and knowing how Laravel works then how to use this package.
/app.AuthenticatesUsers that is a PHP trait applied to the LoginController, which you can override in the LoginController as traits are a way to perform multiple "inheritance". So you can just copy the method into LoginController and make your changes. I'd suggest reading up on PHP a bit too since a lot of your confusion is also related to understanding PHP OOP. If you see a reference in the guide saying pull up a method then it is a method that is being inherited and needs to be overridden.@mtpultz You're right, first time using the underlaying Laravel framework and don't see through it yet. However I got a working solution following this tutorial:
https://scotch.io/tutorials/role-based-authentication-in-laravel-with-jwt
Thank you very much for your support @mtpultz and @spawn-guy
@daeXecutoR nice glad you found a solution that works for you. Only thing I'd point out is your using jwt-auth (version prior to Laravel Guards) instead of the jwt-guard that integrates into Laravel's Guard API introduced in Laravel 5.2, which is a much more robust API that doesn't require a bunch of setup since it integrate thoroughly with Laravel and lets it do all the lifting. Maybe not now since you're having to learn multiple things at the moment, but I'd suggest not using jwt-auth and use jwt-guards in the long run you'll be better off.
console error:
[Symfony\Component\Debug\Exception\FatalThrowableError]
Class 'Tymon\JWTAuth\Providers\LaravelServiceProvider' not found
my composer.json
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.3.*",
"tymon/jwt-auth": "dev-develop"
},
@devtosystems add the service provider to app.php. Service providers aren't hooked up automatically so each time you pull them in they need to be added to the list of providers. I've updated the steps above to explicitly say it needs to be added to your list of providers.
@mtpultz the versions run is 0.5.* like instalation wiki informs.
@devtosystems if you're using jwt-guard you have to use the development branch (or any of the 1.x-alpha branches that also contain the JWTGuard class).
NOTE: 0.5.x only contains the older version this package (JWTAuth class) that existed prior to Laravel v5.2. The wiki is not up to date, and should not be consulted for any version of jwt-auth outside of version v0.5.x or less. The initial version of this "guide" for v5.2 and now v5.3 was due to a lack of documentation on how to implement JWTGuard for the development/alpha branches, and to help provide documentation when the wiki is updated since you can't post PRs to the wiki, otherwise I'd have provided these updates there so they were more readily found. So for now anytime someone has an issue I incrementally update the steps above.
@mtpultz I don't like the jwt-guard use doc middleware. For me it's confuse. =/
I not use jwt-guard more.
Please, answer me, jwt-auth branch dev-develop not use aliases in config/app.php?
Sorry @devtosystems this guide is specific to using JWTGuard since it is so much easier (after you understand how it works), and uses Laravel's built in auth middleware, as well as any you can create. I don't know anything about the JWTAuth class of this package.
@mtpultz I have a problem with post for api route in console. Look:
Whoops, looks like something went wrong.
ReflectionException in /vendor/laravel/framework/src/Illuminate/Container/Container.php line 749"
Class Tymon\JWTAuth\Providers\JWT\NamshiAdapter does not exist<
My test data (without database yet) is:
curl -X POST -H 'Content-Type: application/json' -d '{"email":"[email protected]","password":"123456"}' http://projects.dev/mylaravelapi/public/api/login
@mtpultz can you provide a github sample repo?
@devtosystems remove jwt.php and run php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
Now it is right because the message contains database erros.
<h1>Whoops, looks like something went wrong.</h1>
<h2 class="block_exception clear_fix">
<span class="exception_counter">1/1</span>
<span class="exception_title"><abbr title="PDOException">PDOException</abbr> in <a title="/home/vagrant/mylaravelapi/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php line 119" ondblclick="var f=this.innerHTML;this.innerHTML=this.title;this.title=f;">Connector.php line 119</a>:</span>
<span class="exception_message">SQLSTATE[HY000] [1045] Access denied for user 'homestead'@'localhost' (using password: YES)</span>
</h2>
Now, how do I transcribe plain text messages to the api?
@devtosystem you should be asking questions outside of setting up your application using JWTGuard on a forum like stackoverflow. This is just a guide to get the basics running using the JWTGuard. The hope was that at the end of setting it up people would understand how it works not just copy the code. If these steps don't work to get a basic application running using JWTGuard then by all means drop a question.
What is the best way to generate an token after registering?
Now i have added something like this to the 'register' function.
$credentials = [
'email' => $request['email'],
'password' => $request['password'],
];
$token = $this->guard()->attempt($credentials);
The LoginController uses the "credentials" function. It might be better to reuse this code though.
@BasConijn I did the same. 👍
You probably also need to pass true to the guard's logout method.
@mattmcdonald-uk passing "true" the token will be updated but the user's session is lost?
@devtosystems Sorry - that commit had nothing to do with the behaviour I was seeing! But, unless I pass true to logout (setting forceForever to true when invalidate is called), the logged out user's token could still be used.
This may be a side-effect of my allowing a small grace period on tokens for multiple requests, but it seems more secure to ensure the token is entirely blacklisted when the user logs out.
@BasConijn I've never used api.auth in fact I don't even know where to find it, but auth:api is middleware being passed a parameter, which just means the auth middleware is being passed the api guard.
The idea behind $guards is to provide a means to authenticate against different drivers that you assign associated providers. Following the steps above you're setting the api guard to use the jwt driver, and by adding auth:api on routes you're protecting your routes from access unless authenticated, and if you're not authenticated you'll get a 500 response. So in order to access that route you need to be sending up the authentication token, which will be automatically be decoded, and authentication attempted before any controller action, and if the token is invalid, expired, or missing that action is never executed.
But with the "api.auth" I am getting the correct 401 or 403 http errors which makes much more sense when providing an invalid token.
(It might also be the reason that I use dingo instead of the default router)
Edit: api.auth is something of dingo.
@BasConijn I stand corrected it does return a 401 for invalid tokens. Your 500 error is unrelated to the authentication check performed. I took a valid token, and set the expiry to expire immediately, and also did another check where I just sent an invalid token, and both return a 401. I would have been surprised if Laravel would produce anything that didn't follow all the standards.
If you are getting the login page as response instead of JSON when the authentication failed, that may be because you are not sending the proper headers so laravel treats the request as AJAX.
That was happening to me when testing using postman, I fixed it by passing Content-Type: application/json, and Accept: application/json
@devtosystems you need to ask questions that have nothing to do with setting up a basic implementation of Laravel 5.3 with jwt-auth in a forum like http://stackoverflow.com. The original post clearly states:
This is just a post of the steps to get the JWTGuard in place for an API using Laravel 5.3.x to possibly help with starting the documentation
This isn't a sounding board for your development issues outside the guide. All this does is obscure any questions to help people setup the basics that are having issues specifically with the steps outlined in the original post so it can be refined making it more clear. If you can get up and running using the guide then its purpose has been served, you might understand a bit more about the package and guards based on having to make the changes yourself or you might not, but at least the basics work and you can learn as your application grows based on need and hopefully not be blocked.
Has anyone gotten this to work in Lumen 5.3 without Eloquent?
Hello all, I've been following these steps and I have Authentication behaving correctly. 👍
However my logout method appears to be losing its CORS headers. My routes look like this:
Route::group(['middleware' => 'cors'], function() {
Route::group(['namespace' => 'Auth'], function() {
Route::post('login', 'LoginController@login');
Route::get('logout', 'LoginController@logout')->middleware('auth:api');
// ...
});
Route::group(['middleware' => 'auth:api'], function() {
// ...
});
});
public function logout(Request $request)
{
$this->guard()->logout();
return response()->json(['message' => 'Logged out'], 200);
}
Response:
500 - No 'Access-Control-Allow-Origin' header is present...
The correct header Authorization: Bearer ${token} is passed to the logout route.
Using php artisan route:list I can see the cors middleware is indeed applied to the logout route.
The CORS functionality works as expected for all other routes.
Does anyone have any suggestions? Cheers.
Instead of overwriting the register method you could leave it untouched and instead override the registered method.
/**
* The user has been registered.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function registered(Request $request, $user)
{
return response()->json([
'message' => trans('auth.register'),
]);
}
I managed to receive the token but I cannot hit the /test route. What would be the correct url to enter? api/test? I just always get the NotFoundHttpException.
Hi @anjosi can you show your test route? It isn't in the example you have above. Might be better to show all your routes since using above and the example test route it looks like it should be /test. You might need to check the RouteServiceProvider and indicate what you have in there too.
Here's my routes/api.php file
` use Illuminate\Http\Request;
/*
--------------------------------------------------------------------------
API Routes
--------------------------------------------------------------------------
Here is where you can register API routes for your application. These
routes are loaded by the RouteServiceProvider within a group which
is assigned the "api" middleware group. Enjoy building your API!
*/
Route::post('login', 'Auth\LoginController@login');
Route::group([
'prefix' => 'restricted',
'middleware' => 'auth:api',
], function () {
// Authentication Routes...
Route::get('logout', 'Auth\LoginController@logout');
Route::get('/test', function () {
return 'authenticated';
});
});
/*
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:api');
*/
`
And here's the RouteServiceProvider
`<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* This namespace is applied to your controller routes.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = 'App\Http\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
//
parent::boot();
}
/**
* Define the routes for the application.
*
* @return void
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
//
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::group([
'middleware' => 'api',
'namespace' => $this->namespace,
'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');
});
}
}
`
@anjosi try /api/restricted/test. You have a prefix of api in your route service provider, and restricted in your api.php routes
/api/restricted/test seems to be correct. However it only works if I comment out the middleware element in the argument list. Should I register the auth:api in kernel.php? To which middleware class that alias refers?
I've finally got thing working by setting up the laravel passport as instructed in this wonderful video (https://laracasts.com/series/whats-new-in-laravel-5-3/episodes/13) by Taylor Otwell.
After struggling a bit I got it running on Laravel 5.3. If I can help some one let it me know. Files ended being kind of different in some occasions. I will try to post them later.
@hazzo Been struggling a bit myself but got it to work, please post your results so i can compare.
@maikdiepenbroek Yes here are de LoginController, api routes and User model.
Here I used the same methods only that variable $credentials did not have any value, so I look inside the Laravel login trait and used the same method to crate credentials that Auth uses $credentials = $this->credentials($request);. Also the string part of Auth gave me error when inserting it in the second parameter with the $token. So I eliminate it, I rally don't know how string work but I did not find it necessary because the token I will always be passing is a string...
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Lang;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function login(Request $request)
{
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
$credentials = $this->credentials($request);
if ($token = $this->guard()->attempt($credentials)) {
return $this->sendLoginResponse($request, $token);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
/**
* Log the user out of the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
$this->guard()->logout();
}
/**
* Send the response after the user was authenticated.
*
* @param Request|\Illuminate\Http\Request $request
* @param string $token
* @return \Illuminate\Http\Response
*/
protected function sendLoginResponse(Request $request, $token)
{
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user(), $token);
}
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @param string $token
* @return mixed
*/
protected function authenticated(Request $request, $user, $token)
{
return response()->json([
'token' => $token,
]);
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
return response()->json([
'message' => Lang::get('auth.failed'),
], 401);
}
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'logout']);
}
}
The User model was a bit difficult. By any means it will work with the above settings. I have to look for the elements that constitute the Authenticable Class and use them in the model. I also eliminate the classes that were not been used.
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Tymon\JWTAuth\Contracts\JWTSubject as AuthenticatableUserContract;
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract,
AuthenticatableUserContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey(); // Eloquent model method
}
/**
* @return array
*/
public function getJWTCustomClaims()
{
return [
'user' => [
'id' => $this->id,
]
];
}
}
For me the routes where the same. I did only eliminate the prefix. Did not need it. I have to say that i did all this with a CLEAN Laravel install.
With this settings I recibe a token I can logout with above settings too. Once this is clear it's quite faster than OAuth.
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// Login
Route::post('login', 'Auth\LoginController@login');
Route::group([
'middleware' => 'auth:api',
], function () {
// Authentication Routes...
Route::get('logout', 'Auth\LoginController@logout');
Route::get('test', function () {
return 'authenticated';
});
});
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:api');
@hazzo Thnx for the code example, it's more or less the same as i have so thats a good confirmation.
The string is called Scalar type hint, more info here: https://wiki.php.net/rfc/scalar_type_hints
@maikdiepenbroek oh thanks, I supposed that it was something like that (used int many times) but I don't know why it was not working.
Hello @hazzo , when I use 'auth:api' in middleware it gives me an error, any idea?
PD: Show me your Kernel.php, jwt.php and auth.php pls :(
Command "jwt:secret" is not defined.
jwt:generate doesn't seem to set the key anywhere either.
Version 0.5.9
@JesusGR4 what kind of error do you get?
Here is 'auth.php', Kernel and JWT are untouched.
<?php
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' => 'api',
'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' => 'jwt',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| 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,
],
// '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,
],
],
];
Maybe you are missing this aliases in your app.php
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class
Why is this still not documented?
@hazzo I love u man, that code example help me a lot, was stuck for 2 hours. +1
Hi, I would like to use jwt-auth 1.0.x in laravel 5.4 for JWT multiple table
Is it possible ?
@mtpultz many thanks for bootstrapping me into this. I had just finished a skeleton with vanilla jwt-auth when i saw your post. The move to jwt-guard is worth it 👍
I'd highly recommend to repost this how-to in a different issue so to clean up a bit the mess with comments.
For anyone wondering how to get rid of the override the default auth config in config/auth.php
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
]
just override the auth provider for guard in your LoginController :
/**
* Get the guard to be used during authentication.
* Overrides default Laravel method to get the 'api' guard
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard('api');
}
the important change is the 'api' in return Auth::guard('api');, this will make sure you do not fallback to default auth provider when calling method like
$token = $this->guard()->attempt($credentials)
this way you can use any number of auth provider in you app than needed, by specifying in related controllers which provider to use each time
This should be in the wiki. That would be really helpful.
Secondly, any chance anyone has some code to share for 5.4? I wish there was a repo somewhere with the basics covered, like this tutorial does.
It sample not working absolutely. I spent more five hours to get it work.
I get {token: "1"} as response if I login successfully, instead of a token. I can not trace the reason of this behaviour. I'm using Laravel 5.4 and the dev-develop branch (1.0.x) in composer.json. With some minor 5.3->5.4 tweaks researching errors on the go, this seems to be the last problem I can't tackle myself to get this fully functional.
Shortest way : Change the default auth to "api" in auth.php
I'm ashamed, how did I miss that one... I did have it set to api, but somehow I/something reverted it back. Probably staring too much at the code last few days... Thanks a lot!
I made it all work (returning token upon successful login, restricting access to protected routes) in Laravel 5.3, but when I make a request to а protected route without a token, or with invalid token, I receive 404 Not Found, instead of 401 Unauthorized.
Anyone facing this issue?
Thanks in advance.
@mtpultz I had a fresh Lumen 5.4 installation and followed this tutorial. Login and others work fine but the logout doesn't seem to work properly. What I mean is, if I try to expire a token it doesn't give me an error but if the same token is re-used, it should say expired but still goes through. In simple terms, I believe it is not expiring the token at all.
Here is my code:
class UserController extends Controller
{
protected $jwt;
public function __construct(JWTAuth $jwt)
{
$this->jwt = $jwt;
}
public function Signin(Request $request)
{
$this->validate($request, [
'email' => 'required|email|max:100',
'password' => 'required|min:6',
]);
if (!$token = $this->jwt->attempt($request->only('email', 'password'))) {
return response()->json(['The credentials provided are invalid.'], 500);
}
return response()->json(compact('token'));
}
public function LogoutUser(Request $request){
$this->jwt->invalidate($this->jwt->getToken());
return response()->json([
'message' => 'User logged off successfully!'
], 200);
}
}
my routes:
$app->group(['prefix' => 'api'], function($app){
$app->post('/signup', [
'uses' => 'UserController@Signup'
]);
$app->group(['middleware' => 'auth:api'], function($app){
$app->post('/logout',[
'uses' => 'UserController@LogoutUser'
]);
});
});
my config/auth.php says:
'defaults' => [
'guard' => env('AUTH_GUARD', 'api'),
],
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users'
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => \App\User::class,
],
],
'passwords' => [
//
],
Any help will be greatly appreciated.
Hi, step 2 is telling me "class auth does not exist". Did someone already encountered that ? I pulled in :
"tymon/jwt-auth": "dev-develop"
Hi, I am using 2 different guards to authenticate user i.e one for the customer and one for the admin
'customer' => [
'driver' => 'jwt',
'provider' => 'customer',
],
'admin' => [
'driver' => 'jwt',
'provider' => 'admin',
],
I have two different tables and Models. I implemented the JWTGuard as mentioned in this post and I am able to login on different guard and it return the token as well. But, the problem I am facing is, with the returned token, both customer and admin can access each other's protected(pages which only admin and only customer can access) area. I mean, customer can access the admin area using the customer token. Actually, JWT uses id as a parameter to find the user from the token and if the user with the same id(customer id) exists in the admin it returns that user.
Please suggest me a solution.
@sukhpreetmcc How did you get the jwt multi-auth guard to work. I've implemented something similar but the only guard that works is the one with the User model. The other guard doesn't work, it throws an error.
@kofikwarteng I used Laravel Mutil Guard to login
auth.php code
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'customer' => [
'driver' => 'jwt',
'provider' => 'customer',
],
'admin' => [
'driver' => 'jwt',
'provider' => 'admin',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\ModelsUser::class,
],
'customer' => [
'driver' => 'eloquent',
'model' => App\Models\Customer::class,
],
'admin' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
]
],
admin.php model code
namespace App\Models;
use Illuminate\Notifications\Notifiable;
//use Illuminate\Auth\Authenticatable;
use Illuminate\Foundation\AuthUser as Authenticatable;
use Config;
use Illuminate\Database\Eloquent\Model;
use Tymon\JWTAuthContracts\JWTSubject;
class Admin extends Authenticatable implements JWTSubject
{
protected $guard = "admin";
public function getJWTIdentifier()
{
return $this->getKey(); // Eloquent model method
}
public function getJWTCustomClaims()
{
return [
'user' => [
'id' => $this->id,
]
];
}
}
Same code is used for the customer model as well.
admin auth controller code
namespace App\HttpControllers\Auth;
use App\HttpControllersController;
use Illuminate\Foundation\AuthAuthenticatesUsers;
use Auth,Hash,Mail;
//use Illuminate\Support\Facades\Request;
use Illuminate\Http\Request;
use Validator,JWTAuth;
use App\Models\Admin;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class AdminLoginController extends Controller
{
use AuthenticatesUsers;
protected $redirectTo = '/home';
protected $guard = 'admin';
public function login(Request $request) {
$this->validateLogin($request);
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
$adminAuth = auth()->guard('admin');
$credentials = $request->only('email', 'password');
if ($token = $adminAuth->attempt($credentials)) {
return $this->sendLoginResponse($request, $token);
}
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
protected function sendLoginResponse(Request $request, string $token) {
$this->clearLoginAttempts($request);
return $this->authenticated($request, auth()->guard('admin')->user(), $token);
}
protected function authenticated(Request $request, $user, string $token) {
return response()->json([
'request' => $request,
'user' => $user,
'token' => $token,
'success'=> true,
'message' => 'Successfully logged in'
]);
}
}
And for customer login I don't specify guard as it is the default as mentioned in the auth.php file. It authenticates user from 2 different tables. The problem I am getting is the admin token can be used to authenticate the customer and vice-versa, which is incorrect.
@sukhpreetmcc I still faced the same problem but I solved it by adding this line to CreateUsersProvider.php on like 65. The problem was due to the fact that the index was called 'table' instead of 'model':
/**
* Create an instance of the Eloquent user provider.
*
* @param array $config
* @return \Illuminate\Auth\EloquentUserProvider
*/
protected function createEloquentProvider($config)
{
if(isset($config['table'])) {
$config['model'] = $config['table'];
}
return new EloquentUserProvider($this->app['hash'], $config['model']);
}
I then realized the problem you were facing. I use the token to access the other guard's resource and I was able to access the resource since they both had the same id. We need to figure out a solution to that problem.
@kofikwarteng That's great, your problem is solved. Right, we need to find the solution for the second problem. I am trying to solve this. Do post here if you find any solution before me. I also expect other more experienced developers to help as well.
Thanks, very helpful guide.
the below works for me!
$token = $this->guard()->claims( [
'permissions' => '',
'user_menu' => ''
])->attempt($credentials);
@kofikwarteng Hi, did you find any way out?
How did you get the jwt multi-auth guard to work. I've implemented something similar but the only guard that works is the one with the User model. The other guard doesn't work, it throws an error.
期待更好的解决方案
@mtpultz First of all thank you for this guide. It helped me setting up JWT with JWTGuard. However I can't seem to find any information on how to correctly implement refresh tokens.
Could you please guide me through implementation of refresh tokens?
Route::group(['middleware' => 'auth:api'], function () {
Route::get('/me', 'UserController@me');
});
public function me()
{
return Auth::guard('api')->user();
}
(1/1) InvalidArgumentException
Route [login] not defined.
Hi @chrishessler the refresh tokens are applied using the refresh middleware, and making sure they are turned on in your config. I haven't used them in a bit, but I think that is it. Let me know if that doesn't work and I can have a look at a previous project.
@zanjs not sure what your issue might be, but I'd check if you have a route named login by using artisan to display a list of your routes. If you don't have a login route specifically named login likely Laravel requires it, but from what you've posted there's no correlation between your route and the error.
@zanjs this probably means you're not logged and therefor tries to send you to a login route (which doesn't exist (yet)). This happens in the App\Exceptions\Handler class in its unauthenticated method.
If the request to /me would have the following header; Accept: application/json then it would have different output (a 401 response with an error indicating you're unauthenticated).
@sanderlooijenga Thinks !
It is not possible to inform which guard I'd like to use to retrieve user information.
https://github.com/tymondesigns/jwt-auth/wiki/Authentication
This error is occurring because I do not use the default guard because I'm implementing each one for a different social network.
Call to undefined method Illuminate\Database\Query\Builder::getAuthIdentifierName()
So, I replace this line in my Middleware:
$user = $this->auth->authenticate($token);
to:
$user = $this->auth->getClaim('<inform the guard dinamically>');
and then:
array:7 [
"id" => 9
"firstname" => "3f207KW2ch1UyQvC"
"lastname" => "LnaR3CDziAC9sfRw"
"email" => "[email protected]"
"createdAt" => "2017-07-31 19:42:00-03"
"lastUpdatedAt" => "2017-07-31 19:42:00-03"
"lastAccess" => "2017-07-31 19:42:00-03"
]
... but you lose all control of exceptions under the token.
When I initiate 'route:list' at the console, I still see web being the primary guard being used. I've even changed all of the guards to 'jwt', but sessions are still being used. Is there a way to hack the framework to just work with tokens? There's not enough documentation on how to properly implement Tymon's jwt-auth framework. I've used this tutorial to get past the [api] not defined error, but sessions are still being stored.
@awd22 it is definitely possible. What version of Laravel are you using 5.3, 5.4, 5.5? Where are you putting your routes web.php, api.php, etc? Have you looked in the RouteProvider?
Laravel 5.4, The routes are in the default web.php file. I haven't modified anything in the RouteServiceProvider file
I think if you look in the RouteServiceProvider you'll see very quickly why putting your routes in web.php is using the web middleware.
yeah I saw that Route::middleware was set to web... So do you have any suggestions where I should insert ajax requests at? Newby at JavaScript & HTTP stuff
when i change it to api.. the whole front end breaks and it can't even bring up the landing page without an error
The route service provider is mapping middleware to a file. You're putting your routes in web.php, which has web middleware automatically applied to it so your routes have it applied. So you can either remove the middleware from mapWebRoutes (not advised since it can be used for views or responses that require cookies), put them in mapApiRoutes (api.php), or make up your own mapping.
Leave the route in web.php that serves your views, and put all your HTTP requests in api.php.
I'm trying to follow you... so do I change the Route::middleware line for mapWebRoutes()? Because as soon as I change it from web to api, the whole app breaks and I get an error. I have a template for ajax requests but not sure how to put them in Laravel
@awd22 - I think the easiest way is to leave the web middleware alone. When you want to make ajax requests, use the /api/ route. If you followed this guide correctly, the api route should be protected by the 'api' guard, which means it's using Tymon's JWT middleware.
So, make ajax requests to: yourdomain.com/api/testRoute. (Notice /api/ in the name of your URL)
Edit Actually the middleware isn't automatically used. You need to append that middleware in api.php to your specific routes.
yeah I understand the prefix part of the routeserviceprovider file. I'm trying to have every user be authenticated through tokens, and not have any sessions being recorded. Is that even possible in Laravel?
So you're saying that using the api middleware provided through api routes in api.php and that is creating sessions? I don't understand how that is possible. The api middleware by default is a middleware group that just throttles and does bindings. To create a session you'd have to be using the LoginController and then not making any JWTAuth changes listed above in the guide. I think you should probably revisit the changes you've made to LoginController, or show some code examples.
Also, @awd22 isn't talking about the prefix he's saying you need to do add the auth middleware similar to step 5 in the guide where it adds auth:api, which says for auth use the JWT driver applied to the API guard (see step 4) instead of Laravel's authentication driver that uses sessions.
namespace App\HttpControllers\Auth;
use App\HttpControllersController;
use Illuminate\Foundation\AuthAuthenticatesUsers;
use AppUser;
use Illuminate\Http\Request;
use JWTAuth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Validation\ValidationServiceProvider;
use DB;
use Tymon\JWTAuth\Exceptions\JWTException;
class LoginController extends Controller {
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
/
use AuthenticatesUsers;
/*
* Where to redirect users after login.
*
* @var string
/
protected $redirectTo = '/home';
/*
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
}
protected function authenticate(Request $request) {
// grab credentials from the request
$rules = array(
'userid' => array('required','string','min:6','max:10','regex:/^(?=.*[a-zA-z])(?=.*\d)(?=.*(_|[^\w])).+$/'),
'password' => array('required','string','min:8','max:12','regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*(_|[^\w])).+$/'));
$messages = array(
'userid.regex' => 'The User ID must be 6-10 characters: 1 letter(Upper or Lower), 1 number, 1 special character_!@#$^&*',
'password.regex' => 'The Password must be 8-12 characters: 1 upper case, 1 lowercase, 1 number, 1 special character_!@#$^&*');
$this->validate($request, $rules, $messages);
if (!$this) {
return Redirect::back();
}
else {
$input = $request->all();
$check = DB::table('users')->select('confirmed','confirmation_code')->where('userid',$input['userid'])->get();
if (!$check) {
return Redirect::back();
}
else {
if ($check['confirmed'] = 1) {
$credentials = $request->only('userid', 'password');
try {
$token = JWTAuth::attempt($credentials);
if (!$token) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
if ($token = $this->guard()->attempt($credentials)) {
return $this->sendLoginResponse($request, $token);
//return $insert = response($token)->header('Authorization', 'Bearer ' . $token);
}
}
catch (JWTException $e) {
return response()->json(['error' => 'could_not_create_token'], 500);
}
//$token = JWTAuth::parseToken('bearer', 'HTTP_AUTHORIZATION')->getToken();
//return dd($token);
//return dd(getallheaders());
//return redirect()->route('profile');
//return response()->json(compact('token'));
}
else {
abort(403, 'Email needs to be authenticated.');
}
}
}
}
protected function sendLoginResponse(Request $request, string $token){
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user(), $token);
}
protected function authenticated(Request $request, $user, string $token) {
return response()->json(['token'=>$token, ])->header('Authorization', 'Bearer ' . $token);
}
protected function sendFailedLoginResponse(Request $request) {
return response()->json(['message' => Lang::get('auth.failed'),], 401);
}
}
My validation methods work before and your methods work.. I just dont know how to get rid of the sessions.. I just want JWT auth for everything.
And you've changed /config/auth.php to use the jwt driver for the api guard, and api is your default guard? That is where the default Laravel authentication using sessions is removed and replaced with JWT. After that you can use the auth middleware and it will default to the api guard. If api is not set to be the default guard then you need to explicitly use auth:api middleware on your routes. Now if you're sending up the token it gets read automatically and you can use Auth::method() to access the JWT API and the user like Auth::guard()->getUser() or Auth::guard($this->guard)->getUser(). Where are you seeing a session?
'defaults' => [
'guard' => 'api',
'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' => 'jwt',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
So where are you seeing the session? and maybe provide the routes your using to login and then access authenticated routes.
when I open developer tools, there is a set-cookie in the response/request headers... so the session is being stored in cookies on the browser.. so that's why I know sessions are still being used
Have you tried removing the session middleware from the web middleware group in Kernel.php?
when I comment out all of the lines with session under $middlewareGroups, the whole app breaks & error comes up when trying to display landing page
If like me you run into this issue but setting the default guard to api is not an option then i suggest following this guide: http://mattallan.org/2016/setting-the-guard-per-route-in-laravel/ to set your driver. This fixed it for me.
Worked like charm in Laravel 5.5 too.
if i have two guard merchant and user, both use jwt driver, what can i distinguish them?
user and merchant have same identifier. when user got token, Auth::guard('merchant')->check() will still pass when use user's token
@tradzero This is the solution #825 , thanks for @mtpultz for the answer
@mtpultz you saved me an awful lot of time thanks!!
hello guys, im getting this error, any idea? Auth guard driver [api] is not defined.
Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
Most helpful comment
may i add my 5 cents, @mtpultz ? a quick-fix to make the response of the login endpoint to be "resembling" OAuth2 flows - edit response() lines to
parameter names in response are specified in the RFC