Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 18 days ago.
Improve this question
I am trying to improve my Laravel code design by moving code away from the controllers as much as possible.
In my AuthController I have a store function for logging in the user, where can I move the logic in this function?
So far I have implemented a FormRequest for validation and UserResource for the response fields. I was thinking of creating a file in /Services called AuthService and moving authentication logic there, unless there is a better design pattern? Or maybe logic for authentication can be moved to the User's model?
public function store(StoreAuthRequest $request)
{
$user = User::where('email', $request->input('email'))->first();
if(!$user || !Hash::check($request->input('password'), $user->password)) {
return response([
'message' => 'Bad credentials'
], 401);
}
$token = $user->createToken('AppToken')->plainTextToken;
return response([
'user' => new UserResource($user),
'token' => $token
], 201);
}
I would recommend to never mixup what a "service" is/means. In Laravel a Service (not a ServiceProvider, but a literal folder called Services in the app folder) is a class that allows the developer to communicate with external services, it is not a "logic" class that drives something, like logic for logging in.
What I have done, after working so many years with Laravel, is to "mix" a little bit of DDD (Domain Driven Development), what I have done is just add a Domain folder inside app, and you put ALL your logic in there.
For example, imagine we have a Doctors app, this would be the heriarchy:
app
Console
Domain
Exceptions
Http
Models
Providers
bootstrap
config
database
resources
etc.
So, intead of having stuff inside a app/Services, just put code inside Domain, like this (following our example):
Domain
Common (App\Domain\Common)
Enums
Events
Listeners
Jobs
Servicies
Doctor (App\Domain\Doctor)
Facility (App\Domain\Facility)
Patient (App\Domain\Patient)
So, the idea is to put true logic inside this Domain folder (and further organize them), so you can then NOT mix logic or stuff togheter.
To wrap it up, you would then have controllers like this:
class PatientController extends Controller
{
public function store(PatientRequest $request)
{
return \App\Domain\Patient\Entity::store(
$request->input('name'),
$request->input('email'),
$request->input('age')
);
}
}
All the logic for "registering" (manipulating something from the Patient) model, is on the Entity class.
I have also taken a different approach by following something similar to having "modules", so everything is in a "modules" folder and that logic is just in there. I got that idea from Ryuta Hamasaki: Modularising the Monolith, but this may be way more advance.
You can write the validator directly in the controller. Because this line of code has no possibility of being reused.
Example:
public function store(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
$user = User::query()
->where('email', $request->email)
->first();
// PHP 8.x
if (! Hash::check($request->password, $user?->password)) {
// Throw an exception
}
return response()->json([
'token' => $user->createToken(...)->plainTextToken,
'user' => new UserResource($user), // or UserResource::make($user)
])
}
End.
Related
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);
}
}
I have a Laravel web application (website.nl) AND an Laravel API (api.website.nl).
When the user logs in with the login form on the website. A post request is done to the API. When the login request is successfull a token AND User object is send back in the form of a JSON string. The login method looks like this.
public function login(Request $request)
{
$email = $request->input('email');
$password = $request->input('password');
$response = $this->client->request('POST', self::$apiRootUrl . '/login', [
'headers' => [
'Accept' => 'application/json',
],
'form_params' => [
'email' => $email,
'password' => $password
],
]);
$result = json_decode($response->getBody()->getContents());
if (isset($result->success->token))
{
$user = User::create([
'id' => $result->success->user->id,
'name' => $result->success->user->name,
'email' => $result->success->user->email,
'password' => $password
]);
Auth::login($user);
$this->apiToken = $result->success->token;
return $this->sendLoginResponse($request);
}
return $this->sendFailedLoginResponse($request);
}
Where I want this login function to be as stupid as possible, meaning, let the API figure out if this is a valid login request. This method only passes the arguments.
The main website should not have a database. It only uses the API to login, get a token, and do requests with that token to gather data for the user to see on the main website's dashboard.
For this reason my question is this: How can I store the User object retrieved from the API in such a manner that it will be query-able like an Eloquent model. But without the need to store it in a database. Again, the main website shouldn't need a database. It only communicates with the API for it's data.
I hope this question makes sense, if not, please ask for more details.
I don't want to create a duplicate database with migrations on the main website. Because that's the job of the .api subdomain. Also, all the models are used within a session. Never being stored for a longer time, because that's also located in a store method on the .api subdomain.
This would require you to write your own grammar for Eloquent, an ActiveRecord implementation for the API you're using.
In theory it's possible, in practice it's a lot of work.
Take a look here for how Laravel implements the database types it currently supports.
https://laravel.com/api/5.8/Illuminate/Database/Schema/Grammars.html
I defined an option in my website named "define user". So I need to do some changes in the current laravel registration system. Actually I did what I need:
public function register(Request $request)
{
$this->validator($request->all())->validate();
event(new Registered($user = $this->create($request->all())));
if ( !isset($request->registered_by_id) ) { -- added
$this->guard()->login($user);
$status = ""; -- added
$msg = ""; -- added
} else { -- added
$status = "define-user-alert-success"; -- added
$msg = "the user registered successfully";
} -- added
return $this->registered($request, $user)
?: redirect($this->redirectPath())
->with($status, $msg); -- added
}
As you know, function above is located under /vendor/laravel/framework/src/illuminate/Foundation/Auth/RegisterUsers.php path. The problem is all the change I made will be ignored when I upload my website and execute composer install command.
Any idea how can I keep my changes in the vendor directory?
composer install will overwrite your changes as it just fetches the latest version from the public repo and installs that.
I would suggest one of the following;
Create your own fork of laravel and have composer load this over default laravel. I did this recently with a Symfony component fork, the idea is to change the repo branch name to your modified one and override the source with your own repo. Instruction from my fork are here.
Upload the file manually via after executing composer install (not recommended, only use a stop-gap).
Override/extend the original class, this answer lays out the process nicely.
As defined in this answer on Laracasts (which is very similar to your case), use event listeners to execute your code after user registration.
Hope this helps!
I would strongly recommend against making any changes to the core framework - aside from the issue you mentioned, it can also make upgrades extremely difficult.
Fortunately, Laravel makes user registrations easy. All you need to do is create a new controller (E.g. UserController) and then use a function like this to create a model for them ...
public function registerUser(Request $request){
$request->validate([
'username' => 'bail|required|unique:users,username|max:255',
'first_name' => 'required',
'last_name' => 'required',
'email' => 'required|email'
]);
$user = User::create([
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'username' => $request->username,
'password' => Hash::make($request->password),
'email' => $request->email
]);
return redirect('/settings/people/'.$user->user_id);
}
The key here is to use the hash facade to encrypt the password before committing the model to the database. Otherwise, it's essentially like working with any other model.
Yea, don't edit the file in vendor.
This trait is used in the RegisterController which exists in your app, not the framework. You can simply override that method in your controller, done.
Perhaps your changes can go in registered which is called after registration so you don't have to override this functionality of register itself (since registered is a blank method waiting to be overridden)
Here's the code from my AuthController:
public function postRegister(Request $request)
{
$this->validate($request, [
'name' => 'required|min:3',
'email' => 'required|email|unique:users',
'password' => 'required|min:5|max:15',
]);
}
If the validation fails I'm getting redirected to the previous page. Is there a way to pass additional data along with the input and the errors (which are handled by the trait)?
Edit: Actually, the trait does exactly what I want, except the additional data I want to pass. As #CDF suggested in the answers I should modify the buildFailedValidationResponse method which is protected.
Should I create a new custom trait, which will have the same functionality as the ValidatesRequests trait (that comes with Laravel) and edit the buildFailedValidationResponse method to accept one more argument or traits can be easily modified following another approach (if any exists)?
Sure you can, check the example in the documentation:
http://laravel.com/docs/5.1/validation#other-validation-approaches1
Using the fails(); method, you can flash the errors and inputs values in the session and get them back with after redirect. To pass other datas just flash them with the with(); method.
if ($validator->fails()) {
return back()->withErrors($validator)
->withInput()
->with($foo);
}
I've been developing web apps using Yii 1.1.14 so far, but now it's time for an upgrade.
The company where I work has developed its own Access Control system, and I was really OK with it until I saw what it was really like... A combination of 8 tables in the database (not counting the users table), with a bunch of foreign keys.
1 table for controllers
1 table for the actions
1 table for the menu categories
1 table for types of users
And the other tables basically just connect 2 or 3 of those tables at a time.
It works well, but in my point of view it's highly time consuming to maintain all those tables, and at some point, when your application goes online, if it hits a certain amount of users it could get really slow. specially because 2 of those tables have the user's table primary key as foreign key.
So I've decided that, when I start developing on Yii 2, I'm going to start using RBAC, so I started looking for tutorials online... Only finding many different versions of the same code with author's role, and permissions for create or update posts.
I found a combination of 5 videos on Youtube, but they are about Yii 1 RBAC. They were helpful because I managed to understand most of RBAC's functionality, but I still have some doubts that I'll
enumerate below. And keep in mind that for this Access Control system I'm using the DBManager class.
My Doubts
Yii 1's RBAC used to have 3 tables: auth_assignment, auth_item and auth_item_child. Now in Yii 2 RBAC, a new table appears that is called auth_rule and I still don't understand what that specific table is doing there, how to use it or how to populate it.
I see that it's possible to restrict the user's access to some actions by using the controller's behavior method, and assigning access to some actions depending on the user's role, but when it comes to this I have to split my question into 2:
2.1. First: If you can just restrict the access to actions by setting it up in the behaviors method, then what's the use of saving permissions to the auth_item table?
2.2. Second: If you DO decide to control access according to permissions, then how exactly do you do it, because I find myself writing the following type of code inside of every function and I don't think using RBAC is supposed to be this tedious. There has to be another way.
public function actionView($id)
{
if(Yii::$app->user->can('view-users')){
return $this->render('view', [
'model' => $this->findModel($id),
]);
}else{
#Redirect to a custom made action that will show a view
#with a custom error message
$this->redirect(['//site/notauthorized']);
}
}
Because of the Access Control System that we use right now, when a user logs in, a complex query is executed that will end up returning an array that will be saved as a session variable, and will be used to create a menu with as many dropdownlists as menu categories, that the controllers that the user has access to belong to. How can this be done with RBAC?
I can only really answer 2.2 of your question, as 3 doesn't sound at all like something an RBAC should do. You could, however, get the information you needed from the rules table most likely, provided you followed a naming convention that matched your controllers or actions.
On to answering 2.2 though:
You can simply set the behavior like such:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'actions' => ['view'],
'roles' => ['view-users'], //<-- Note, rule instead of role
],
]
]
}
This doesn't solve a different problem of 'view-own-users' style permissions, as this needs to inspect the ActiveRecord model (well, at least it does in my application). If You want to achieve this, take a look at my post in the Yii forums here:
http://www.yiiframework.com/forum/index.php/topic/60439-yii2-rbac-permissions-in-controller-behaviors/#entry269913
I use it in one of the simplest method,I use them in the behaviours of my controller.
public function behaviors()
{
return [
'access' => [
'class' => \yii\filters\AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['sysadmin'],
'actions' => ['index','view','update'],
],
[
'allow' => true,
'roles' => ['staff'],
'actions' => ['index','create','update','view'],
],
],
],
];
}
Here roles are the one created in the auth-item table in the database and they have been assigned for users in auth-assignment table. In the behaviours we just use it as above. In the above code sysadmin can have access to index, view and update action, whereas staff can have access to index,create, update and view action.
Yii2 needs a little setup when it comes to using RBAC under your controllers AccessControl. I got around it by making my own AccessRule file.
namespace app\components;
use Yii;
class AccessRule extends \yii\filters\AccessRule
{
protected function matchRole($user)
{
if (empty($this->roles)) {
return true;
}
foreach ($this->roles as $role) {
if(Yii::$app->authManager->checkAccess($user->identity->code, $role))
return true;
}
return false;
}
then in your controller u can use something like this:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'ruleConfig' => [
'class' => 'app\components\AccessRule'
],
'rules' => [
[
'actions' => ['index', 'resource-type'],
'allow'=> true,
'roles' => ['admin'],
],
],
],
];
}
Where admin is defined as a auth_item and the user is in the auth_item_assignments.
As I have created a new Rbac system for yii2. you can direct permission for a action and action will show you are not authorisez for this action.
By this you find that you will only provide access for action that need to identify.
I uploaded my detail here you can find lot of solution here.
This is the best solution i could come up with when facing the need to filter access by permissions, it's bothersome but can be useful if you're trying to create roles in a productive enviroment and want to use rbac.
use yii\web\ForbiddenHttpException;
if(Yii::$app->user->can('view-users')){
return $this->render('view', [
'model' => $this->findModel($id),
]);
}else{
throw new ForbiddenHttpException('You dont have access to this site');
}