framework: Password reset e-mail missing e-mail in URL
- Laravel Version: 5.3.15
- PHP Version: 7.0.0
- Database Driver & Version: Oracle
Description:
When sending the password reset e-mail in Laravel 5.3, the reset link have a token, but doesn’t have the user e-mail. This way, the reset form will not load the user e-mail. In 5.2, the reset link had the user e-mail.
Without the user e-mail in the URL, Illuminate\Foundation\Auth\ResetsPasswords.php
will send a null e-mail in showResetForm
, because $request->email
will evaluate to null. Then, the reset form provided by the framework from Illuminate\Auth\Console\stubs\make\views\auth\passwords\email.stub
will show a blank e-mail in the following input:
<input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required>
Digging up who missed sending the e-mail in the URL, we can found a Illuminate\Auth\Notifications\ResetPassword.php
which does the following action:
->action('Reset Password', url('password/reset', $this->token))
As you can see, by default Laravel does not add the e-mail in the request URL. Since this trait usually is overwritten, we can add the e-mail to the URL to make things working again. The issue here is that, by default, Laravel is missing this e-mail in the request url.
Steps To Reproduce:
Send a reset e-mail link and open the link provided in the e-mail.
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Comments: 64 (23 by maintainers)
Solution
Here’s how to do it without altering vendor/core - tested with Laravel 5.6 (should be fine with later versions, too):
1. Create own notification
Firstly create a new notification for your app:
php artisan make:notification ResetPassword
Then open the newly created file:
app\Notifications\ResetPassword.php
and make the following changes:Add
public $token;
to the beginning of the class (ie. afteruse Queueable
).Add
$this->token = $token;
to the body of your__construct()
method.Add
$token
as parameter to the__construct()
method (so it reads__construct($token)
).Add the following to the body of your
toMail()
method (replacing what’s there by default):What you’ve done is create a copy of the original vendor/core version of the ResetPassword notification (found in
/vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php
). This allows you to make customisations to it without changing the core Laravel files.(One difference we’ve made is adding
$notifiable->email
to the body of the email.)2. Point to your notification
Now we need to tell Laravel to call our notification instead of the one found in the
CanResetPassword
trait. To do this, just edit yourApp/User.php
model:Add
use App\Notifications\ResetPassword;
to the top of the file (ie. a link to the notification you just created) and then add a new method:3. Update your routes
Finally we need to update our routes to include the new
{email}
parameter:Route::get('/password/reset/{token}/{email}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
Now if the user fills in the
/password/reset
form, they will be sent your notification, with the additional email parameter.This now works perfectly!
Additional: Handle errors
You can improve on the above by adding a bit of error handling. To do this, just pass the email to your form, to handle things if there’s a problem:
Add the following method to
Auth/ForgotPasswordController.php
:And then place the following at the top:
use Illuminate\Http\Request;
.And you’re done!
Additional: Finesse the UX
If you want, I’d also recommend modifying the semantic HTML in your views (regardless of your design). Go to
resources/views/auth/passwords/reset.blade.php
and remove theautofocus
attribute from the email input element, and addreadonly
. (I move theautofocus
to the first password input element myself.)Additional: Encrypt the email address in the URL
You can still tidy things up a bit more by encrypting the email in the URL. This isn’t for security, it’s to make it look prettier (so to speak) and make it less likely for the user to mess with the URL and break something.
To do this, just go to the notification we created in step 1 and change
$notifiable->email
toencrypt($notifiable->email)
.Then go to
app/Http/Controllers/Auth/ResetPasswordController.php
and add the following method:Don’t forget to place
use Illuminate\Http\Request;
at the top, too.And you’re done! (Again.)
I’ve tested all this fully, and it works perfectly, but if you have an issue, let me know.
Good luck!
Which does not make sense. If I open a reset url, the form should be opened with the user’s e-mail.
Otherwise, why there are these pieces of codes in the framework:
Illuminate\Foundation\Auth\ResetsPasswords.php
Illuminate\Auth\Console\stubs\make\views\auth\passwords\reset.stub
If the desired behavior in 5.3 is to force the user to input it’s e-mail again, why the
showResetForm
and the view are trying to send and display it?@khalilst @Mayonado @zmonteca
I have fixed it by editing/hacking
ResetPassword.php
notification &reset.blade.php
file. Now this does mean you are editing vendor file, so it’s your choice to do this or not.ResetPassword.php
The file can be found at (5.4):
Amend the
toMail
method:From:
To
Notice the updated parameters argument,
$notifiable
resolves toUser
object hence you can access the email property:reset.blade.php
The file can be found at (5.4):
The HTML may slightly differ here based on your CSS framework. In addition, you may want to show email field as read-only or make it hidden input. I prefer the user seeing the email they requested the password reset for.
From
To
Notice the updated value attribute, added the readonly attribute to stop user amending the email.
Hope this helps others.
here’s a hack for finding the email with token only
don’t know about the performance though…
Well this is going nowhere! This is the last that I’m trying to justify myself: Yes, I have used flash messages. As I said you can store anything on the session however because it’s stored on the server it is (again!) mainly for storing sensitive data or in another word it’s the best option if you need for example store authenticated user, shopping cart details, etc.
@devcircus: in a high traffic website you never know how many users are requested to reset their passwords and the size of session files can grow quickly.
I faced this issue with laravel 5.7
The fix was very simple, update file
resources\views\auth\passwords\reset.blade.php
:change the line from:
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email or old('email') }}" required autofocus>
to
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" required autofocus>
notice the difference is the little ‘or’ to ‘??’
or you want to overide route reset
in ResetPassword notifiation:
use URL;
$link = URL::to("password-reset?token={$this->token}&email={$notifiable->email}");
and then you create route
Route::get('password-reset', YourController);
acces on your controller$request->email or $request->token
Hi guys, I’ve just realize this issue since I was working with 5.2. I love @Vrutin answer and found a better way rather than editing vendor.
Since the notification email is sent from
CanResetPassword
trait, and our user model extendsIlluminate\Foundation\Auth\User
which use the trait. We can just overridesendPasswordResetNotification()
in our user model and create our own notification class like so:Hope it helps. 😃
@Vrutin I use your solution, and works great!! I use encrypt() and decrypt() to hide the values.
Agreed. It’s terrible as is.
@Vrutin My mistake. Actually I forgot how to solve this problem before. Email souldn’t be a hidden field, it must be a simple input field. In fact I asked the user to enter his/her email in password reset form.