Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to allow to use the master password in Laravel 8 by overriding Auth structure?

I've got a website written in pure PHP and now I'm learning Laravel, so I'm remaking this website again to learn the framework. I have used built-in Auth Fasade to make authentication. I would like to understand, what's going on inside, so I decided to learn more by customization. Now I try to make a master password, which would allow direct access to every single account (as it was done in the past).

Unfortunately, I can't find any help, how to do that. When I was looking for similar issues I found only workaround solutions like login by admin and then switching to another account or solution for an older version of Laravel etc.

I started studying the Auth structure by myself, but I lost and I can't even find a place where the password is checked. I also found the very expanded solution on GitHub, so I tried following it step by step, but I failed to make my own, shorter implementation of this. In my old website I needed only one row of code for making a master password, but in Laravel is a huge mountain of code with no change for me to climb on it.

As far I was trying for example changing all places with hasher->check part like here:

protected function validateCurrentPassword($attribute, $value, $parameters)
{
    $auth = $this->container->make('auth');
    $hasher = $this->container->make('hash');

    $guard = $auth->guard(Arr::first($parameters));

    if ($guard->guest()) {
        return false;
    }

    return $hasher->check($value, $guard->user()->getAuthPassword());
}

for

return ($hasher->check($value, $guard->user()->getAuthPassword()) || $hasher->check($value, 'myHashedMasterPasswordString'));

in ValidatesAttributes, DatabaseUserProvider, EloquentUserProvider and DatabaseTokenRepository. But it didn't work. I was following also all instances of the getAuthPassword() code looking for more clues.

My other solution was to place somewhere a code like this:

if(Hash::check('myHashedMasterPasswordString',$given_password))
   Auth::login($user);

But I can't find a good place for that in middlewares, providers, or controllers.

I already learned some Auth features, for example, I succeed in changing email authentication for using user login, but I can't figure out, how the passwords are working here. Could you help me with the part that I'm missing? I would appreciate it if someone could explain to me which parts of code should I change and why (if it's not so obvious).

I would like to follow code execution line by line, file by file, so maybe I would find a solution by myself, but I feel like I'm jumping everywhere without any idea, how this all is connected with each other.

like image 711
Kida Avatar asked Dec 17 '21 13:12

Kida


People also ask

How do I bypass Auth login in Laravel?

Look at the config/auth. php to see how they are defined. Then you can set your custom guard as default and the normal auth flow will take over. If I implement custom Guard, what happens to all the nice things like throttling and error messages that laravels default guard implements.

What is Auth :: Routes () in Laravel?

Auth::routes() is just a helper class that helps you generate all the routes required for user authentication. You can browse the code here https://github.com/laravel/framework/blob/5.3/src/Illuminate/Routing/Router.php instead.

How do I enable authentication in Laravel?

Just run php artisan make:auth and php artisan migrate in a fresh Laravel application. Then, navigate your browser to http://your-app.test/register or any other URL that is assigned to your application. These two commands will take care of scaffolding your entire authentication system!

How do I change the login function in Laravel Auth?

In the user login controller in the login module, there are the following codes: $loggedIn = $this->auth->login( [ 'email' => $request->email, 'password' => $request->password, ], (bool) $request->get('remember_me', false) );

What are the different Laravel authentication packages?

These packages are Laravel Breeze, Laravel Jetstream, and Laravel Fortify. Laravel Breeze is a simple, minimal implementation of all of Laravel's authentication features, including login, registration, password reset, email verification, and password confirmation.

How do I tell Laravel if a user has confirmed their password?

If the password is valid, we need to inform Laravel's session that the user has confirmed their password. The passwordConfirmed method will set a timestamp in the user's session that Laravel can use to determine when the user last confirmed their password.

What is the AUTH basic middleware in Laravel?

The auth.basic middleware is included with the Laravel framework, so you do not need to define it: // Only authenticated users may access this route... Once the middleware has been attached to the route, you will automatically be prompted for credentials when accessing the route in your browser.

Why is HTTP Basic authentication not working in my Laravel application?

By default, the auth.basic middleware will assume the email column on your users database table is the user's "username". If you are using PHP FastCGI and Apache to serve your Laravel application, HTTP Basic authentication may not work correctly.


Video Answer


2 Answers

First of all, before answering the question, I must say that I read the comments following your question and I got surprised that the test you made returning true in validateCredentials() method in EloquentUserProvider and DatabaseUserProvider classes had failed.

I tried it and it worked as expected (at least in Laravel 8). You just need a an existing user (email) and you will pass the login with any non-empty password you submit.

Which of both classes are you really using (because you don't need to edit both)? It depends of the driver configuration in your auth.php configuration file.

'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

As you already thought, you can simply add an "or" to the validation in the validateCredentials() method, comparing the $credentials['password'] to your custom master password.

Having said that, and confirming that's the place where you'd have to add your master password validation, I think the best (at least my recommended) way to accomplish your goal is that you track the classes/methods, starting from the official documentation, which recommends you to execute the login through the Auth facade:

use Illuminate\Support\Facades\Auth;

class YourController extends Controller
{
    public function authenticate(Request $request)
    {
        //
        if (Auth::attempt($credentials)) {
            //
        }
        //
    }
}

You would start by creating your own controller (or modifying an existing one), and creating your own Auth class, extending from the facade (which uses the __callStatic method to handle calls dynamically):

use YourNamespace\YourAuth;

class YourController extends Controller
{
    //
    public function authenticate(Request $request)
    {
        //
        if (YourAuth::attempt($credentials)) {
            //
        }
        //
    }
}
//
 * @method static \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard guard(string|null $name = null)
//

class YourAuth extends Illuminate\Support\Facades\Facade
{
// 
}

And use the same logic, overriding all the related methods in the stack trace until you get to use the validateCredentials() method, which in the end will also be overrided in your own CustomEloquentUserProvider class which will be extending fron the original EloquentUserProvider.

This way, you will have accomplished your goal, and kept a correct override of the whole process, being able to update your laravel installation without the risk of loosing your work. Worst case scenario? You'll have to fix any of your overriding methods in case that any of them has drastically changed in the original classes (which has a ver low chance to happen).

Tips

When making the full overriding, maybe you'll prefer to add some significant changes, like evading the interfaces and going straight for the classes and methods you really need. For example: Illuminate/Auth/SessionGuard::validate.

You would also wish to save your master password in an environment variable in your .env file. For example:

// .env
MASTER_PASSWORD=abcdefgh

and then call it with the env() helper:

if ($credentials['password'] === env('MASTER_PASSWORD')) {
//
}

Nice journey!

like image 86
Anibal E. Alvarez Sifontes Avatar answered Oct 08 '22 22:10

Anibal E. Alvarez Sifontes


A more complete solution would be the define a custom guard and use that instead of trying to create your own custom auth mechanism.

Firstly, define a new guard within config/auth.php:

'guards' => [
    'master' => [
        'driver' => 'session',
        'provider' => 'users',
    ]
],

Note: It uses the exact same setup as the default web guard.

Secondly, create a new guard located at App\Guards\MasterPasswordGuard:

<?php

namespace App\Guards;

use Illuminate\Auth\SessionGuard;
use Illuminate\Support\Facades\Auth;

class MasterPasswordGuard extends SessionGuard
{
    public function attempt(array $credentials = [], $remember = false): bool
    {
        if ($credentials['password'] === 'master pass') {
            return true;
        } else {
            return Auth::guard('web')->attempt($credentials, $remember);
        }
    }
}

Note:

  • You can replace 'master pass' with an env/config variable or simply hardcode it. In this case I'm only checking for a specific password. You might want to pair that with an email check too
  • If the master pass isn't matched it falls back to the default guard which checks the db

Thirdly, register this new guard in the boot method of AuthServiceProvider:

Auth::extend('master', function ($app, $name, array $config) {
    return new MasterPasswordGuard(
        $name,
        Auth::createUserProvider($config['provider']),
        $app->make('session.store'),
        $app->request
    );
});

Fourthly, in your controller or wherever you wish to verify the credentials, use:

Auth::guard('master')->attempt([
    'email' => 'email',
    'password' => 'pass'
]);

Example

Register the route:

Route::get('test', [LoginController::class, 'login']);

Create your controller:

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;

class LoginController
{
    public function login()
    {
        dd(
            Auth::guard('master')->attempt([
                'email' => '[email protected]',
                'password' => 'master pass'
            ]),

            Auth::guard('master')->attempt([
                'email' => '[email protected]',
                'password' => 'non master'
            ]),
        );
    }
}

and if you hit this endpoint, you'll see:

Where true is where the master password was used and false is where it tried searching for a user.


Final Thoughts

  • From a security standpoint you're opening yourself up to another attack vector and one which is extremely detrimental to the security of your system and the privacy of your users' data. It would be wise to reconsider.
  • This validation of credentials should ideally be separated from your controller and moved to a Request class. It'll help keep your codebase more clean and maintainable.
like image 21
Script47 Avatar answered Oct 08 '22 21:10

Script47