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.
No comments or questions yet...