Some application which contains sensitive data requires you to change your password every N number of days. Applications in Banking sector or others which contain more sensitive data usually follow this approach.
In this article we will cover on how to implement password expiry feature on top of Laravel Basic Authentication. Before we go into the steps make sure you have a Laravel Setup with Basic Laravel Authentication System in place.
Before we dig into the steps, Let's take a moment to understand of the application flow we are trying to acheive. Our application will store the timestamp of last updated user password (via password reset, user regestration or change password).
Everytime user logs into the application we will check if the password updation date has crossed 30 days. If yes, we will log him out and force him to change the password. Once the password is updated we will update the timestamp in our database and will allow user to Log into the application.
Here is what the flow will look like.
Alright, Let's dig into the steps.
Model, Migration and Database Table for Password Expiration Data
We want a new table in our database, where we can store the data related to password expiration. Following is the data we are looking to store against all users.
- Number of days in which password will expire.
- Last time the password was updated.
I am keeping Number of days for password expiration in database for each user, since we may want to configurable as per user. And allow user the option to shorten the number of days (if they want tighter security for themselves). If you want to the Number of days to be fixed for all user's in your applicaiton you can avoid storing it in the database table.
Let's go ahead and create the Model and migration file for the new table.
php artisan make:model PasswordSecurity -m
Run this command in command line on your project root. This will create a new Model Class as well as the migration file to create the new table.
Open the newly created migration file create_password_securities
under App \ Database \ Migrations folder and modify the up()
method to add the new fields in the table.
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();
});
}
As the name suggests, we have added new fields to be added in the database table, password_expiry_days
will contain the integer value of number of days in which user password will expire and password_updated_at
contains a timestamp value of last password updation.
Let's go ahead and run the migration
php artisan migrate:refresh
Defining Model Relationships
Next, let's go ahead and define our relationship between User
and PasswordSecurity
Model.
As it looks a user will have only one entry in the password_securities table. We will setup a one-to-one relationship between these two..
Open User.php
Model class located at App directory and add the following code.
public function passwordSecurity()
{
return $this->hasOne('App\PasswordSecurity');
}
Open PasswordSecurity.php
Model class located under App direcrort and add the following method.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PasswordSecurity extends Model
{
protected $guarded = [];
public function user()
{
return $this->belongsTo('App\User');
}
}
Setting up Password Expiration Data
We need to setup default data in our password expiration data in password_securities table. We will add the in this table as soon as the user registers for our application.
Open RegisterController.php
which is located under App / Http / Conrtollers / Auth and modify its create method to include code for creating the default entry of PasswordSecurity.
protected function create(array $data)
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
$passwordSecurity = PasswordSecurity::create([
'user_id' => $user->id,
'password_expiry_days' => 30,
'password_updated_at' => Carbon::now(),
]);
return $user;
}
Just after User is created, we insert our default data in passwordSecurity table as well. You need to include use Carbon\Carbon;
in your RegisterController.php imports.
Note : You will need to update the password_updated_at timestamp whenever user changes or resets his password. Thus make sure you modify the code of ChangePassword and ResetPassword functionality if you have in your application.
Checking for Password Expiration on Login
Now since we have necessary table and Model file in place to store the details of user's password expiration. Let's modify our LoginController.php to add the code of checking for password expiration every time user logs in.
We will override the authenticated()
method of AuthenticatesUsers
trait which is used by LoginController. authenticated()
method is executed everytime user is successfully logs in. Thus we will use this method to do our job of checking for password expiration.
Open LoginController.php
located at App \ Http \ Controllers \ Auth and add the authenticated() method with following code.
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('/passwordExpiration')->with('message', "Your Password is expired, You need to change your password.");
}
return redirect()->intended($this->redirectPath());
}
If you look into the code, we check for password expiry with the help of Carbon
class. If the password is expired, we log the user out and redirect him to passwordExpiration page where he can update his password.
Let's take some time to understand the session variable password_expired_id
that we are creating just before logging the user out. Since we dont want random user to be able to access password expiration page. We are setting up a session variable for authentication.
Only user's who have password expired will have this session variable available to them and they can only be able to set a new password using passwordExpiration page.
Setting up New Password After Expiration
Let's look at the code where user can set their new password if their password is expired.
Add the following routes in your routes / web.php file.
Route::get('/passwordExpiration','Auth\PwdExpirationController@showPasswordExpirationForm');
Route::post('/passwordExpiration','Auth\PwdExpirationController@postPasswordExpiration')->name('passwordExpiration');
Add a new controller PwdExpirationController.php under your App / Http / Controllers / Auth directory and add following code into it.
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
class PwdExpirationController extends Controller
{
public function showPasswordExpirationForm(Request $request){
$password_expired_id = $request->session()->get('password_expired_id');
if(!isset($password_expired_id)){
return redirect('/login');
}
return view('auth.passwordExpiration');
}
public function postPasswordExpiration(Request $request){
$password_expired_id = $request->session()->get('password_expired_id');
if(!isset($password_expired_id)){
return redirect('/login');
}
$user = User::find($password_expired_id);
if (!(Hash::check($request->get('current-password'), $user->password))) {
// The passwords matches
return redirect()->back()->with("error","Your current password does not matches with the password you provided. Please try again.");
}
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.");
}
$validatedData = $request->validate([
'current-password' => 'required',
'new-password' => 'required|string|min:6|confirmed',
]);
//Change Password
$user->password = bcrypt($request->get('new-password'));
$user->save();
//Update password updation timestamp
$user->passwordSecurity->password_updated_at = Carbon::now();
$user->passwordSecurity->save();
return redirect('/login')->with("status","Password changed successfully, You can now login !");
}
}
The method showPasswordExpirationForm
will accept the GET request and will be used to show the password expiration page to the user where he can set the the new password.
The method postPasswordExpiration
will accept the POST request from the the form. It updates user password after completing a certain checks on the input provided by the user.
Once the pasword is changed, we go ahead and change our password_updated_at
timestamp in password_securities table.
Also notice that we have restricted access to both the methods to user's who has session variable 'password_expired_id' set at their end.
Create a blade file passwordExpiration.blade.php under directory resources / views / auth and put following content in it. This the view file will be used by user to change their passwords when expired.
@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">Password Expired</div>
<div class="panel-body">
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
@if (session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if (session('message'))
<div class="alert alert-info">
{{ session('message') }}
</div>
@endif
<form class="form-horizontal" method="POST" action="{{ route('passwordExpiration') }}">
{{ csrf_field() }}
<div class="form-group{{ $errors->has('current-password') ? ' has-error' : '' }}">
<label for="new-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_confirmation" 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
That's all is required to implement password expired functionality in your application.
[caption id="attachment_955" align="aligncenter" width="1212"] Password Expired Screen Laravel[/caption]
[caption id="attachment_956" align="aligncenter" width="1190"] Password Expired Changed Successfully.[/caption]