简体   繁体   中英

Laravel Passport's Password Grant Flow for First-party Apps

I'm using Laravel Passport to give access to some parts of my API to third-party apps.

But, I also use my own API through my own first-party Native Android App. So, I looked over the whole internet for the best practice in this case, but getting stuck to get to a conclusion.

Here are the possibilities I found:

Possibility #01

I can follow the User Credential Password Grant Flow .
In this case I need to pass a client_secret and client_id to the authorization server. In order to keep them safe I can't write them in my mobile application's source code (APKs are decompilable...).

So, I have 2 choices.

Possibility #01 - Choice A

Proxyfying through my own server and inject the secret before calling the oauth endpoint:

$proxy = Request::create('/oauth/token', 'post', [
    'grant_type' => 'password',
    'client_id' => 1,
    'client_secret' => 'myownclientsecretishere',
    'username' => $username,
    'password' => $password
]);
$proxy->headers->set('Accept', 'application/json');
$response = app()->handle($proxy);

Possibility #01 - Choice B

Inject the secret when calling the oauth endpoint using a Middleware:

class InjectPasswordGrantSecret
{
    public function handle($request, Closure $next)
    {
        $request->request->add([
            'client_id' => 1,
            'client_secret' => 'myownclientsecretishere'
        ]);
        return $next($request);
    }
}

These are working examples, but they are also greedy in resources.. I tried to use Apache benchmark on my local machine and I got something like 9 requests/second.

Possibility #02

I can follow the Personal Access Grant .
This one doesn't look like a standard in OAuth2 , it allows us to create a token through any custom route, just like this:

if (! auth()->attempt(compact('username', 'password'))) {
    return error_response(__('auth.failed'));
}
$user = auth()->user();
$token = $user->createToken(null)->accessToken;

Using Apache benchmark I get better result (something like 30 requests/second).

But, token lifetime is not configurable by default and is set to 1 year (note that there are workarounds to get this lifetime configurable using a Custom Provider).

I am really wondering if this solution is meant to be used in a production environment.

Initially, I used JWT tymon library because I only had my own app. But now that I need to get it to work with first-party AND third-party apps, I thought that OAuth2 (through Laravel Passport) would be a good solution...

I hope someone can help me out with this, and explain what could be a good solution to get it to work securely and [not slowly] on production servers.

OAuth2 is a standard but implementations done by people can sometimes be different, but in most cases it doesn't change anything at the end.
For example, using oauth/token or myserver/mytokenroute as route path won't change anything to what the route is supposed to do, as long as the token generation is still done in the same way. For another example, whether client_secret is required by the route or not is not important if it's safely provided (and not stored on client side).

So, if you need a nice control over your OAuth2 flows, you can make your own implementation of Laravel Passport routes.

First, you can remove Passport::route(); from AuthServiceProvider@boot method to remove access to Passport routes, and then create only the routes you will need (oauth/token for example).

Then, create your own controller extending Laravel\\Passport\\Http\\Controllers\\AccessTokenController in order to be able to use some functionalities provided by Laravel Passport . In this controller you can create as many methods as you need for your routes.
Here is an example with token route :

use Laravel\Passport\Http\Controllers\AccessTokenController;
use Laravel\Passport\TokenRepository;
use Lcobucci\JWT\Parser as JwtParser;
use League\OAuth2\Server\AuthorizationServer;
use Psr\Http\Message\ServerRequestInterface;

class MyOAuthController extends AccessTokenController
{
    public function __construct(AuthorizationServer $server, TokenRepository $tokens, JwtParser $jwt)
    {
        parent::__construct($server, $tokens, $jwt);
    }

    public function token(ServerRequestInterface $request)
    {
        $data = $request->getParsedBody();
        // You can inject your secrets here
        $data['grant_type'] = 'password';
        $data['client_secret'] = 'SECRET-HERE';
        $data['client_id'] = 'ID HERE';
        return parent::issueToken($request->withParsedBody($data));
    }
}

If you need, you can also store the result of parent::issueToken in an intermediate variable to use it instead of returning it to client.

Then, declare the route in your AuthServiceProvider@boot method :

Route::post('/myserver/mytokenroute', '\App\Http\Controllers\MyOAuthController@token');

Don't forget you can use middlewares, I would have use this to limit requests for a single client for example :

Route::middleware('throttle:10,1')->post('/myserver/mytokenroute', '\App\Http\Controllers\MyOAuthController@token');

With this approach you won't use proxy and I find it better than using Personal Access Token because you have a better control over all others flows (authorization code, refresh flow, etc..). If you need a specific behavior, just create a new method and link it to a route, keeping your source code clean and centralized in a single controller.

Here is the page that I always refer: https://oauth2.thephpleague.com/authorization-server/which-grant/ 在此处输入图片说明

It says

We strongly recommend that you use the Authorization Code flow over the Password grant for several reasons. We eliminate the password grant option.

Then, it says clearly in the diagram that you should use

Authorization Code Grant with PKCE

and also indicates that

If the client is a web application that has runs entirely on the front end (eg a single page web application) or a native application such as a mobile app you should implement the authorization code grant with the PKCE extension. You can read further in the document.

Additionally, there is a good tutorial here explaining every detail of the flow with an example: https://auth0.com/docs/architecture-scenarios/mobile-api

I hope these help.

PS: When the time I needed to authorize my users in my first party application, I had used password grant by referencing this very chart. However, it seems like it changed and password grant is now not a best practice anymore and is not recommended.

What I've done before is implemented my own controller that extends the Laravel\\Passport\\Http\\Controllers\\AccessTokenController . Not only can you set this controller up to handle creating the tokens, you can add in routes for refreshing and revoking the tokens, as well.

For example, I had a create method for showing the login form, a store method for creating the access token, a refresh method for using the refresh token to refresh expired access tokens, and a revoke method for revoking the provided access token and related refresh token.

I can't provide exact code since the project is not open source, but this is a quick example controller with just the store method:

use Illuminate\Http\Request;
use Psr\Http\Message\ServerRequestInterface;
use Laravel\Passport\Http\Controllers\AccessTokenController;

class MyAccessTokenController extends AccessTokenController
{
    public function store(Request $request, ServerRequestInterface $tokenRequest)
    {
        // update the request to force your grant type, id, secret, and scopes
        $request->request->add([
            'grant_type' => 'password',
            'client_id' => 'your-client-id',
            'client_secret' => 'your-client-secret',
            'scope' => 'your-desired-scopes',
        ]);

        // generate the token
        $token = $this->issueToken($tokenRequest->withParsedBody($request->request->all()));

        // make sure it was successful
        if ($token->status() != 200) {
            // handle error
        }

        // return the token
        return $token;
    }
}

The refresh method would basically be the same, but the grant_type would be refresh_token .

The revoke method would get the authed user's token ( $token = $request->user()->token() , revoke it ( $token->revoke() ), and then manually go through the oauth_refresh_tokens table for the related tokens ( access_token_id = $token->id ) and update the revoked field to true .

Create some routes to use your custom controller and you're good to go (NB: the revoke route will need the auth:api middleware).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM