14

In my Laravel app users can disable (not delete) their account to disappear from the website. However, if they try to login again their account should be activated automatically and they should log in successfully.

This is done with "active" column in the users table and a global scope in User model:

protected static function boot() {
    parent::boot();

    static::addGlobalScope('active', function(Builder $builder) {
        $builder->where('active', 1);
    });
}

The problem now is that those inactive accounts can't log in again, since AuthController does not find them (out of scope).

What I need to achieve:

  1. Make AuthController ignore global scope "active".
  2. If username and password are correct then change the "active" column value to "1".

The idea I have now is to locate the user using withoutGlobalScope, validate the password manually, change column "active" to 1, and then proceed the regular login.

In my AuthController in postLogin method:

$user = User::withoutGlobalScope('active')
            ->where('username', $request->username)
            ->first();

if($user != null) {
    if (Hash::check($request->username, $user->password))
    {
        // Set active column to 1
    }
}

return $this->login($request);

So the question is how to make AuthController ignore global scope without altering Laravel main code, so it will remain with update?

Thanks.

Trakus Ret
  • 311
  • 3
  • 8
  • 1
    I had a similar problem. I used this solution: http://stackoverflow.com/questions/34696134/laravel-global-scope-auth Create two separate User models. The first doesn't have the global scope, and is used for just authentication. The second extends the first, includes the global scope, and is used for everything else. – programmerKev Apr 21 '17 at 19:43
  • 1
    Correct me if I'm wrong but haven't you answered your own question here by using `withoutGlobalScope`? – user3574492 Jun 22 '18 at 21:28
  • Please refer the link: https://stackoverflow.com/a/59297932/6196907 – Shailesh Matariya Dec 12 '19 at 05:00

7 Answers7

1

Create a class GlobalUserProvider that extends EloquentUserProvider like below

class GlobalUserProvider extends EloquentUserProvider {

    public function createModel() {
         $model = parent::createModel();
         return $model->withoutGlobalScope('active');
    }

}

Register your new user provider in AuthServiceProvider:

Auth::provider('globalUserProvider', function ($app, array $config) {
     return new GlobalUserProvider($this->app->make('hash'), $config['model']);
});

Finally you should change your user provider driver to globalUserProvider in auth.php config file.

 'providers' => [
    'users' => [
        'driver' => 'globalUserProvider',
        'model' => App\Models\User::class
    ]
 ]
Sasan Farrokh
  • 184
  • 2
  • 12
  • This does not work. ```$model->withoutGlobalScope('active');``` fails because ```parent::createModel();``` returns a user object. ```withoutGlobalScope``` must be called on a query, though. – raphael Oct 30 '18 at 16:59
  • Works perfectly for me - helped avoid `Symfony\Component\Debug\Exception\FatalThrowableError: Maximum function nesting level of '256' reached, aborting!` error when global scope was applied to user model, which was checking if user was authenticated (and thus booting the user again). – Sam Jul 02 '19 at 15:43
  • 1
    Right, you have to override this function instead newModelQuery() – Eleazar Resendez Oct 02 '20 at 17:33
1
protected static function boot() 
{
    parent::boot();
    if (\Auth::check()) {
        static::addGlobalScope('active', function(Builder $builder) {
            $builder->where('active', 1);
        });
    }
}

Please try this for login issue, You can activate after login using withoutGlobalScopes().

Amit Kumar
  • 19
  • 4
1

@Sasan's answer is working great in Laravel 5.3, but not working in 5.4 - createModel() is expecting a Model but gets a Builder object, so when EloquentUserProvider calls $model->getAuthIdentifierName() an exception is thrown:

BadMethodCallException: Call to undefined method Illuminate\Database\Query\Builder::getAuthIdentifierName() in /var/www/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:2445

Instead, follow the same approach but override more functions so that the right object is returned from createModel().

getQuery() returns the builder without the global scope, which is used by the other two functions.

class GlobalUserProvider extends EloquentUserProvider
{
    /**
     * Get query builder for the model
     * 
     * @return \Illuminate\Database\Eloquent\Builder
     */
    private function getQuery()
    {
        $model = $this->createModel();

        return $model->withoutGlobalScope('active');
    }

    /**
     * Retrieve a user by their unique identifier.
     *
     * @param  mixed  $identifier
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveById($identifier)
    {
        $model = $this->createModel();

        return $this->getQuery()
            ->where($model->getAuthIdentifierName(), $identifier)
            ->first();
    }

    /**
     * Retrieve a user by their unique identifier and "remember me" token.
     *
     * @param  mixed  $identifier
     * @param  string  $token
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByToken($identifier, $token)
    {
        $model = $this->createModel();

        return $this->getQuery()
            ->where($model->getAuthIdentifierName(), $identifier)
            ->where($model->getRememberTokenName(), $token)
            ->first();
    }
}
Sam
  • 492
  • 5
  • 10
1

Sasan Farrokh has a right answer. The only thing not to rewrite createModel but newModelQuery and this will work

protected function newModelQuery($model = null)
{
    $modelQuery = parent::newModelQuery();
    return $modelQuery->withoutGlobalScope('active');
}
turalex
  • 11
  • 1
0

Extend the AuthController with the code you used in your OP. That should work.

public function postLogin(Request $request)
{
    $user = User::withoutGlobalScope('active')
                ->where('username', $request->username)
                ->first();

    if($user != null){
        if (Hash::check($request->password, $user->password)){
            $user->active = 1;
            $user->save();
        }
    }

    return $this->login($request);
}
Mike Harrison
  • 1,309
  • 12
  • 17
  • Does this work for later versions of Laravel? I tried finding 'postLogin' in the documentation, but couldn't find any. Furthermore it seems AuthController is now LoginController and RegisterController. – Eugene van der Merwe Jun 10 '19 at 10:20
  • The problem here is in the `auth` middleware it will see no auth user so you will be logged out – TheGeeky Jan 24 '21 at 13:41
  • @TheGeeky I'm not using the auth middleware here – Mike Harrison Jan 25 '21 at 22:45
  • In any other route, The `auth` middleware will use the scope you made and consider you a guest user – TheGeeky Jan 26 '21 at 09:41
  • Thats not true - I'm setting $user->active = 1 so that the next time they make a request it considers them an active user and will work with the auth middleware – Mike Harrison Jan 29 '21 at 18:31
0

I resolved it by creating the new package.

mpyw/scoped-auth: Apply specific scope for user authentication.

Run composer require mpyw/scoped-auth and modify your User model like this:

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Mpyw\ScopedAuth\AuthScopable;

class User extends Model implements UserContract, AuthScopable
{
    use Authenticatable;

    public function scopeForAuthentication(Builder $query): Builder
    {
        return $query->withoutGlobalScope('active');
    }
}

You can also easily pick Illuminate\Auth\Events\Login to activate User on your Listener.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        \Illuminate\Auth\Events\Login::class => [
            \App\Listeners\ActivateUser::class,
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

 

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;

class ActivateUser
{
    /**
     * Handle the event.
     *
     * @param  Illuminate\Auth\Events\Login $event
     * @return void
     */
    public function handle(Login $event)
    {
        $event->user->fill('active', 1)->save();
    }
}

 

mpyw
  • 5,526
  • 4
  • 30
  • 36
0

I had to use ->withoutGlobalScopes() instead

in order for it to work

Harry Bosh
  • 3,611
  • 2
  • 36
  • 34