Automatic password resets with Laravel

Publié le 06/10/2015 | #laravel , #php

I’ve recently had to implement a way to force the registred users of a Laravel 5.1 application to change their passwords after a given period. It’s not a very good security measure if you ask me because of the burden it puts on the users: an effective security policy is a necessary trade-off between ease of use and things like password complexity. But if you ever need to implement such functionnality, here’s how I’ve done it.

Password timestamp

We’ll need to add a timestamp column to the users table in order to retain the last date at which the password has been modified, so let’s make a migration:

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddPasswordUpdatedAtToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function(Blueprint $table) {
            $table->timestamp('password_updated_at');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function(Blueprint $table) {
            $table->dropColumn('password_updated_at');
        });
    }
}

Don’t forget to actually run the migration: php artisan migrate.

Laravel prodives an easy way to convert timestamps to instances of Carbon with the dates mutator properties on the Eloquent model. By default the created_at and updated_at attributes are automatically converted but if you want to add a column you need to explicitly list them all:

    /**
     * Date mutators.
     *
     * @var array
     */
    protected $dates = ['created_at', 'updated_at', 'password_updated_at'];

Date comparison

This date mutator allows us to make use of the Carbon library to easily compare the newly added password_updated_at attribute to a date three months before. There’s a lot of others useful methods to compare date with the Carbon library so if the months approach doesn’t fit your project, just check them out.

Let’s wrap that verification into a method of our User model, not that you will use it much but it makes for a more readable code:

/**
 * Check if user has an old password that needs to be reset
 * @return boolean
 */
public function hasOldPassword()
{
    return $this->password_updated_at->lt(Carbon::now()->subMonths(3));
}

Great. Now we can add it to the Authenticate middleware:

if ($request->user()->hasOldPassword()) {
    return redirect('old-password-reset');
}

The form

To recap: the Autenticate middleware checks if the current user is authenticated, if not it redirects to the auth/login route. If the user is logged it will then check for the last password reset and if it is too old then it redirects to the old-password-reset. But we don’t have such a route don’t we? Let’s tackle that:

Route::get('old-password-reset', 'Auth\PasswordController@getOldPasswordReset');

This route references the getOldPasswordReset method on the PasswordController. What we need this route to do is to return a view explaining why the user is redirected and a form to reset his password:

/**
* Get the password reset view when it is too old
* @return Response
*/
public function getOldPasswordReset()
{
    return view('auth.reset-old');
}

Nothing crazy. We still need that view though. I’ll intentionnaly leave off the template part of it and just expose the necessary. I’m using Bootstrap and Laravel Collective Forms here though:

{!! Form::open(['method' => 'POST', 'route' => 'store-new-password', 'class' => 'form-horizontal']) !!}

    <div class="form-group">
        {!! Form::label('old_password', 'Password *', ['class' => 'col-sm-3 control-label']) !!}
            <div class="col-sm-9">
                {!! Form::password('old_password', ['class' => 'form-control', 'required' => 'required']) !!}
                <small class="text-danger">{{ $errors->first('old_password') }}</small>
            </div>
    </div>

    <div class="form-group">
        {!! Form::label('password', 'New password *', ['class' => 'col-sm-3 control-label']) !!}
            <div class="col-sm-9">
                {!! Form::password('password', ['class' => 'form-control', 'required' => 'required']) !!}
                <small class="text-danger">{{ $errors->first('password') }}</small>
            </div>
    </div>

    <div class="form-group">
        {!! Form::label('password_confirmation', 'Confirm your new password *', ['class' => 'col-sm-3 control-label']) !!}
            <div class="col-sm-9">
                {!! Form::password('password_confirmation', ['class' => 'form-control', 'required' => 'required']) !!}
                <small class="text-danger">{{ $errors->first('password_confirmation') }}</small>
            </div>
    </div>

    <div class="btn-group pull-right">
        {!! Form::submit("Save", ['class' => 'btn btn-success']) !!}
    </div>

{!! Form::close() !!}

Validation and storage

Yep. So three form inputs here: the first one will check for the actual password as a security measure while the second and the third will store the new password. Let’s make a new method inside our PasswordController to validate and store the new password:

/**
 * Store new password in DB
 * @param  Request $request
 * @return Response
 */
public function storeNewPassword(Request $request)
{
    $this->validate($request, [
        'old_password' => 'required|hashmatch:' . auth()->user()->id,
        'password'     => 'required|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{8,20})|confirmed|hashdiffer:' . auth()->user()->id,
    ]);

    $user = auth()->user();

    $user->password            = $request->password;
    $user->password_updated_at = Carbon::now();

    $user->save();

    return redirect('admin')->with('message', 'Your password has been updated');
}

I’m using two customs validation rules here in order to check that:

  1. the old_password field matches the old one.
  2. the new password is not the same as the old one

In order to add those custom validation rules we’ll going to add them to the boot() method of the AppServiceProvider class:

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    // Check given password against password stored in database
    Validator::extend('hashmatch', function($attribute, $value, $parameters)
    {
        $user = User::find($parameters[0]);

        return Hash::check($value, $user->password);
    });

    // Check that given password is not the same as the password stored in database
    Validator::extend('hashdiffer', function($attribute, $value, $parameters)
    {
        $user = User::find($parameters[0]);

        return ! Hash::check($value, $user->password);
    });
}

And that’s it! You may still want to add custom errors messages to provide user-friendly feedbacks, but other than that we’re done.

✦✦✦

Feedback

✦✦✦