tymondesigns / jwt-auth

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

Feature: Laravel 5.2 Custom Authentication Guard and Driver #513

Closed mtpultz closed 6 years ago

mtpultz commented 8 years ago

The docs indicate it is possible to create your own implementation of Illuminate\Contracts\Auth\Guard and registering it as a driver in a service provider.

I was reading about the new stateless token authentication that was added in 5.2 in a JacobBennett Gist (the docs are really vague), but it doesn't appear to be the same as JWT tokens. That said it would be amazing to be able to leverage Laravel's API the same way.

Would it be possible to create a custom driver to reduce the amount of changes required to implement JWT tokens, and reduce the API a bit so using more of Laravel's API? For example getting a user is Auth::guard('api')->user(); using the API guard, and the equivalent could be Auth::guard('jwt')->user();

daviestar commented 8 years ago

+1

tdhsmith commented 8 years ago

This is already being worked on on the develop branch. (cf. JWTGuard.php)

If you're interested, there are some discussions on the matter at #376, #384, and #479, among others.

mtpultz commented 8 years ago

Thanks @tdhsmith, I'm very interested. Thanks for the links. Are there any areas that you need help on to make this usable for early stages of development using Laravel 5.2? From the links it appears that Laravel 5.2 has a lot of methods implemented, but I'm not familiar enough with it to tell how far along it is. Most of the issues are related to Lumen integration. Trying to time this with our needs of upcoming development.

mtpultz commented 8 years ago

@tdhsmith are there steps to setup the use of the JWTGuard to test it out? The current wiki setup doesn't seem to work the same way.

tdhsmith commented 8 years ago

Are there any areas that you need help on to make this usable for early stages of development using Laravel 5.2?

Well I haven't personally worked on any of the new guard stuff, so I don't have a good sense myself. Perhaps @tymondesigns has suggestions. I think the main goals are just to test it thoroughly (are there unit tests for everything?) and then evaluate which other methods on SessionGuard might be useful to adapt for it.

are there steps to setup the use of the JWTGuard to test it out?

Not yet. I think it should be sufficient to set your auth driver to jwt (assuming you've already done the config stuff in the docs). Then in general you should replace calls to JWTAuth with calls to Auth itself, and explore the new helpers this provides. In particular you should test out "traditional" auth sugars like Auth::user(), Auth::guest(), Auth::id(), and Auth::logout(). (If you're working in an app that already accomplishes everything with middleware route gating though, you probably won't see big changes then, since you probably aren't running any checks like this anywhere.)

tdhsmith commented 8 years ago

These are some other thoughts I've had on ways forward. They might not be exactly related to your question, but I figured I would share them here as long as I have them:

tymondesigns commented 8 years ago

@tdhsmith on your first point, I removed the JWTAuth dependency from the JWTGuard so there is no weird auth call stack now, only JWTGuard --> JWT --> Laravel's AuthManger. Or did I miss something?

And you second point, It has been on my mind for a while, that it would be great to have concrete examples where people can see how things plumb together. Once I get this next release out of the way, that will be next I think.

tdhsmith commented 8 years ago

Nope you're totally right. I was simultaneously looking at pre-guard code and the new code, and not keeping them separate in my mind. The JWT / JWTAuth split keeps that cycle stuff from happening. Smart choice! :+1:

daviestar commented 8 years ago

This is the L5.1 demo app that led me here: https://laracasts.com/discuss/channels/laravel/starter-application-vuejs-laravel-dingo-jwt

mtpultz commented 8 years ago

This is just a post of the steps to get the JWTGuard in place for an API using Laravel 5.2.x to possibly help with starting the documentation of this feature, but also to see if this is the best way to implement JWTGuard. For example, is there a way that not so many methods need to be overridden with one or two changes.

Using JWTGuard with Laravel 5.2.x

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" 3) In your terminal generate the secret: php artisan jwt:secret 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) /app/routes.php - add a few basic authentication routes

Route::group([
    'prefix' => 'api'
], function () {

    $this->post('login', 'Auth\AuthController@login');
    $this->get('logout', 'Auth\AuthController@logout');

    Route::group([
        'prefix' => 'restricted',
        'middleware' => 'auth:api',
    ], function () {

        Route::get('/test', function () {
            return 'authenticated';
        });

        Route::get('/index', 'HomeController@index');
    });
});

6) /app/AuthController.php

Authentication appears to almost work out of the box using the JWTGuard. To maintain the existing throttling I copied the AuthenticateUsers::login into AuthController and edited the second parameter of the call to attempt from $request->has('remember'); to be the default used in JWTGuard::attempt, and stored the $token for use.

public function login(Request $request)
{
    ...

    if ($token = Auth::guard($this->getGuard())->attempt($credentials)) {
        return $this->handleUserWasAuthenticated($request, $throttles, $token);
    }

    ...
}

7) AuthenticatedUsers::handleUserWasAuthenticated also needs to know about the $token

protected function handleUserWasAuthenticated(Request $request, $throttles, $token)
{
    if ($throttles) {
        $this->clearLoginAttempts($request);
    }

    if (method_exists($this, 'authenticated')) {
        return $this->authenticated($request, Auth::guard($this->getGuard())->user(), $token);
    }

    return redirect()->intended($this->redirectPath());
}

8) AuthenticateUsers::handleUserWasAuthenticated checks for a method authenticated, which I added to AuthController to respond when authentication is successful

protected function authenticated($request, $user, $token)
{
    return response()->json([
        'user'    => $user,
        'request' => $request->all(),
        'token'   => $token
    ]);
}

9) AuthenticatesUsers::sendFailedLoginResponse can be pulled up to AuthController to respond with JSON instead of redirecting failed authentication attempts

protected function sendFailedLoginResponse(Request $request)
{
    return response()->json([
        'message'  => $this->getFailedLoginMessage(),
        'username' => $this->loginUsername(),
        'request'  => $request,
    ]);
}

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;

    ...

    /**
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey(); // Eloquent model method
    }

    /**
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

11) Test out whether authentication works by hitting the /login route with Postman by setting the header to key: Authorization, value: Bearer TOKEN_STRING

12) In order to hit the authenticated routes /test this setup will work, but if you're using the HomeController and hitting a route like /index, make sure you remove the auth middleware from the controller's constructor, otherwise the routes to actions within the HomeController won't work.

Registration Using JWTGuard with Laravel 5.2.x

  1. The RegistersUsers trait added to the AuthController also needs an update as JWTGuard doesn't have a login method only an attempt method. So you need to pull up the register method to AuthController and substitute out the guard invoking login for attempt, capture the token, and then return the token in a response.
public function register(Request $request)
{
    $validator = $this->validator($request->all());

    if ($validator->fails()) {
        $this->throwValidationException(
            $request, $validator
        );
    }

    $this->create($request->all());

    $credentials = [
        'username' => $request['username'],
        'password' => $request['password'],
    ];

    $token = Auth::guard($this->getGuard())->attempt($credentials);

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

Logout

To logout invoke JWTGuard's logout method, which invalidates the token, resets the user, and unsets the token.

public function logout()
{
    Auth::guard($this->getGuard())->logout();

    // ...
}
MitchellMcKenna commented 8 years ago

Thanks @mtpultz, would someone be able to post a similar guide to using JWTGuard with Lumen as well?

mtpultz commented 8 years ago

@tymondesigns seems worth adding a login action to the guard to allow for easy registration (this is more of a note). If I have time on the weekend I'll have a look at it.

ctadlock commented 8 years ago

How is this coming along? I have it working but Im forced to use dev-develop so that I can use the JWTGuard login method.

Luddinus commented 8 years ago

+1

ghost commented 8 years ago

+1, is that included into next first stable release (1.0)?

3amprogrammer commented 8 years ago

Can someone explain me what is this parameter doing / what is going on in this line ?

public function attempt(array $credentials = [], $login = true)
{
    // ...
        return $login ? $this->login($user) : true;
    // ...
}
tymondesigns commented 8 years ago

@3amprogrammer by default the method will return a token, but if you pass false as the second param then a boolean will be returned, indicating whether the credentials are valid

3amprogrammer commented 8 years ago

@tymondesigns thanks for explanation. It is kinda strange cause when this method returns either a token or a boolean true we can be sure that the credentials where valid.

HlaingTinHtun commented 8 years ago

Can you show logout function in auth controller? @mtpultz

fer-ri commented 8 years ago

@mtpultz thats really great .. also need logout example :+1:

mtpultz commented 8 years ago

Hi @HlaingTinHtun and @ghprod,

Logout

/**
 * Log the user out of the application.
 *
 * @return \Illuminate\Http\Response
 */
public function logout()
{
    Auth::guard($this->getGuard())->logout();

    // ...
}

This invokes JWTGuard's logout method, which invalidates the token, resets the user, and unsets the token.

JWTGuard's Logout

/**
 * Logout the user, thus invalidating the token.
 *
 * @param  bool  $forceForever
 *
 * @return void
 */
public function logout($forceForever = false)
{
    $this->requireToken()->invalidate($forceForever);

    $this->user = null;
    $this->jwt->unsetToken();
}
HlaingTinHtun commented 8 years ago

@mtpultz Thanks. It works successfully

mtpultz commented 8 years ago

To keep the complete answer all in one place I've also added the logout to the original post of the "guide", also added a few changes with regards to the artisan commands.

belohlavek commented 8 years ago

@mtpultz I'm geting Method [handle] does not exist. with Lumen (using dev-develop) after following your instructions on how to configure auth.php. This was the result of trying to apply the Middleware to my routes group (using Dingo/Api). I guess that this happens because it's unable to find the middleware, but I really have no idea :sweat_smile:

Any clue why this happens?

Thanks for reading :muscle:

Edit: Here's a fragment of the trace returned with the error

"#0 [internal function]: Tymon\\JWTAuth\\JWTGuard->__call('handle', Array)",
"#1 /home/vagrant/Code/sistema-trabajos/vendor/illuminate/auth/AuthManager.php(288): call_user_func_array(Array, Array)",
"#2 [internal function]: Illuminate\\Auth\\AuthManager->__call('handle', Array)",
"#3 /home/vagrant/Code/sistema-trabajos/vendor/illuminate/pipeline/Pipeline.php(136): call_user_func_array(Array, Array)",
"#4 [internal function]: Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Dingo\\Api\\Http\\Request))",
belohlavek commented 8 years ago

Never mind, forgot to register the middleware, which is commented-out by default in Lumen. My bad :sweat:

antonioreyna commented 8 years ago

@mtpultz for that should i install the dev-master? using composer? thanks

mtpultz commented 8 years ago

Hi @antonioreyna, yah you'll need dev-master. That's the branch the currently has the JWTGuard class. If you look in /src of the master branch you'll see that there is no JWTGuard.php file, but switching to dev-master you'll see it.

Cheers

pouyaamreji commented 8 years ago

I'm sorry for this noobish question but i don't get it after step 6. where can i find AuthenticateUsers::login?

mtpultz commented 8 years ago

Hi @pouyaamreji,

The AuthenticatesUsers::login is a trait applied to the AuthController. It is actually a composite trait since it is a trait within the AuthenticatesAndRegistersUsers trait that you see being used by the AuthController. If you need to find these files, and your IDE doesn't provide a shortcut to indexed classes or methods, look at the use statement at the top of the file:

use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

This is the mapping to the file in your composer vendors file so you'd look inside:

vendors/larave/framework/src/Illuminate/Foundation/Auth

In there you'll find the AuthenticatesAndRegistersUsers class and if you open it up you'll see the AuthenticatesUsers class being used, and you'll also see the AuthenticatesUsers file in the same folder. Inside that is the login action you're trying to find.

adstechnetwork commented 8 years ago

Hi. I am working on getting the guard functionality working. If I install dev-master through Composer, the JWTGuard.php file is not included in source. If I install dev-develop, however, JWTGuard is included, but I get an error: Fatal error: Class 'Tymon\JWTAuth\Providers\JWTAuthServiceProvider' not found when I include it in app.php under service providers. Any suggestions would be most appreciated.

mr-feek commented 8 years ago

@adstechnetwork if you take a look at the providers in the src directory, you'll see there is a LumenServiceProvider and a LaravelServiceProvider now.

adstechnetwork commented 8 years ago

Thank you @feeekkk . That worked. However now, when I make a post to login I get the following reflection exception: Class Tymon\JWTAuth\Providers\JWT\NamshiAdapter does not exist.

Any thoughts?

mr-feek commented 8 years ago

@adstechnetwork one of these should help you https://github.com/tymondesigns/jwt-auth/search?q=NamshiAdapter&type=Issues&utf8=%E2%9C%93

adstechnetwork commented 8 years ago

@feeekkk thank you very much. That fixed that piece and I can keep moving forward. Most appreciated.

murbanowicz commented 8 years ago

Hi, ( EDITED: see on bottom) I don't know what I am doing wrong, but I am handling register,login,logout in User Controller. I am using Laravel 5.3.

I have two issues:

First issue I did copy AuthenticateUsers::login to UserController and changed it to

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 ($lockedOut = $this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        $credentials = $this->credentials($request);

        if ($token = Auth::guard($this->getGuard())->attempt($credentials)) {
            return $this->handleUserWasAuthenticated($request, $throttles, $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.
        if (! $lockedOut) {
            $this->incrementLoginAttempts($request);
        }

        return $this->sendFailedLoginResponse($request);
    }

$this->validateLogin and all others of course does not exist. What should I do?

Second issue In User model I have following code:

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Validator;

use Tymon\JWTAuth\Contracts\JWTSubject as AuthenticatableUserContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract,
    AuthenticatableUserContract
{
    use Notifiable, Authenticatable, Authorizable, CanResetPassword;

Authenticable is giving errors already in IDE.

All implements: Class must be declared abstract or implement methods sendPasswordResetNotification, getEmailForPasswordReset and Authenticatable - Trait expected, class found

Can someone help?

EDIT: I realized that I should use AuthenticatesUsers trait. I did add it, but now I am getting error: Method [getGuard] does not exist. in this line if ($token = Auth::guard($this->getGuard())->attempt($credentials)) { and now I really have no idea what should I do ? `

mtpultz commented 8 years ago

Hi @mariaczi, so this post is really for Laravel 5.2 since 5.3 was only released 3 days ago, but that said it should probably be updated for 5.3 so if I have time I might drop something in this weekend since I have a new project that needs to be started up. That said the gist is the same you have a token and you need to get it to the client so hopefully these points might help:

  1. You're moving an authentication action out from the trait and placing it in the UserController, which seems to suggest you might have a few misunderstandings regarding OOP (though your edit seems to suggest you figured that out), but aside from that you need to move AuthenitcatesUsers::login up into the LoginController, which uses the AuthenticatesUsers trait so it overrides the traits method. The UserController is not for authentication so for separation of concerns keep all authentication related actions within the controllers inside of /http/Controllers/Auth.
  2. Reviewing step 6 with regards to what is related to the JWTAuth package, which is collecting a token, and passing it along for the response you should look at JWTGuard::attempt() where you'll see it returns the token, which is collected and then should be passed on to sendLoginResponse since handleuserWasAuthetnicated doesn't exist anymore. You'll probably want to drop $request->session()->regenerate() in sendLoginResponse (but I'm just guessing right now), and then jump to step 8 and implement authenticated and add the $token param. This should give you a good start now that you can return the token.
  3. For your second issue I'd suggest reading the docs I think you'll find the answers you're looking for on how to setup the User model outside of applying the JWTAuth package.

Hope this helps

murbanowicz commented 8 years ago

@mtpultz I and probably others would be really grateful if you would find time for complete tutorial for 5.3 to let understand it correctly as 5.3 changed some main ideas behind.

EDIT: I moved login function to LoginController but again after fixing few errors - I got the same with Method [getGuard] does not exist. Can you advise ?

kylesean commented 8 years ago

@mtpultz Could you please show me your middleware file, you defined 'middleware' => 'auth:api', I don't know how to use the middleware . I write it like this:

<?php

namespace App\Http\Middleware;
use Closure;
use Tymon\JWTAuth\Facades\JWTAuth;
use Exception;
class authJWT
{
    public function handle($request, Closure $next)
    {
        try {
            $user = JWTAuth::toUser($request->input('token'));
        } catch (Exception $e) {
            if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException){
                return response()->json(['error'=>'Token is Invalid']);
            }else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException){
                return response()->json(['error'=>'Token is Expired']);
            }else{
                return response()->json(['error'=>'Something is wrong']);
            }
        }
        return $next($request);
    }
}

But it didn't work. It couldn't return the exception for json. Hope you can help me .Thanks.Happy Mid-Autumn Festival

mtpultz commented 8 years ago

Hi @kylesean, I actually don't use JWTAuth I use the JWTGuard in Laravel 5.2+ so I don't have to write any middleware like you are doing.

mabasic commented 8 years ago

Hi, I have written a lesson on getting this package working with Lumen 5.3 on my website JSON Web Token Authentication for Lumen REBOOT

mtpultz commented 8 years ago

I've added an updated version for getting JWTGuard working with Laravel 5.3.x #860.

galexth commented 8 years ago

I have such an error with laravel 5.2 BadMethodCallException in JWTGuard.php line 405: Method [handle] does not exist.

mtpultz commented 8 years ago

You might have to provide a bit more information like the version of JWT Auth that you're using, maybe the action that is calling JWTGuard, etc

alexlopezit commented 7 years ago

Ran into this issue as I have 2 users tables and needed the authentication to work on both, so I found this Setting the Guard Per Route in Laravel simple customization to use the "guard" in the routes.

Leaving it here as it's working fine for me and might work for others.