Passport: Auth events not fired

Created on 5 Jun 2017  路  9Comments  路  Source: laravel/passport

I'm developing a multi-tenant application that requires to catch authenticated requests to determine the context according with the logged user and to archive it I'm created a shared object across the application. We determined to use Passport with Password grant type as authentication strategy for SPA.

The problem is when a API request is made with passport the Auth events are not triggered. Looking the SessionGuard, it is done here:

if (! is_null($id)) {
   if ($user = $this->provider->retrieveById($id)) {
      $this->fireAuthenticatedEvent($user);
   }
}      

But looking in TokenGuard of Passport, these events are not triggered anytime.

I think these events are basic for any application and only Laravel\Passport\Events\AccessTokenCreated and Laravel\Passport\Events\RefreshTokenCreated are not enough.

enhancement

Most helpful comment

I have the same issue. I worked around it by replacing the auth middleware Illuminate\Auth\Middleware\Authenticate with a slightly modified version:

// App/Http/Middleware/Authenticate.php
...
class Authenticate extends AuthenticateBase
{
    protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            return $this->auth->authenticate();
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                $res = $this->auth->shouldUse($guard);

                // fire auth event for passport/api guard
                if ($guard === 'api') {
                    event(new Authenticated($this->auth->guard($guard)->user()));
                }
                return $res;
            }
        }

        throw new AuthenticationException('Unauthenticated.', $guards);
    }
}

The event is intentionally fired after the shouldUse($guard) call as this ensures that Auth::user() already points to the user authenticated through passport. This would not be the case when dispatching the event in the TokenGuard as it's done in the PR #568

The auth middleware alias has to be changed to this class.

// App/Http/Kernel.php
...
    protected $routeMiddleware = [
        // 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth' => \App\Http\Middleware\Authenticate::class,
...

This matches the guard by the alias defined in config/auth.php. This is usually api for the passport driver.

All 9 comments

Just ran into this issue as well. I use those events to prepare authenticated API calls to an external API with tokens I have stored in my database...

I have the same issue. I worked around it by replacing the auth middleware Illuminate\Auth\Middleware\Authenticate with a slightly modified version:

// App/Http/Middleware/Authenticate.php
...
class Authenticate extends AuthenticateBase
{
    protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            return $this->auth->authenticate();
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                $res = $this->auth->shouldUse($guard);

                // fire auth event for passport/api guard
                if ($guard === 'api') {
                    event(new Authenticated($this->auth->guard($guard)->user()));
                }
                return $res;
            }
        }

        throw new AuthenticationException('Unauthenticated.', $guards);
    }
}

The event is intentionally fired after the shouldUse($guard) call as this ensures that Auth::user() already points to the user authenticated through passport. This would not be the case when dispatching the event in the TokenGuard as it's done in the PR #568

The auth middleware alias has to be changed to this class.

// App/Http/Kernel.php
...
    protected $routeMiddleware = [
        // 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth' => \App\Http\Middleware\Authenticate::class,
...

This matches the guard by the alias defined in config/auth.php. This is usually api for the passport driver.

I agree that a few extra events might be useful but I wouldn't re-use the existing events from Laravel. These events would get fired a lot, for every single time an api request was made so this might lead to unwanted side effects. But adding a few more events might be useful yeah. I'll mark this as a feature request. Feel free to send in a PR.

Just ran into this problem as well. I'd be keen to work on a PR @driesvints but i'm struggling to see where in the code alot of things are happening.

It seems like the auth happens here in Laravel\Passport\Bridge\UserRepository

public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        $provider = config('auth.guards.api.provider');

        if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
            throw new RuntimeException('Unable to determine authentication model from configuration.');
        }

        if (method_exists($model, 'findForPassport')) {
            $user = (new $model)->findForPassport($username);
        } else {
            $user = (new $model)->where('email', $username)->first();
        }

        if (! $user) {
            return;
        } elseif (method_exists($user, 'validateForPassportPasswordGrant')) {
            if (! $user->validateForPassportPasswordGrant($password)) {
                return;
            }
        } elseif (! $this->hasher->check($password, $user->getAuthPassword())) {
            return;
        }

        return new User($user->getAuthIdentifier());
    }

But i've searched high and low can't find getUserEntityByUserCredentials called from anywhere. I'm guessing it's called via a string somewhere which is my IDE and Github search aren't picking it up.

The easiest thing would be to fire a success event and a failed event based on the response from this method.

Does anyone know where getUserEntityByUserCredentials is called from?

@robjbrain This method is actually an override which is called by League\OAuth2\Server\Grant\PasswordGrant on line 94

Hope this helps!

@jbbr Your solution is for successful login, correct?
do you know how can I fire an event at the Auth Attempt? like the IlluminateAuth\EventsAttempting.
I am trying to make access logs that include invalid login

do I have to override the Passport::routes and add a middleware for it?

@jbbr Your solution is for successful login, correct?
do you know how can I fire an event at the Auth Attempt? like the IlluminateAuth\EventsAttempting.
I am trying to make access logs that include invalid login

do I have to override the Passport::routes and add a middleware for it?

I exactly did what I was planning, which is add a middleware. here is my answer in SO

But I really prefer handling it with events.

I just ran into this as well. I found the easiest workaround with staying in the Laravel-y way is to add an observer on the Laravel\Passport\Token model (and refresh token, if you are wanting that too) and putting my code in the created method (for my app I wanted to keep track of the first and last login as well as revoke any other tokens):

<?php

namespace App\Observers;

use App\AppLogin;
use Laravel\Passport\Token;

class AccessTokenObserver
{
    public function created(Token $token)
    {
        $user = $token->user;
        AppLogin::updateOrCreate([
            'user_id'=>$user->id,
        ], [
            'updated_at'=>now(),
        ]);
        // Revoke any other access tokens
        Token::where('user_id', $token->user_id)
            ->where('id', '!=', $token->id)
            ->update(['revoked'=>true]);
    }
}

Hello everyone. As we won't be working on this ourselves I'm closing this issue. However, we are open to PRs to anyone who wants to work on this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rudolfdobias picture rudolfdobias  路  3Comments

seriousjelly picture seriousjelly  路  3Comments

cookiejarblush picture cookiejarblush  路  4Comments

SwiTool picture SwiTool  路  3Comments

Patskimoto picture Patskimoto  路  3Comments