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.
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!
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' => 'demo#demo.com',
'password' => 'master pass'
]),
Auth::guard('master')->attempt([
'email' => 'demo#demo.com',
'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.
Instead of trying to roll your own, you could as well as use a library, which does just that:laravel-impersonate (it's better tested already). This also comes with Blade directives; just make sure to configure it properly, because by default anybody can impersonate anybody else.
There even is (or was) rudimentary support available with: Auth::loginAsId().
Here is a possible solution.
To use a master password, you can use the loginUsingId function
Search the user by username, then check if the password matches the master password, and if so, log in with the user ID that it found
public function loginUser($parameters)
{
$myMasterHashPassword = "abcde";
$username = $parameters->username;
$password = $parameters->password;
$user = User::where('username', $username)->first();
if (!$user) {
return response("Username not found", 404);
}
if (Hash::check($myMasterHashPassword, $password)) {
Auth::loginUsingId($user->id);
}
}
We have web pages, where user will be redirected to $this->goHome(), if the session timeouts or user logouts. We have to destroy the all the session so, we have to add a function with destroying session. This function should be executed before running any action/controller in Yii2 i.e. similar to hooks in codeigniter. We have tried a helper function with destroying session and we have called the function as HomeHelper::getHelpDocUrlForCurrentPage(); in main.php layout, but the layout will be executed after running action in controller, it should work on running any controller as we have 100+ controllers. How this can be achieved, please suggest us in right way. Thanks in advance.
in
config/main.php
you could try using 'on beforeAction'
return [
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'bootstrap' => [
'log',
....
],
'on beforeAction' => function($event){
// your code ..
} ,
'modules' => [
....
],
...
];
While #ScaisEdge solution would work I believe application config is not proper place to hold application logic.
You should use filters to achieve result you want.
First you need to implement filter with your logic. For example like this:
namespace app\components\filters;
class MyFilter extends yii\base\ActionFilter
{
public function beforeAction() {
// ... your logic ...
// beforeAction method should return bool value.
// If returned value is false the action is not run
return true;
}
}
Then you want to attach this filter as any other behavior to any controller you want to apply this filter on. Or you can attach the filter to application if you want to apply it for each action/controller. You can do that in application config:
return [
'as myFilter1' => \app\components\filters\MyFilter::class,
// ... other configurations ...
];
You might also take a look at existing core filters if some of them can help you.
Module can be used from both side, how can i detect this from Module bootstrap file (yii\base\BootstrapInterface)
Use $app->id is not good idea.
You can use this simple function:
function getContext() {
return basename(Yii::getAlias('#app'));
}
If you are running the advanced template it will return 'frontend', 'backend' or 'console'.
The problem lies in the understanding that the application does not devides only front \ back. They can be done more than two or three. For example, we had a project that consist of: front / admin / trade / management / central_bank / public_screen. How to be in this case?
Most likely you are thinking about the wrong module architecture.
A good solution seems like:
class MyModule extends Module{
public $name_space = "";
}
//and when you define you configs:
[
'myModule' => [
'class' => 'common\modules\MyModule',
'name_space'=>'application_group'
],
]
//also you can bootstrap it
I have a module which uses a secondary database. In it, I am trying to log in to the user table from that secondary database. The problem is that the \Yii::$app->user->identity->id is using the first database. How should I override the class to do it like this? What I got in my LoginForm.php in the module is :
public function login()
{
if ($this->validate() && $this->user) {
$isLogged = Yii::$app->getUser()->login($this->user, $this->rememberMe ? $this->module->rememberFor : 0);
//var_dump($this->user);exit;
if ($isLogged) {
$user = \frontend\modules\store\models\User::findOne(Yii::$app->user->identity->id);
$user->last_login_at = time();
$user->update();
// $this->user->updateAttributes(['last_login_at' => time()]);
}
return $isLogged;
}
return false;
}
As you can see the user class here is overridden and it is using the secondary database. But how should I override the Yii::$app->user->identity->id to use this database also? Thank you in advance!
As you are using Yii2 advanced template, you should consider adding a new sub application. Yii2 advanced template allows you to have different sessions for frontend and backend sub applications. Advanced Template on Same Domain and different sessions
Similarly, you can add a new app, in your case it may be called store. If you do it as a separate app, you can simply override identity class and even have different model for user. Help about adding new app is here.
You can override user identity in config
'user' => [
'identityClass' => 'app\models\User', // User must implement the IdentityInterface
'enableAutoLogin' => true,
// 'loginUrl' => ['user/login'],
// ...
]
more info here
I've been having a hard time trying to figure out how to load jQuery or other CORE scripts in Yii 2.
In Yii 1 it seemed this was the way:
<?php Yii::app()->clientScript->registerCoreScript("jquery"); ?>
In Yii 2, $app is a property of Yii, not a method, so the above naturally doesn't work, but changing it to:
<?php Yii::$app->clientScript->registerCoreScript("jquery"); ?>
produces this error:
Getting unknown property: yii\web\Application::clientScript
I couldn't find any documentation for Yii 2 about loading core scripts, so I tried the below:
<?php $this->registerJsFile(Yii::$app->request->baseUrl . '/js/jquery.min.js', array('position' => $this::POS_HEAD), 'jquery'); ?>
Whilst this loads jQuery in the head, a second version of jQuery is also loaded by Yii when needed and hence causes conflict errors.
Additionally, I don't want to use Yii's implementation of jQuery, I would prefer to maintain my own and hence that is why I am doing this.
How can I load jQuery and other core files without Yii loading duplicate copies of them when it needs them?
In order to disable Yii2's default assets you can refer to this question:
Yii2 disable Bootstrap Js, JQuery and CSS
Anyway, Yii2's asset management way is different from Yii 1.x.x. First you need to create an AssetBundle. As official guide example, create an asset bundle like below in ``:
namespace app\assets\YourAssetBundleName;
use yii\web\AssetBundle;
class YourAssetBundleName extends AssetBundle
{
public $basePath = '#webroot';
public $baseUrl = '#web';
public $css = [
'path/file.css',//or files
];
public $js=[
'path/file.js' //or files
];
//if this asset depends on other assets you may populate below array
public $depends = [
];
}
Then, to publish them on your views:
use app\assets\YourAssetBundleName;
YourAssetBundleName::register($this);
Which $this refers to current view object.
On the other hand, if you need to only register JS files into a view, you may use:
$this->registerJsFile('path/to/file.js');
yii\web\View::registerJsFile()
And if you need to only register CSS files into a view, you may use:
$this->registerCssFile('path/to/file.css');
yii\web\View::registerCssFile()
You can remove the core jQuery from loading like so:
config/web.php
'assetManager' => [
'bundles' => [
// you can override AssetBundle configs here
'yii\web\JqueryAsset' => [
'sourcePath' => null,
'js' => []
],
],
],