How to create a password reset method in Laravel when using the Database User Provider
Asked Answered
L

6

17

I've been searching the internet and have yet to find a solution to the following problem...

We currently have a website developed using Laravel which the user table is a remote Microsoft SQL database. The driver in config/auth.php has been set to "database". All is working fine except for the password reset functionality, which we get the following error:

UnexpectedValueException in PasswordBroker.php line 238: User must implement CanResetPassword interface.

From my limited understanding of Laravel (this is my first experiance with Laravel), the Eloquent driver has support for the CanResetPassword functionality, however, this has not been implemented in the Database User Provider by Laravel, hence the error.

So my question is thus, has anyone had a configuration where they have the driver to “Database” and implemented a reset password functionality? All the examples I have seen to date relate to using the Eloquent model, which from my understanding of Laravel is not an option since during the initial development we had to change the driver from Eloquent to database to get the remote Microsoft SQL server working in the first place. Moving the Microsoft SQL database to a local database is not an option I’m afraid.

Alternatively, if anyone has implemented another method of a user resetting their password using an email address I would be open to suggestions.

Lazaretto answered 14/2, 2017 at 14:46 Comment(2)
I can't see any 'driver' in config/app.php. But I can help you with a custom password reset if you still need it.Jocundity
My bad, it was the config/auth.php, not config/app.php that the driver settings were specified. I'll correct the post. Help with the custom password reset would be much appreciated.Lazaretto
J
36

To write your own password reset logic, you can still use the default migration that comes out of the box or simply create yours. The most important part is the token. Because you are making your own password reset, you have a couple of decisions to make:

  • Will the token expire?
  • Can a user use the same token multiple times?

You will need 2 pages, 4 different routes and 4 different functions in the same controller. The 'I forgot my password' page and the 'Reset password' page. In the first page, display a form where you take the user email. And post to the following controller.

//to be added on top as use statements 
use DB;
use Auth;
use Hash;
use Carbon;
use App\User;

public function sendPasswordResetToken(Request $request)
{
    $user = User::where ('email', $request->email)-first();
    if ( !$user ) return redirect()->back()->withErrors(['error' => '404']);

    //create a new token to be sent to the user. 
    DB::table('password_resets')->insert([
        'email' => $request->email,
        'token' => str_random(60), //change 60 to any length you want
        'created_at' => Carbon::now()
    ]);

    $tokenData = DB::table('password_resets')
    ->where('email', $request->email)->first();

   $token = $tokenData->token;
   $email = $request->email; // or $email = $tokenData->email;

   /**
    * Send email to the email above with a link to your password reset
    * something like url('password-reset/' . $token)
    * Sending email varies according to your Laravel version. Very easy to implement
    */
}

Second part, when the user clicks on the link

/**
 * Assuming the URL looks like this 
 * http://localhost/password-reset/random-string-here
 * You check if the user and the token exist and display a page
 */

 public function showPasswordResetForm($token)
 {
     $tokenData = DB::table('password_resets')
     ->where('token', $token)->first();

     if ( !$tokenData ) return redirect()->to('home'); //redirect them anywhere you want if the token does not exist.
     return view('passwords.show');
 }

Display a page with a form containing 2 inputs - New password password or whateveer you want - New password confirmation password_confirm or whatever you want The form should post to the same URL mapped to the following controller. Why? because we still need to use the token to find the actual user.

 public function resetPassword(Request $request, $token)
 {
     //some validation
     ...

     $password = $request->password;
     $tokenData = DB::table('password_resets')
     ->where('token', $token)->first();

     $user = User::where('email', $tokenData->email)->first();
     if ( !$user ) return redirect()->to('home'); //or wherever you want

     $user->password = Hash::make($password);
     $user->update(); //or $user->save();

     //do we log the user directly or let them login and try their password for the first time ? if yes 
     Auth::login($user);

    // If the user shouldn't reuse the token later, delete the token 
    DB::table('password_resets')->where('email', $user->email')->delete();

    //redirect where we want according to whether they are logged in or not.
 }

Don't forget to add routes

Route::get('password-reset', 'PasswordController@showForm'); //I did not create this controller. it simply displays a view with a form to take the email
Route::post('password-reset', 'PasswordController@sendPasswordResetToken');
Route::get('reset-password/{token}', 'PasswordController@showPasswordResetForm');
Route::post('reset-password/{token}', 'PasswordController@resetPassword');

Note: There might be typos or syntax errors because I did not test this and wrote it here directly from the top of my head. If you see an error/exception, don't panick, read the error and search google.

Jocundity answered 16/2, 2017 at 1:13 Comment(8)
Thank you, this is very userful. I'll try to implement the changes today and will let you know how it went :-)Lazaretto
And don't forget to mark my answer and/or upvote the answer.Jocundity
I have now implemented the password reset with only minor changes for the SQL database to be tailored to our database. Thanks again @JocundityLazaretto
On each reset request it will generate a new token hence inserting a new row in the database table password_resets. This can create issue in case user has requested password multiple times and there are multiple entries in the table for the same email address. Whereas ideally it should update the token for respective email address when requesting password reset.Euphonic
Instead of str_random(60) I used the laravel way of doing this: private function generateToken() { // This is set in the .env file $key = config('app.key'); // Illuminate\Support\Str; if (Str::startsWith($key, 'base64:')) { $key = base64_decode(substr($key, 7)); } return hash_hmac('sha256', Str::random(40), $key); } Kentigerma
This seems to work as of Laravel 7.x for generating tokens: $token = app(PasswordBroker::class)->createToken($user); This was an amazing answer. And to one of your comments, how can you make the token expire?Ricercare
@Ricercare were you able to figure out how to make the tokens expire?Capone
You can use created_at to decide how long the token is valid for. Could be 1hr, 8hrs, 12hrs, 24hrs, 48hrs, etc. That's why the field created_at appears in the code sample.Jocundity
K
3

Just to add to what @eddythedove said.
Instead of str_random(60) I used the Laravel way of creating a token:

private function generateToken()
{
    // This is set in the .env file
    $key = config('app.key');

    // Illuminate\Support\Str;
    if (Str::startsWith($key, 'base64:')) {
        $key = base64_decode(substr($key, 7));
    }
    return hash_hmac('sha256', Str::random(40), $key);
}
Kentigerma answered 2/6, 2020 at 14:41 Comment(0)
S
1

If you find an error in str_random, make sure you import the module first:

use Illuminate\Support\Str;

Then call with Str::random (60).

Safranine answered 21/7, 2020 at 7:48 Comment(0)
U
0
$key = config('app.key');

if (Str::startsWith($key, 'base64:')) {
  $key = base64_decode(substr($key, 7));
}

$token = hash_hmac('sha256', Str::random(40), $key);
$dbToken = app(Hasher::class)->make($token);

DB::insert('password_resets', [
           'email' => '[email protected]',
            'token' => $dbToken,
        ]);

This should work in Laravel 8

Uniat answered 28/5, 2021 at 10:19 Comment(0)
O
0

The default way Laravel handles the Reset Password has a few security issues.

  • No track record reset password attempts (Delete the token in the table after success attempt is not acceptable)
  • No expiry date
  • No token used time

We always better keep track of these security functions.

I have altered the default table like this on my db migration:

public function up()
{
    Schema::table('password_resets', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->enum('is_used', ['t', 'f'])->default('f');
        $table->dateTime('updated_at')->nullable();
    });
}

Instead of deleting the record I simply update the table 'is_used' to 't' and updated_at column.

I use following query to filter is_used = 'f' and created on the same day to gather with token.

$data = PasswordReset::where('token', $token)->where('is_used', 'f')
        ->whereDate('created_at', '>=', Carbon::today()->toDateString())->first(); 
Ontario answered 7/9, 2021 at 8:17 Comment(0)
G
0

i try some code after that i get some solution that will work in laravel 8+.

    $key = config('app.key');
    if (Str::startsWith($key, 'base64:')) {
        $key = base64_decode(substr($key, 7));
    }
    $token = hash_hmac('sha256', Str::random(40), $key);
    $dbToken =Hash::make($token);
Gabrielegabriell answered 21/9, 2022 at 17:14 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.