dusterio / lumen-passport

Making Laravel Passport work with Lumen
MIT License
654 stars 141 forks source link

Thanks! #9

Closed PCoetzeeDev closed 7 years ago

PCoetzeeDev commented 8 years ago

Hi @dusterio

This looks exactly like what I've been looking for. Was kind of surprised when I saw Passport wasn't working with Lumen out of the box - seems like that would be the first stop for it :)

Anyway, I'm going to try out your package with the latest Lumen install on a production environment. We use a multi node setup, as I like to keep things separate and such. Please let me know if you need any help maintaining this project, code wise or anywhere else.

Thanks again!

DCdeBrabander commented 7 years ago

what @SturmDrangAltaar said, also going to try it in a production environment. :) Searching for a OAuth2 package for simple token authentication, this will provide that right?

please keep us posted.

thanks!

nmfzone commented 7 years ago

Is there any documentation how to use it in Lumen? Thank you.

DCdeBrabander commented 7 years ago

@nmfzone do you man https://github.com/dusterio/lumen-passport/blob/master/README.md ? Or what do you want to know, specifically?

nmfzone commented 7 years ago

@DCdeBrabander No, I mean the real implementation when use in Lumen. Like how to logging in when using Passport, etc. I think the implementation is slightly different with Laravel before. But, forget it, I've successfully implemented Passport using your package, thank you very much.

DCdeBrabander commented 7 years ago

@nmfzone Aah, well good you had no more problems :) for the record. It is @dusterio 's package, I'm just trying to help and get information in any way, since I'm implementing this package myself probably tomorrow. I just want to be sure if I need to know more before I try to implement this in a production environment.

DCdeBrabander commented 7 years ago

@nmfzone can you explain, in short, how you did it and especially where to keep an eye on. I only need to implement a simple oAuth process where clients with (pre-given) id+secret+token have to do requests and refresh there tokens

nmfzone commented 7 years ago

@DCdeBrabander Whoops, I'm sorry. I think you're the owner of this package, because your name starts with 'D' :smile: I think you could see my code here. My implementation located in LoginController. Fyi, I use Password grant.

DCdeBrabander commented 7 years ago

@nmfzone I will probably use 'Password Grant' aswell, since that approach matches our (client's) wishes the most. I have some problems setting it up though. Lib complaining about User model (and its dependencies), and bit of trouble finding the correct way to send all params to the Passport providers. I was now using Guzzle in Controller aswell. Can you eolaborate on how you came up with your solution? :)

nmfzone commented 7 years ago

@DCdeBrabander Whoops, I think it would be a long explanation. But, it's okay. First, what version of Lumen do you use?

nmfzone commented 7 years ago

"Lib complaining about User model (and its dependencies)" ? Could you provide some screenshot or maybe a bit of explanation?

@DCdeBrabander

DCdeBrabander commented 7 years ago

@nmfzone I use Lumen's latest version, 5.3.2. I'll come back on this tomorrow, when I am back at dev-codebase on my local machine at the office 👍 Hopefully I'll figure it out with your example :)

DCdeBrabander commented 7 years ago

@nmfzone After using your User model (and its db table), setting single testuser and sending following request to /oauth/token ,

--removed image--

I get the following response:

image

jeej But why do I have to create an user with email and password, seperate from client_id and client_secret? Can't the application create a client_id and secret from these credentials or..?

Also why does the route /oauth/token/refresh say that I'm unauthorized if I use it like https://laravel.com/docs/5.3/passport#refreshing-tokens says I should? image

I'm probably coming across as a noob, but implementing oAuth2 is new for me, and I need to do it well xD

nmfzone commented 7 years ago

@DCdeBrabander The client_id and client_secret are shared for all users. It means client_id and client_secret are "something" to communicate between your application and oauth2. So, It doesn't rely on a specific user.

If you want to create them for the specific user, it's called Personal Access Token. Of course, you can make it.

Hmm, the refresh token will not work because Passport refresh token relies on a session (see here), but Lumen doesn't support session anymore. If you read my code, I make my own implementation ('refresh-token' route). Hope that helps.

nmfzone commented 7 years ago

Comment updated.

DCdeBrabander commented 7 years ago

@nmfzone thank you for pointing me (assuming) in the right direction :) I will check Personal Access Token's today then. You think I can realise my wanted implementation via your code https://github.com/TeknikInformatikaUII/insta-dakwah-api/blob/master/app/Http/Controllers/Auth/LoginController.php ?

Btw, if you say IM'ing is better. Just say the word.

p.s. correct you used https://github.com/esbenp/lumen-api-oauth/blob/master/app/Auth/Proxy.php as reference? :D

nmfzone commented 7 years ago

@DCdeBrabander Wohoo, good catch! :smile: You're right, I use it as a reference. I've used it since the first time I implement OAuth2 in Lumen (5.1).

No no, I will not do that. It's just about time. It just because I implementing the OAuth2 earlier than you.

I don't know you "can" or not. But, if you're having trouble, It has been my pleasure to help you.

DCdeBrabander commented 7 years ago

@nmfzone Personal Access Token's wont work for my goal, they do not expire.

michaelmano commented 7 years ago

@DCdeBrabander you can set up a small script to remove them from the db if their date is past a certain timeframe. wouldn't be that hard. then again there are easier solutions. just depends how bad you need it.

DCdeBrabander commented 7 years ago

@michaelmano @nmfzone I have trouble implementing a process where seperate 'users'/clients can request (and refresh!) tokens and send requests with these (access) tokens. Everything I try in postman (when requesting an 'auth grouped route') returns as 'Unauthorized'. Even when I just 'logged in' (got tokens) and used access token in request. Am I doing something wrong? How do you guys test routes/endpoints where you need oauth2 authentication?

If I want to support multiple users requesting/refreshing tokens, and using these tokens to request 'authed' routes do I still need that cookie implementation? I'm a bit lost what approach I should take and what is missing/need to built seperately in every approach. Do I need Personal Access Tokens (with custom tinkering...) or Password Grant? I don't want a callback-step in de process.

I tried some stuff, but for the sake of helping, assume that I currently have no code/controllers/models myself anymore. @michaelmano I think you are right, I think I need a Password Grant ( officially a client_credentials grant?) and somehow invalidate these tokens myself?

My apologies if I come across as a noob, but I simply have no understanding how to implement OAuth2 in Lumen (and more importantly, what to write myself where needed). Help is much needed, thanks!

p.s. If after all this my understanding of the package and OAuth2 has improved a great deal, I'd like to help maintain these packages. I think Passport should be enabled by default in Lumen.

nmfzone commented 7 years ago

@DCdeBrabander Do you have clone my repo and try it? I just believe my implementation is what are you want.

I use password_grant (using username and password). Every user can make a request to the application to get their access token. And of course, every user can make a request for the refresh_token.

p.s. Why don't you use my code and forget about "how to" implementing OAuth2 in Lumen. And you just need to start to build your own API.

DCdeBrabander commented 7 years ago

@nmfzone That would be nice, but the functionality exists mainly in your logincontroller yes? If you could tell me how to call a route which uses the auth middleware (ones that are protected)? In other words, apparantly I have some trouble testing these routes in Postman, can you provide an example of such a call (with params)? If I can test your implementation that would be great. :D (since I can 'login' with your implementation).

And lastly, does your version really works with multiple refresh_tokens? You store them as cookie on server, these are saved seperately every login?

I need to 'know' how everything works, because I have to share the (routes of) API with 'outside' first party (/predefined) clients. So I need to test the (middleware of the) API as such :)

nmfzone commented 7 years ago

@DCdeBrabander First, Cookies aren't stored in the server. Cookies stored in the client browser. I think you must read first about it. I'm sorry, but It's very fundamental.

Secondly, my implementation isn't using "auth" middleware, but "auth:api" middleware. And all routes under "routes/api.php" are protected "auth:api" by default (look at the bottom bootstrap/app.php). You can see my example "how to access" routes that protected by "auth:api" in the video (look at the bottom Readme.md).

DCdeBrabander commented 7 years ago

@nmfzone Thanks for clearing that up but do I really need to store the refresh token in cookie at client? Can't I just send them the token and they need to provide it when refreshing access_token? I've seen your video, I tried that and via that approach postman (thus API) says I'm unauthorized.

Same middleware group: middleware

My Controller (at moment of writing almost identical to yours again): http://hastebin.com/zequtemuza.xml

So my login works (getting first tokens) works: --removed image--

But getting that auth route: --removed image--

(note, access_token differs over time because 'I logged in again before sending request to auth route)

Why am I unauthorized, I see access_tokens and refresh_tokens being made in DB?

nmfzone commented 7 years ago

@DCdeBrabander If you see my video correctly, you won't send Authorization via HTTP params. The authorization is sent in the header.

No, you don't really need to store the refresh_token in cookies, no. It's only my implementation. If you don't need to use cookies, you can remove all code outside the try catch (and remove the try catch too). Because the /oauth/token already gives you a complete response.

But, if you need a cookie, don't forget to install "illuminate/cookie" in your composer.json and setup it. 1 2 3

p.s. If you could show me all your code, maybe I can check then guide you.

nmfzone commented 7 years ago

@DCdeBrabander If you still get "Unauthorized", what web server are you using? Apache or NginX?

If you use Apache, please take a look at my .htaccess. You MUST add it to your .htaccess. Or you can copy from your Laravel's .htaccess.

But, if you still get unauthorized, hmm I think you aren't setup this package correctly or maybe you forget about some configuration.

DCdeBrabander commented 7 years ago

@nmfzone thanks for clearing up the authorization header, silly mistake. that in combination with the .htacess-edit did it!! I got a response instead of 'unauthorized'. :D Now I have to test and finish the refresh_token stuff (rather without cookies). I rather want more user-friendly errors when token is not valid anymore and stuff but we are somewhere now.

About the .htaccess, in production we have a nginx-environment maintained by other party. What do I need to give/say to them to make this work in the production environment?

And the cookies, I had all requirements set up, it works but just rather not needed. ;)

nmfzone commented 7 years ago

@DCdeBrabander haha, good. I'm glad to hear your code works.

If using NginX, you don't need to add special configuration. It works out-of-the-box. Because I use NginX, and I don't need to add special configuration.

DCdeBrabander commented 7 years ago

@nmfzone Glad to hear NginX will give no trouble :) That means you are almost done with me ;-)

I get errors trying to use the refreshtoken: When using normal params (apparantly wrong): {"error":"invalidrequest","message":"The refresh token is invalid.","hint":"Cannot decrypt the refresh token"} or when using form-data body: _{"error":"invalidrequest","message":"The refresh token is invalid.","hint":"Token is not linked to client"}

I'm calling my /token/refresh endpoint which calls refreshToken() in the controller where only this happens: return $this->access('refresh_token', [ 'refresh_token' => $request->refresh_token //instead of ->cookie( 'refresh_token' ) ]);

nmfzone commented 7 years ago

@DCdeBrabander Is your middleware still active? Don't forget to comment the middleware and all code that you don't use.

haha "almost done with me". Whoops, I do not think so. If I have the knowledge about "it", any time you can ask me.

nmfzone commented 7 years ago

@DCdeBrabander Aaah, I think you use the access_token instead refresh_token, so the passport says like that. Make sure you send "refresh_token", not "access_token".

DCdeBrabander commented 7 years ago

@nmfzone what do you mean with "Is your middleware still active?" Do you mean Middleware/Authenticate.php?

I use the default auth config:

    return [
        'defaults' => [
            'guard' => 'api',
            'passwords' => 'users',
        ],

        'guards' => [
            'api' => [
                'driver' => 'passport',
                'provider' => 'users',
            ],
        ],

        'providers' => [
            'users' => [
                'driver' => 'eloquent',
                'model' => \App\User::class
            ]
        ]
    ];

But for some reason it seems to work now, perhaps I was sending to much data / copy+pasted it wrong. I'll keep you posted. Thanks for all your help though!

nmfzone commented 7 years ago

@DCdeBrabander No, I mean middleware in "bootstrap/app.php".

Good. You're welcome.

p.s. Don't forget about "clean code" :smile:

DCdeBrabander commented 7 years ago

@nmfzone you mean the default: $app->routeMiddleware([ 'auth' => App\Http\Middleware\Authenticate::class, ]); ?

I have only one question left; What is the built-in way (if there is any) to revoke 'old' access tokens after refreshing? When user logs in / refreshes token, I want to revoke all old ones of that user (1 active at a time).

p.s. I won't forget :smile: :+1: All for clean, readable and maintainable code.

nmfzone commented 7 years ago

@DCdeBrabander No, I mean $app->middleware(). Forget it if you don't have it in your bootstrap/app.php.

All the access_token has been revoked automatically (by default) if someone requesting a new access token.

DCdeBrabander commented 7 years ago

@nmfzone I indeed have no $app->middleware() in my bootstrap/app.php.

I don't hope all access_tokens get revoked (of all users) when someone requests a new one? 😄

My goal of today is to determine / implement a way to differentiate each call from each user. I need to save (for example) user name if they send a request with only the access_token in Authorization header. Is this where the different client_id's come in?

nmfzone commented 7 years ago

@DCdeBrabander Whoops :laughing: I mean all the access_token for the specific user.

What do you mean? Do you mean how to get the user which make a request? You only need $request->user(). $request is Illuminate\Http\Request.

DCdeBrabander commented 7 years ago

@nmfzone I know what Illuminate\Http\Request is, not that noob with Laravel/Lumen. hehe 😄 still need to test multiple 'different' users but I indeed got user information via $request->user(), only had to use the Auth::user() in the past.

Also scopes started working (have 2 different type of users, the api 'owner' and rest):

// Define different 'scopes'.
// normally this is used for 'rights' instead of 'usertypes'
\Laravel\Passport\Passport::tokensCan([
    'reseller' => 'Reseller Token',
    'owner' => 'API Owner'
]);

And reading it via:

$r->user()->tokenCan('owner');
$r->user()->tokenCan('reseller');
DCdeBrabander commented 7 years ago

Just for information, I implemented the scopes like such, maybe it helps someone:

in a config file, i got the following:

'scopes' =>[
            'reseller' => 'Reseller Token',
            'owner' => 'API Owner'
        ]

which I load into \Laravel\Passport\Passport::tokensCan( config('oauth_client.scopes') );

what should be in line with users table: $table->enum('type', ['owner','reseller'])->comment('All 3rd party users should be \'reseller\' ');

with an extra check while requesting /login:

if( !in_array($User->type, array_keys(config('oauth_client.scopes'))) ){
    return $this->sendIncorrectScopeResponse();
}

and send scope in password grant to an access() or a proxy() method: 'scope' => $User->type

Currently I solved my route-groups as such, better suggestions are welcome (I tried putting it in middleware but didn't get it right..) :

....
    if( $request->user()->tokenCan('reseller') ){
        $app->group(['middleware'=>['auth:api']], function () use ($app) {

              ...routes for resellers only...
....

And that's it 😄 Because I have a fallback-route underneath routes file fetching everything what didn't get 'caught' by others it gives a JSON-response for 'invalid' users/tokens that route doesn't exist.

nmfzone commented 7 years ago

@DCdeBrabander haha, no no, I don't say it :smile: . I mean maybe someone will think about $request with Illuminate\Support\Facades\Request.

Just my two cents, I think tokenCan() better used in the controller (in every function) (like Taylor said 'maybe'). Or if you used it in routes, maybe you could make it as a custom middleware.

p.s. I use Bouncer, instead of token scopes.

DCdeBrabander commented 7 years ago

@nmfzone I don't have the time to use another package to take care of a simple user role 'problem'. But you are right, I changed the routes part to a Middleware class:

$app->routeMiddleware([
    'scope' => App\Http\Middleware\Scope::class,
]);
public function handle($request, Closure $next, $scope = null)
    {

        if( $scope !== '*' && !$request->user()->tokenCan($scope)){
            return Response::error(
               'You don\'t have the correct permissions to access this endpoint.',
               401
           );
        }

        // If request is good, proceed to the requested endpoint
        return $next($request);
    }

and in routes just; $app->group(['middleware'=>['auth:api','scope:*']].... or $app->group(['middleware'=>['auth:api', 'scope:owner']]....

Much cleaner solution this way. Also needed If you wan't 'correct' response instead everything falling back to a 'route not existing' response.

miansohailanjum commented 7 years ago

Does any one give an example how to use passport personal access tokens in lumen? And do we need User Model implementation? Like username and password.

dusterio commented 7 years ago

@miansohailanjum usually personal tokens are created by authenticated user himself, manually somewhere on the website. So it's more likely to be used in Laravel than in Lumen (because Lumen by default doesn't have sessions). You don't need username anywhere for access tokens - they rely on user numeric IDs, and password is only needed for password grant (if you are going to have it).

miansohailanjum commented 7 years ago

Thanks @dusterio. So what will be the best approach to use if i do not want to use user login and authorization code for my API?

dusterio commented 7 years ago

@miansohailanjum it's definitely doable, you just need to implement League\OAuth2\Server\Repositories\UserRepositoryInterface - it's a class (contract) with just one method, and re-bind Laravel Passport to it (you can see example in AccessTokenController.php in this package). Then you can issue your tokens for pseudo users, something internal

miansohailanjum commented 7 years ago

Thanks @dusterio. I will give it a try. Hopefully will be able to get through this.

Bpflugrad commented 7 years ago

@nmfzone I noticed in your previous post that you mentioned tokens should be automatically revoked when a new one is requested but that is definitely not happening for me. I have over 10 active tokens for my user in my DB now.

nmfzone commented 7 years ago

@Bpflugrad I don't really remember. But which version you're using? It has been so long, I think at that time I use Passport version 0.2.2.

Bpflugrad commented 7 years ago

@nmfzone It appears Lumen Passport brings with it laravel/passport >= 0.2.2. So I just did a bunch of tests. Route /oauth/token is defined to Dusterio\LumenPassport\Http\Controllers\AccessTokenController@issueToken. However, when I try to follow the code Laravel\Passport\Http\Controllers\AccessTokenController@issueToken is what gets called. I added a bunch of logging in places to find this. Dusterio\LumenPassport\Http\Controllers\AccessTokenController@issueToken has code to revoke old tokens if LumenPassport::$allowMultipleTokens is false, however since it is never being called this never happens. @dusterio do you know why this would be? Sorry if this is obvious and I just didn't install everything correctly :(

Bpflugrad commented 7 years ago

For those who may come searching for a solution to revoking tokens when a new token is issued I ended up writing a listener that does the job:

<?php
namespace App\Listeners;

use Laravel\Passport\Events\AccessTokenCreated;
use Laravel\Passport\Token;
use Laravel\Passport\Passport;
use Illuminate\Database\Connection;

class RevokeOldTokens
{

    /**
     * The database connection.
     *
     * @var \Illuminate\Database\Connection
     */
     protected $database;

    /**
     * Create the event listener.
     *
     * @param  \Illuminate\Database\Connection  $database
     * @return void
     */
    public function __construct(Connection $database)
    {
        $this->database = $database;
    }

    /**
     * Handle the event.
     *
     * Will revoke all unrevoked tokens for this userID and clientID, including the associated refresh tokens.
     *
     * @param  AccessTokenCreated  $event
     * @return void
     */
    public function handle(AccessTokenCreated $event)
    {
        $query = Token::where('user_id', $event->userId)->where('client_id', $event->clientId)->where('id', '<>', $event->tokenId)->where('revoked', false);

        $tokens = $query->get();

        foreach($tokens as $token) {
            if (Passport::$pruneRevokedTokens) {
                $this->database->table('oauth_refresh_tokens')
                    ->where('access_token_id', $token->id)->delete();
                $token->delete();
            } else {
                $this->database->table('oauth_refresh_tokens')
                    ->where('access_token_id', $token->id)->update(['revoked' => true]);
                $token->update(['revoked' => true]);
                $token->save();
            }
        }
    }
}
nmfzone commented 7 years ago

@Bpflugrad Yea, but >= 0.2.2 doesn't mean Lumen Passport using Laravel Passport version 0.2.2. But of course, you can tweak it so when the new token is issued, the older version should be revoked.