Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Laravel 5.2 Custom Authentication Guard and Driver #513

Closed
mtpultz opened this issue Feb 16, 2016 · 44 comments
Closed

Feature: Laravel 5.2 Custom Authentication Guard and Driver #513

mtpultz opened this issue Feb 16, 2016 · 44 comments

Comments

@mtpultz
Copy link

mtpultz commented Feb 16, 2016

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
Copy link

+1

@tdhsmith
Copy link
Contributor

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
Copy link
Author

mtpultz commented Feb 16, 2016

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
Copy link
Author

mtpultz commented Feb 16, 2016

@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
Copy link
Contributor

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
Copy link
Contributor

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:

  • There're some interesting questions on how the auth provider should be adapted, because right now there is an odd cycle in the call stack JWTGuard --> JWT --> auth provider --> JWTGuard --> user provider. In any case, I think it's probably dangerous to keep using the same provider class, Illuminate, for both guards. The guards inherently behave different and we shouldn't limit ourselves by expecting them to both conform to one provider. (I've argued previously that I don't think we should keep the once method names in JWTGuard because they imply an alternative that doesn't exist in a stateless auth system, but that might only be opinionated bikeshedding on my behalf.)
  • In an ideal world, there would be an sibling repo to this that has example applications for different Laravel and Lumen versions and can also double as a base for functional tests. A number of recent issues, including the Lumen problems that eventually sparked the guard, have been caused by situations that couldn't really be detected by pure unit tests, so having whole app architectures to run would really help with some of the subtler specification/functionality issues.

@tymondesigns
Copy link
Owner

@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
Copy link
Contributor

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! 👍

@daviestar
Copy link

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

@mtpultz
Copy link
Author

mtpultz commented Feb 19, 2016

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',
    ],
],
  1. /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');
    });
});
  1. /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);
    }

    ...
}
  1. 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());
}
  1. 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
    ]);
}
  1. 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,
    ]);
}
  1. 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 [];
    }
}
  1. Test out whether authentication works by hitting the /login route with Postman by setting the header to key: Authorization, value: Bearer TOKEN_STRING

  2. 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
Copy link
Contributor

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

@mtpultz
Copy link
Author

mtpultz commented Mar 9, 2016

@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
Copy link

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
Copy link

+1

@ghost
Copy link

ghost commented Apr 18, 2016

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

@3amprogrammer
Copy link

3amprogrammer commented Apr 21, 2016

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
Copy link
Owner

@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
Copy link

@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
Copy link

Can you show logout function in auth controller? @mtpultz

@fer-ri
Copy link

fer-ri commented Apr 29, 2016

@mtpultz thats really great .. also need logout example 👍

@mtpultz
Copy link
Author

mtpultz commented May 4, 2016

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
Copy link

@mtpultz Thanks. It works successfully

@mtpultz
Copy link
Author

mtpultz commented May 9, 2016

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
Copy link

belohlavek commented May 22, 2016

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

Any clue why this happens?

Thanks for reading 💪

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
Copy link

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

@antonioreyna
Copy link

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

@mtpultz
Copy link
Author

mtpultz commented May 28, 2016

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

@mtpultz
Copy link
Author

mtpultz commented Jun 4, 2016

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
Copy link

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
Copy link

mr-feek commented Jun 5, 2016

@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
Copy link

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
Copy link

mr-feek commented Jun 6, 2016

@adstechnetwork
Copy link

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

@murbanowicz
Copy link

murbanowicz commented Aug 26, 2016

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
Copy link
Author

mtpultz commented Aug 26, 2016

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
Copy link

murbanowicz commented Aug 27, 2016

@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
Copy link

kylesean commented Sep 14, 2016

@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
Copy link
Author

mtpultz commented Sep 19, 2016

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
Copy link

mabasic commented Sep 26, 2016

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
Copy link
Author

mtpultz commented Sep 28, 2016

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

@galexth
Copy link

galexth commented Oct 27, 2016

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

@mtpultz
Copy link
Author

mtpultz commented Oct 27, 2016

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
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests