This might sound like an antipattern or a weak system design, but the client of my app has demanded that there can be multiple users with same email address.
So I added another unique column named username to the users table and removed ->unique() constraint from email column.
Registration, Login are working fine but the problem arises during the password reset.
Consider the scenario:
username - johndoe, email - john#example.com
username - janedoe, email - john#example.com
username - jimmydoe, email - john#example.com
If any one of them makes a request for a password reset link, they would have to use johndoe#example.com as their email. So which user's password is actually going to be reset when they click on reset link from mail? Turns out, the first user, in this case, johndoe. Even if the request was made by janedoe or jimmydoe.
So how do I reset password for a single username, rather than an email? What changes should I make in the ForgotPasswordController and/or ResetPasswordController controllers to solve this? Or, do I have to make changes in the core framework? If so, where and how?
Tested in Laravel 5.3 [This answer modifies some core files(you may override it if capable) and it's not a clean solution.]
Ask user for the unique username value instead of email on password forget form.
Override the sendResetLinkEmail() method in ForgotPasswordController.php as folows. [Originally written in SendsPasswordResetEmails.php].
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
$response = $this->broker()->sendResetLink(
$request->only('username')
);
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($response)
: $this->sendResetLinkFailedResponse($request, $response);
}
you would also need to override the validateEmail() method.
protected function validateEmail(Request $request)
{
$this->validate($request, ['username' => 'required']);
}
Add username field instead of email on password reset form.
Override rules() in ResetPasswordController.php to over come the email field change.
protected function rules()
{
return [
'token' => 'required',
'username' => 'required',
'password' => 'required|confirmed|min:6',
];
}
Also override the credentials() in ResetPasswordController.php
protected function credentials(Request $request)
{
return $request->only(
'username', 'password', 'password_confirmation', 'token'
);
}
Update or override the getEmailForPasswordReset() method in Illuminate\Auth\Passwords\CanResetPassword.php to the folowing.
public function getEmailForPasswordReset()
{
return $this->username;
}
Laravel uses key-value pair to find the user and send email. If you pass 'username => 'xyz' it will look for the first record with value 'xyz' in username field.
Note: The unique column in users table is expected as username.
Illuminate\Auth\Passwords\CanResetPassword.php is a trait, and I was not able to overide the getEmailForPasswordReset method, so i just modified the core file itself.
This might sound like an antipattern or a weak system design, but the client of my app has demanded that there can be multiple users with same email address.
Then you need to rewrite this feature and ask user for some more unique information, no matter what it is going to be. Laravel provided password reset expects email to be unique and with your current design it won't work. There's no magic here. You you cannot disambiguate your user using non unique data.
You will need to rework some things for this, but I feel like the user experience is better. Generate a unique key for each user (for data hiding). There is a helper method for creating unique keys.
Then, when the email is sent out, link the button to a route that utilizes this key.
Then, modify or create that route that points to the reset password controller. You would then know which user it was referring to.
Remove the need for the user to insert their password because you'd already know who it was.
Related
I'm doing a small project. I have a database that is meant to verify logins. I'm creating an Admin Account with my own email, I have the bog-standard error messages of:
"Unregistered Email"
"Password or Email Incorrect"
"Missing Password"
etc
When I enter JUST my email, my login page recognizes it and doesn't throw an error message (so I know it's accessing my database correctly).
When I enter my password, the login page declares the email or password is incorrect.
I know it's recognizing my email, so it's the password that's incorrect, but I also KNOW it's correct (I typed it manually into my Table Plus database.)
Images included below: (This bug is killing me, help would be EXTREMELY appreciated)
Email being recognized first picture.
My database entries second picture.
Third picture is showing my password not being accurate. In the picture the password is plain text, because I am trouble shooting.
Email Recognized
Password is wrong, even though it's clearly right!
example table entry where password is put as test5
You are using a blank password stored in the database, this is a very dangerous approach and not recommended at all, you need to hash your passwords in order to login, I'm not sure what authentication you are using (ui/sanctum/breeze) but I recommend you use laravel/breeze, instead of building your own authentication using: https://laravel.com/docs/9.x/starter-kits#breeze-and-blade
Once that is installed, you can simply modify the blade to look anyway you like it to look.
To hash a password to be stored correctly you can do the following:
User Controller (Top Area):
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
User Controller (store method example):
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$data = request()->validate([
'email' => ['required','email',Rule::unique('users')],
'password' => 'required',
]);
$user = new User();
$user->password = Hash::make($request->password);
$user->email = $request->email;
$user->save();
return redirect('somewhere');
}
Note: You may also opt for using bcrypt($request->password) to directly crypt the password without having to use Hash::make();
I have this custom function for atempting to login in Laravel 8
protected function attemptLogin(Request $request)
{
$credentials = $this->credentials($request);
$credentials['estado']=1;
return $this->guard()->attempt(
$credentials, $request->filled('remember')
);
}
How I can make to accept the login atempt when $credentials['estado'] also has 2 as value.
Don't know how to make it accept multiple values.
I managed to make the custom function accept the value of 1 but dunno how to make it accept multiple $credentials['estado'] values.
You don't need to change anything in attemptLogin() method, instead you can customize the crededentials() method in LoginController like this:
// login, if user have like a following described data in array
protected function credentials(Request $request)
{
$username = $this->username();
return [
$username => $request->get($username),
'password' => $request->get('password'),
'estado' => [ 1, 2 ], // OR condition
];
}
Answer for comments:
Honestly in my experience I didn't have that case, but if you want to redirect to the another view on failed login (for specific field 'estado'), you can customize the "sendFailedLoginResponse" method, and add some additional if-condition for checking the 'estado'.
As the "sendFailedLoginResponse" method will be called only for getting failed login response instance, then you can check: is that fail comes from 'estado' field actually. Something like this:
protected function sendFailedLoginResponse(Request $request)
{
// custom case, when login failed and estado is 2
if ($request->get('estado') == 2) {
return view('some.specific.view');
}
// laravel by default implementation
else {
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
]);
}
}
Remember, in this case (when we're redirecting the user to some page) we actually not redirecting as for always, but instead we're just returning a view. We do that because I think you don't want to let the users to open that specific view page anytime their want, as you need to let them see that page only for specific case. But when you'll do the actual redirect, then you will let the users to visit that page with some static URL.
Of course, you can do some additional stuff (add something in DB or the Session, and check is the request comes actually from 'estado' fails, but not from any user), but this could be a headeche for you, and in my opinion that will not be a real laravel-specific code.
Anyway, this is the strategy. I don't think, that this is mandatory, but this can be do your work easy and secure.
Note: I've got this deafault implementations from "AuthenticatesUsers" trait (use use Illuminate\Foundation\Auth\AuthenticatesUsers;). In any time you can get some available methods from there and override them in your LoginController, as the LoginController used that as a trait.
I have emails on the users table with upper and lower case, e.g
MyName#email.com
On the login I use email and password field.
I want to success login when the user send the email no matter upper or lower case e.g
if the user send: myNAME#email.com
I think to change all to lower case before compare.
Now I send the email field using overload credentials method on Logincontroller class
protected function credentials(Request $request)
{
$credentials = [
$this->username() => strtolower($request->get($this->username())),
"password" => $request->get("password")
];
return $credentials;
}
This convert the email sended to lowercase.
But how can I change to lowercase, database email value before compare?
hi every one i am using laravel 5.2 default auth but i want that the user must only be logged in with active status.
https://laravel.com/docs/5.2/authentication
in this link they given the method like the following
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
// The user is active, not suspended, and exists.
}
but I did not find this where it is located in laravel 5.2.
I searched but the solutions are for previous versions and not for 5.2.
So please help me to login the users that has active status only so give me laravel 5.2 not of 5.1 or previous versions built in or custom solution to solve the problem
Assuming you are using the default auth setup, you can override the getCredentials method on the AuthController, which comes from the AuthenticatesUsers trait.
protected function getCredentials(Illuminate\Http\Request $request)
{
return $request->only($this->loginUsername(), 'password') + ['active' => 1];
}
That method returns the credentials array that is passed to Auth::attempt in the login method of AuthController. As other people have mentioned you will need to have a active field on your users table.
That example is only to show you can add custom fields when attempting to login.
You need to add an extra field to your User table like isActive.
When you have done this you can check if a user is active in your application.
if($user->isActive) {
// do something
}
It is possible to create user from Admin panel, by administrator without password? I imagine follow procedure:
Administrator create user without password
User get email with instruction for entering password and activation account
User can register with email and his password
I don't think so. That's why when I create my users I generate a random password.
$user->password = str_shuffle("Random_Password"); // generate random initial password
I have done this before by hacking the 'forgotten password' functionality of Laravel (rather that reinventing the wheel). I can't say how well this fits into Sentry but it was pretty trivial to do it in plain old Laravel:
Create user with blank password
Add an entry into the password reminders table (manually, don't use Auth::remind or whatever it is as it'll send an email, but do use the code from the class to generate the token)
Send welcome email to user with link to /user/confirm (or whatever, the point is that it doesn't have to be /user/forgotten-password) and hook that route up in the normal way for forgotten password with an added check for $user->password == '' if you wanna make sure only unconfirmed people can go to that page (not that it really matters).
You may also wish to extend the timeout on the forgotten passwords or, as I did (proper hacky I know), when the user's in the /user/confirm version of the forgotten password functionality, just refresh the timeout in the table before passing through to Laravel's auth system for checking.
Our code is something like this:
On register:
// however you register the user:
$user = new User;
$user->email = Input::get('email');
$user->password = '';
$user->save();
// create a reminder entry for the user
$reminderRepo = App::make('auth.reminder.repository');
$reminderRepo->create($user);
Mail::send(
'emails.registered',
[
'token' => $reminder->token,
],
function ($message) use ($user) {
$message->to($user->email)->setSubject(Lang::get('account.email.registered.subject', ['name' => $user->name]));
}
);
Now the confirm link:
class AccountController extends Controller
{
public function confirm($token)
{
$reminder = DB::table('password_reminders')->whereToken($token)->first();
if (! $reminder) {
App::abort(404);
}
// reset reminder date to now to keep it fresh
DB::table('password_reminders')->whereToken($token)->update(['created_at' => Carbon\Carbon::now()]);
// send token to view but also email so they don't have to type it in (with password reminders it's is a good thing to make users type it, but with confirm account it feels weird)
return View::make('account.confirm-account')->withToken($token)->withEmail($reminder->email);
}
public function postConfirm($token)
{
$credentials = Input::only('email', 'password', 'password_confirmation', 'token');
$response = Password::reset($credentials, function ($user, $password) {
$user->password = $password;
$user->save();
});
switch ($response) {
case Password::INVALID_PASSWORD:
case Password::INVALID_TOKEN:
case Password::INVALID_USER:
return Redirect::back()->withInput()->with('message-error', Lang::get($response));
case Password::PASSWORD_RESET:
Auth::login(User::whereEmail(Input::get('email'))->first());
return Redirect::route('account.home')->with('message-info', Lang::get('messages.confirm_account.succeeded'));
}
}