Trakus Ret
Trakus Ret

Reputation: 311

Laravel authentication without global scope

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.

Upvotes: 14

Views: 4615

Answers (7)

Harry Bosh
Harry Bosh

Reputation: 3790

I had to use ->withoutGlobalScopes() instead

in order for it to work

Upvotes: 0

turalex
turalex

Reputation: 11

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');
}

Upvotes: 1

mpyw
mpyw

Reputation: 5754

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();
    }
}

 

Upvotes: 0

Sam
Sam

Reputation: 492

@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();
    }
}

Upvotes: 1

Mike Harrison
Mike Harrison

Reputation: 1393

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);
}

Upvotes: 0

Amit Kumar
Amit Kumar

Reputation: 19

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().

Upvotes: 1

Sasan Farrokh
Sasan Farrokh

Reputation: 190

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
    ]
 ]

Upvotes: 1

Related Questions