laravel / lumen-framework

The Laravel Lumen Framework.
https://lumen.laravel.com
MIT License
1.48k stars 419 forks source link

[5.5] Default auth guard called with custom one #767

Closed hakuno closed 6 years ago

hakuno commented 6 years ago

Description:

I have an auth:custom route middleware that calls an authentication custom guard.

But the application has its own default guard too.

So, my default guard and custom guard are being called in the same request.

The drivers are placed by Auth's viaRequest method in AuthServiceProvider class.

Steps To Reproduce:

config/auth.php

'defaults' => [
        'guard' => 'multitenancy',
    ],
'guards' => [
        'api' => ['driver' => 'api'],
        'multitenancy' => ['driver' => 'kerberos'],
    ],

routes/web.php

$router->group(['prefix' => 'client/v2', 'middleware' => 'auth:api', 'namespace' => 
...
$router->group(['prefix' => 'client/v2', 'middleware' => 'auth:multitenancy', 'namespace' => 
...
hakuno commented 6 years ago

The callers in the same request...

When the auth.defaults.guard is NOT equal to the ROUTE MIDDLEWARE

// The guard class itself
array(4) {
  [0]=>
  string(21) "App\Zen\KerberosGuard"
  [1]=>
  string(5) "check" // from GuardHelpers inheritance
  [2]=>
  string(4) "user"
  [3]=>
  string(21) "App\Zen\KerberosGuard"
}
// Service Container???
array(4) {
  [0]=>
  string(25) "Laravel\Lumen\Application"
  [1]=>
  string(23) "Laravel\Lumen\{closure}"
  [2]=>
  string(4) "user"
  [3]=>
  string(21) "App\Zen\KerberosGuard"
}

When the auth.defaults.guard is equal to the ROUTE MIDDLEWARE sounds fine however

// The guard class itself
array(4) {
  [0]=>
  string(21) "App\Zen\KerberosGuard"
  [1]=>
  string(5) "check" // from GuardHelpers inheritance
  [2]=>
  string(4) "user"
  [3]=>
  string(21) "App\Zen\KerberosGuard"
}

Ps. That IoC may be applied by Laravel\Lumen\Application

    protected function prepareRequest(SymfonyRequest $request)
    {
        if (! $request instanceof Request) {
            $request = Request::createFromBase($request);
        }

        $request->setUserResolver(function ($guard = null) {
            return $this->make('auth')->guard($guard)->user();
        })->setRouteResolver(function () {
            return $this->currentRoute;
        });

        return $request;
    }

So the default guard is called always by setUserResolver in prepareRequest. It is unabled to consider the given guard.

Plus, whenever the current guard is not equal to the default one, it isn't locked by this:

    public function user()
    {
        // If we've already retrieved the user for the current request we can just
        // return it back immediately. We do not want to fetch the user data on
        // every call to this method because that would be tremendously slow.
        if (! is_null($this->user)) {
            return $this->user;
        }
        //...

Certainly buggy.

hakuno commented 6 years ago

I've found 2 workarounds...

One of those is to make them alike (I meant custom as application default).

App\Http\Middleware\Authenticate::handle

// ...
$this->auth->setDefaultDriver($guard);
driesvints commented 6 years ago

I can't seem to reproduce this. Can you please share more code? Probably best if you set up a test repo somewhere which we can check out.

spacenate commented 5 years ago

FWIW, I ran in to this as well, in Lumen 5.8, PHP 7.3.

Some relevant details:

/config/auth.php:

'defaults' => [
    'guard' => env('AUTH_GUARD', 'app'),
],
'guards' => [
    'api' => ['driver' => 'token'],
    'app' => ['driver' => 'jwt'],
],

/app/Providers/AuthServiceProvider.php:

// Register a new callback based request guard, using API token.
$this->app['auth']->viaRequest('token', function ($request) {
    Log::info('Using API guard', ['route' => $request->path()]);
    $api_token = $request->input('api_token');
    if ($api_token && strpos($api_token, '::') !== false) {
        list($user_id, $api_token) = explode('::', $api_token);
        $user = User::find($user_id);
        if ($user && Hash::check($api_token, $user->api_token)) {
            return $user;
        }
    }
});

// Register a new callback based request guard, using JWT.
$this->app['auth']->viaRequest('jwt', function ($request) {
    Log::info('Using JWT guard', ['route' => $request->path()]);
    $jwt = app()->make('App\JWT');
    if ($jwt_header = $request->header('Authorization')) {
        $claims = $jwt->getValidClaims($jwt_header, true);
        $user = $claims ? User::find($claims->uid) : null;
        return $user;
    }
});

/routes/web.php:

$router->group([
    'prefix' => 'app'
    , 'middleware' => ['auth:app']
], function() use ($router) {

    $router->get('/', [
        'uses' => 'AppController@getAll'
    ]);

});

$router->group([
    'prefix' => 'api'
    'middleware' => ['auth:api']
], function() use ($router) {

    $router->get('/', [
        'uses' => 'ApiController@getAll'
    ]);

});

When I call app routes (using auth:app middleware, which is the default, as specified in config/auth.php), the JWT guard is called and authentication works fine.

When I call api routes (using auth:api middleware), the API guard is called, and then the JWT guard is called too, and when the request hits the controller, $request->user() is null.

Relevant portion from logs:

[2019-04-11 00:29:56] local.INFO: Using API guard {"route":"api/"} 
[2019-04-11 00:29:56] local.INFO: Using JWT guard {"route":"api/"} 
[2019-04-11 00:29:56] local.ERROR: Symfony\Component\Debug\Exception\FatalThrowableError: Call to a member function can() on null in /var/www/app/Http/Controllers/ApiController.php:27

Thank you @hakuno for sharing your Authenticate middleware workaround, this helped me!

For clarity, this is what I added in my Authenticate::handle() method:

if (substr($request->path(), 0, 4) === 'api/') {
    $this->auth->setDefaultDriver('api');
}