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)

Most upvoted comments

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. after use 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):

        return (new MailMessage)
            ->line('You are receiving this email because we received a password reset request for your account.')
            ->action('Reset Password', url(config('app.url').route('password.reset', [$this->token, $notifiable->email], false)))
            ->line('If you did not request a password reset, no further action is required.');

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 your App/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:

    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetPassword($token));
    }

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:

    protected function sendResetLinkFailedResponse(Request $request, $response)
    {
        return back()->withErrors(
            ['email' => trans($response)]
        )->withInput($request->only('email'));
    }

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 the autofocus attribute from the email input element, and add readonly. (I move the autofocus 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 to encrypt($notifiable->email).

Then go to app/Http/Controllers/Auth/ResetPasswordController.php and add the following method:

    public function showResetForm(Request $request, $token = null)
    {
        return view('auth.passwords.reset')->with(
            ['token' => $token, 'email' => decrypt($request->email)]
        );
    }

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

public function showResetForm(Request $request, $token = null)
{
    return view('auth.passwords.reset')->with(
        ['token' => $token, 'email' => $request->email]
    );
}

Illuminate\Auth\Console\stubs\make\views\auth\passwords\reset.stub

<input id="email" type="email" class="form-control" name="email" value="{{ $email or old('email') }}" required autofocus>

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):

…/vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php

Amend the toMail method:

From:

public function toMail($notifiable)
    {
        return (new MailMessage)
            ->line('You are receiving this email because we received a password reset request for your account.')
            ->action('Reset Password', url(config('app.url').route('password.reset', $this->token, false)))
            ->line('If you did not request a password reset, no further action is required.');
    }

To

public function toMail($notifiable)
    {
        return (new MailMessage)
            ->line('You are receiving this email because we received a password reset request for your account.')
            ->action('Reset Password', url(config('app.url') . route('password.reset', [$this->token, 'email=' . $notifiable->email], false)))
            ->line('If you did not request a password reset, no further action is required.');
    }

Notice the updated parameters argument, $notifiable resolves to User object hence you can access the email property:

[$this->token, 'email=' . $notifiable->email]

reset.blade.php

The file can be found at (5.4):

…/resources/views/auth/passwords/reset.blade.php

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

<input type="text" id="email" name="email" class="input {{ $errors->has('email') ? ' is-danger' : '' }}" value="{{ old('email') }}">

To

<input type="text" id="email" name="email" class="input {{ $errors->has('email') ? ' is-danger' : '' }}" value="{{ $email }}" readonly>

Notice the updated value attribute, added the readonly attribute to stop user amending the email.

... value="{{ $email }}" readonly>

Hope this helps others.

here’s a hack for finding the email with token only

public function showResetForm(Request $request, $token = null)
    {
        $dbTokens = DB::table('password_resets')->get();
        $email = '';
        foreach($dbTokens as $dbToken) {
            if($this->hasher->check($token, $dbToken->token)) {
                $email = $dbToken->email;
                break;
            }
        }

        return view('auth.passwords.reset')->with(
            ['token' => $token, 'email' => $email]
        );
    }

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}");

return (new MailMessage)
                    ->line('You are receiving this email because we received a password reset request for your account.')
                    ->line('Click the button below to reset your password:')
                    ->action('Reset Password', $link)
                    ->line('If you did not request a password reset, no further action is required.');

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 extends Illuminate\Foundation\Auth\User which use the trait. We can just override sendPasswordResetNotification() in our user model and create our own notification class like so:

public function sendPasswordResetNotification($token)
{
    $this->notify(new App\Notifications\ResetPasswordNotification($token));
}

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.