Implement Password Expiration & Force Reset in Laravel

Today I’m going to share how to implement password expiration and force reset password in Laravel.

I’m testing on Laravel 7.15. Let’s get started:

Table of Contents

  1. Install Laravel and Basic Configurations
  2. Generate Auth Scaffolding
  3. Create Model & Migration
  4. Set Model Relationships
  5. Modify Register Controller
  6. Check Password Expiration
  7. Create Password Security Controller
  8. Create Reset Form
  9. Define Routes and Test

Install Laravel and Basic Configurations

Each Laravel project needs this thing. That’s why I have written an article on this topic. Please see this part from here: Install Laravel and Basic Configurations.

Generate Auth Scaffolding

We’ve to enable auth scaffolding. Run these commands to enable auth scaffolding:

# install laravel ui
composer require laravel/ui --dev

# auth scaffolding
php artisan ui vue --auth

# finally run
npm i && npm run dev

Create Model & Migration

To keep password expiration data, we need a table. Run this command to create a model and migration:

php artisan make:model PasswordSecurity -m

Go to database\migrations folder and open create_password_securities_table.php file.  Then update up() function like this:

public function up()
{
    Schema::create('password_securities', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id');
        $table->tinyInteger('password_expiry_days');
        $table->timestamp('password_updated_at');
        $table->timestamps();
    });
}

Now run the migration:

php artisan migrate:refresh

Set Model Relationships

Navigate to app folder and open PasswordSecurity.php model. We need to add $fillable array and need to set relation with User model. Let’s got this:

PasswordSecurity.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class PasswordSecurity extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'user_id', 'password_expiry_days', 'password_updated_at'
    ];

    // set relation
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

Open User.php from the same location and add this relation:

// set relation
public function passwordSecurity()
{
    return $this->hasOne('App\PasswordSecurity');
}

Modify Register Controller

Open RegisterController.php from app\Http\Controllers\Auth folder and update create() function:

use App\PasswordSecurity;
use Carbon\Carbon;

protected function create(array $data)
{
    $user =  User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
    ]);

    $passwordSecurity = PasswordSecurity::create([
        'user_id' => $user->id,
        'password_expiry_days' => 30,
        'password_updated_at' => Carbon::now(),
    ]);

    return $user;
}

I’ve set 30 days in password_expiry_days. You can set your own days to reset the password.

Check Password Expiration

We’ll check password expiration from the login controller. We will override the authenticated() method of AuthenticatesUsers trait.

Open LoginController.php located at app\Http\Controllers\Auth and add the authenticated() method:

use Carbon\Carbon;

// user authenticated
public function authenticated(Request $request, $user)
{
    $request->session()->forget('password_expired_id');

    $password_updated_at = $user->passwordSecurity->password_updated_at;
    $password_expiry_days = $user->passwordSecurity->password_expiry_days;
    $password_expiry_at = Carbon::parse($password_updated_at)->addDays($password_expiry_days);
    if ($password_expiry_at->lessThan(Carbon::now())) {
        $request->session()->put('password_expired_id', $user->id);
        auth()->logout();
        return redirect('/reset-password')->with('message', "Your password is expired. You need to change your password.");
    }

    return redirect()->intended($this->redirectPath());
}

Create Password Security Controller

From this controller we’ll show password reset form & store new password. Create the controller:

php artisan make:controller Auth\PasswordSecurityController

Open PasswordSecurityController.php from app\Http\Controllers\Auth directory and paste this code:

PasswordSecurityController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class PasswordSecurityController extends Controller
{
    // reset password form
    public function resetPasswordForm(Request $request)
    {
        $password_expired_id = $request->session()->get('password_expired_id');
        if (!isset($password_expired_id)) {
            return redirect('/login');
        }
        return view('auth.reset_password');
    }

    // reset password
    public function resetPassword(Request $request)
    {
        // check expire id
        $password_expired_id = $request->session()->get('password_expired_id');
        if (!isset($password_expired_id)) {
            return redirect('/login');
        }

        // validate
        $validatedData = $request->validate([
            'current_password' => 'required',
            'new_password' => 'required|string|min:6|confirmed',
        ]);

        // the requests
        $request_current_password = $request->current_password;
        $request_new_password = $request->new_password;
        $request_new_password_confirm = $request->new_password_confirm;

        // the passwords matches
        $user = User::find($password_expired_id);
        if (!(Hash::check($request_current_password, $user->password))) {
            return redirect()->back()->with("error", "Your current password does not matches with the password you provided. Please try again.");
        }

        // current password and new password are same
        if (strcmp($request_current_password, $request->new_password) == 0) {
            return redirect()->back()->with("error", "New password cannot be same as your current password. Please choose a different password.");
        }

        // new password and new password confirm doesn't match
        if (strcmp($request_new_password, $request_new_password_confirm) == 1) {
            return redirect()->back()->with("error", "New password doesn't match with confirm password.");
        }

        // change Password
        $user->password = bcrypt($request->new_password);
        $user->save();

        // update password update time
        $user->passwordSecurity->password_updated_at = Carbon::now();
        $user->passwordSecurity->save();

        return redirect('/login')->with("status", "Password changed successfully. Now you can login!");
    }
}

Create Reset Form

Go to resources/views/auth folder and create a new file named reset_password.blade.php. After that open the file and paste this code:

reset_password.blade.php
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">{{ __('Password Expired') }}</div>
                    <div class="card-body">

                        @if (session('error'))
                            <div class="alert alert-danger alert-square">
                                {{ session('error') }}
                            </div>
                        @endif
                        @if (session('success'))
                            <div class="alert alert-success alert-square">
                                {{ session('success') }}
                            </div>
                        @endif
                        @if (session('message'))
                            <div class="alert alert-info alert-square">
                                {{ session('message') }}
                            </div>
                        @endif

                        <form method="POST" action="{{ route('resetPassword') }}">
                            @csrf

                            <div class="form-group{{ $errors->has('current_password') ? ' has-error' : '' }}">
                                <label for="current_password" class="col-md-4 control-label">Current Password</label>

                                <div class="col-md-6">
                                    <input id="current_password" type="password" class="form-control"
                                           name="current_password" required>

                                    @if ($errors->has('current_password'))
                                        <span class="help-block">
                                        <strong>{{ $errors->first('current_password') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>

                            <div class="form-group{{ $errors->has('new_password') ? ' has-error' : '' }}">
                                <label for="new_password" class="col-md-4 control-label">New Password</label>

                                <div class="col-md-6">
                                    <input id="new_password" type="password" class="form-control" name="new_password"
                                           required>

                                    @if ($errors->has('new_password'))
                                        <span class="help-block">
                                        <strong>{{ $errors->first('new_password') }}</strong>
                                    </span>
                                    @endif
                                </div>
                            </div>

                            <div class="form-group">
                                <label for="new_password_confirm" class="col-md-4 control-label">Confirm New
                                    Password</label>

                                <div class="col-md-6">
                                    <input id="new_password_confirm" type="password" class="form-control"
                                           name="new_password_confirm" required>
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    <button type="submit" class="btn btn-primary">
                                        Change Password
                                    </button>
                                </div>
                            </div>

                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Define Routes and Test

Open web.php from routes folder and define 2 routes:

Route::get('/reset-password','Auth\PasswordSecurityController@resetPasswordForm');
Route::post('/reset-password','Auth\PasswordSecurityController@resetPassword')->name('resetPassword');

Our project is ready to test. Run the project and register a user. Then set old date & time at password_updated_at column in password_securities table. Then try to login and you’ll see the password expiration notice with form.

That’s it. You can download this project from GitHub. Thank you.