In some systems it is required to change password every X days: in banking sector, or working with more sensitive data. Laravel doesn’t have that functionality out-of-the-box, but it’s easy to build. Let’s do it today.
Here’s what I mean by “expired password” page. When user logs in, they will see something like this:

To build this functionality, we will go through following steps:
- Add timestamp field “password_changed_at” to users DB table
- Build that page to force reset password: route/controller/view
- Process password reset, also validation on current password and new passwords
- Create middleware to check whether password has expired (was unchanged in X days)
- Add that middleware to Http Kernel and to Routes
- Final small details: make X days configurable from config file
So, let’s begin?
Step 1. Password_changed_at field
First, we add this migration:
class AddPasswordChangedAtToUsers extends Migration { public function up() { Schema::table('users', function (Blueprint $table) { $table->timestamp('password_changed_at')->nullable(); }); } public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('password_changed_at'); }); } }
Then we need to make it fillable, so we edit app/User.php:
class User extends Authenticatable { protected $fillable = [ 'name', 'email', 'password', 'password_changed_at' ];
Step 2. Page to force reset password
We will almost copy-paste current Reset Password page into separate MVC structure.
routes/web.php:
Route::get('password/expired', 'Auth\ExpiredPasswordController@expired') ->name('password.expired');
New controller app/Http/Controllers/Auth/ExpiredPasswordController.php:
class ExpiredPasswordController extends Controller { public function expired() { return view('auth.passwords.expired'); }
And new view resources/views/auth/password/expired.blade.php – see above:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Reset Password</div>
<div class="panel-body">
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
<a href="/">Return to homepage</a>
@else
<div class="alert alert-info">
Your password has expired, please change it.
</div>
<form class="form-horizontal" method="POST" action="{{ route('password.post_expired') }}">
{{ csrf_field() }}
<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('password') ? ' has-error' : '' }}">
<label for="password" class="col-md-4 control-label">New Password</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control" name="password" required="">
@if ($errors->has('password'))
<span class="help-block">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group{{ $errors->has('password_confirmation') ? ' has-error' : '' }}">
<label for="password-confirm" class="col-md-4 control-label">Confirm New Password</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required="">
@if ($errors->has('password_confirmation'))
<span class="help-block">
<strong>{{ $errors->first('password_confirmation') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<button type="submit" class="btn btn-primary">
Reset Password
</button>
</div>
</div>
</form>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
At this point, we should see this:

Now, how do we process the password change? POST request goes to route password.post_expired, so let’s implement that one.
routes/web.php:
Route::post('password/post_expired', 'Auth\ExpiredPasswordController@postExpired') ->name('password.post_expired');
ExpiredPasswordController.php:
use App\Http\Requests\PasswordExpiredRequest; use Carbon\Carbon; use Illuminate\Support\Facades\Hash; class ExpiredPasswordController extends Controller { public function postExpired(PasswordExpiredRequest $request) { // Checking current password if (!Hash::check($request->current_password, $request->user()->password)) { return redirect()->back()->withErrors(['current_password' => 'Current password is not correct']); } $request->user()->update([ 'password' => bcrypt($request->password), 'password_changed_at' => Carbon::now()->toDateTimeString() ]); return redirect()->back()->with(['status' => 'Password changed successfully']); }
As you may have noticed, for validation I’ve created a separate Request class in app/Http/Requests/PasswordExpiredRequest.php:
class PasswordExpiredRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'current_password' => 'required', 'password' => 'required|confirmed|min:6', ]; } }
Step 3. Middleware to check expired password
Ok, now we have a page to reset the expired password, and after the reset we set the field password_changed_at. Now, time to actually use that field and check when the password was actually reset.
Let’s assume that after logging in, user will go to some dashboard:
Route::get('/dashboard', function () { return 'See dashboard'; });
Let’s wrap it in auth middleware and Route Group:
Route::middleware(['auth'])->group(function () { Route::get('/dashboard', function () { return 'See dashboard'; }); });
Now, we need to add those previous routes under auth as well, cause you can change your password only if you’re logged in, right?
So we have this:
Route::middleware(['auth'])->group(function () { Route::get('/dashboard', function () { return 'See dashboard'; }); Route::get('password/expired', 'Auth\ExpiredPasswordController@expired') ->name('password.expired'); Route::post('password/post_expired', 'Auth\ExpiredPasswordController@postExpired') ->name('password.post_expired'); });
Final step here – introducing our own middleware to check the expired password. It should be called on all internal pages, for now we have only the dashboard, but in the future pages should be created under that special sub-group with password_expired middleware:
Final routes/web.php:
Route::middleware(['auth'])->group(function () { Route::middleware(['password_expired'])->group(function () { Route::get('/dashboard', function () { return 'See dashboard'; }); }); Route::get('password/expired', 'Auth\ExpiredPasswordController@expired') ->name('password.expired'); Route::post('password/post_expired', 'Auth\ExpiredPasswordController@postExpired') ->name('password.post_expired'); });
Now, let’s create our middleware:
php artisan make:middleware PasswordExpired
And then we fill app/Http/Middleware/PasswordExpired.php:
namespace App\Http\Middleware; use Carbon\Carbon; use Closure; class PasswordExpired { public function handle($request, Closure $next) { $user = $request->user(); $password_changed_at = new Carbon(($user->password_changed_at) ? $user->password_changed_at : $user->created_at); if (Carbon::now()->diffInDays($password_changed_at) >= 30) { return redirect()->route('password.expired'); } return $next($request); } }
Basically, we’re checking when the password was changed (if ever, otherwise we take users.created_at), and if it’s older than 30 days – we redirect to password expired page, that we’ve created above.
To make that middleware work, we need to register it in app/Http/Kernel.php:
protected $routeMiddleware = [ // ... previous list 'password_expired' => \App\Http\Middleware\PasswordExpired::class, ];
The final mini-step in this whole function is to make that 30 days configurable. It’s pretty easy – we just need to add a parameter into array of some config file – logically, we choose config/auth.php and add this line there:
return [ // ... all other config values 'password_expires_days' => 30, ];
Then we edit our middleware to use this value. Instead of:
if (Carbon::now()->diffInDays($password_changed_at) >= 30) {
We have this:
if (Carbon::now()->diffInDays($password_changed_at) >= config('auth.password_expires_days')) {
I guess, that’s it! Now you can force your users to change your passwords every 30 days or so.
Good effort. Thank you 🙂
If website force me to change my passwd, I must think a new passwd that i don’t familiar with it, I would forgot it for a while, Next Time when i login…. WTF
Online banking does that shit a lot.
Is there any check for the new password to be different from the existing one? You should put a condition for this otherwise one can still post the same password and it will update.
If hash::check (request(password), auth()-user()-password) rediret back with error.
if (strcmp($request->get(‘current-password’), $request->get(‘new-password’)) == 0) {
//Current password and new password are same
return redirect()->back()->with(“error”, “New Password cannot be same as your current password. Please choose a different password.”);
}
at app/Http/Requests/PasswordExpiredRequest.php you have done like class PasswordExpiredRequest extends FormRequest. but where is the programmatically implementation of FormRequest class? can you please explain me.?
Hi,
Ive managed to make it work on my local PC, but when i move the code to the client side. it run for ever and die saying there is too much redirect. i am using lavarel 5.3. can you please me to fix this?
Implementing the above works great, however it seems to override validate by email functionality.
Is there any way of restoring the validate email functionality so that both validate and password expires work?
Ignore my ridiculous comment above. Turns out i didnt have verify middleware turned on in my home controller.
Hi, there.
I’m having this error after using the reset password form:
Class App\Http\Requests\PasswordExpiredRequest does not exist
Does anyone know about it?
Thanks
Thank you very much for this solution which I used.
Hi, there.
I’m having this error after using the expired password :”
The page isn’t redirecting properly”
i have route like this :
Route::group([‘middleware’ => [‘web’, ‘cekuser:1’ ]], function(){
//unterview
Route::get(‘karyawan/data’, ‘KaryawanController@listData’)->name(‘karyawan.data’);
Route::post(‘karyawan/approve’, ‘KaryawanController@approve’);
Route::resource(‘karyawan’, ‘KaryawanController’);
});
Route::group([‘middleware’ => [‘web’, ‘cekuser:2’ ]], function(){
//unterview
Route::get(‘apporve/data’, ‘ApporveController@listData’)->name(‘aprove.data’);
Route::post(‘apporve/approve’, ‘ApporveController@approve’);
Route::resource(‘ApporveController’);
});
and many routes soo on ..
How to add middleware password_expired on my route group?
Thanks