Force users to change password

This commit applies the password_expiration setting to all users.
Users won't be able to do anything other than update password until it's done.
This commit is contained in:
Miguel Nogueira 2021-01-06 05:03:38 +00:00
parent aa2bfac3e5
commit 14a8e9e9d5
Signed by: miguel456
GPG Key ID: 2CF61B825316C6A0
8 changed files with 178 additions and 38 deletions

View File

@ -172,6 +172,8 @@ class UserController extends Controller
if (! is_null($user)) {
$user->password = Hash::make($request->newPassword);
$user->password_last_updated = now();
$user->save();
Log::info('User '.$user->name.' has changed their password', [

View File

@ -85,6 +85,8 @@ class Kernel extends HttpKernel
'usernameUUID' => \App\Http\Middleware\UsernameUUID::class,
'forcelogout' => \App\Http\Middleware\ForceLogoutMiddleware::class,
'2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
'passwordexpiration' => \App\Http\Middleware\PasswordExpirationMiddleware::class,
'passwordredirect' => \App\Http\Middleware\PasswordExpirationRedirectMiddleware::class,
'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class,
'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class,
'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class,

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Middleware;
use App\Facades\Options;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class PasswordExpirationMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if(Auth::check())
{
$sinceUpdate = Carbon::parse(Auth::user()->password_last_updated)->diffInDays(now());
$updateThreshold = Options::getOption('password_expiry');
if ($updateThreshold !== 0 && $sinceUpdate > $updateThreshold)
{
session()->put('passwordExpired', true);
}
else
{
session()->put('passwordExpired', false);
}
}
return $next($request);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class PasswordExpirationRedirectMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (Auth::check() && session('passwordExpired'))
{
// WARNING!! Routes under the profile group must not have this middleware, because it'll result in an infinite redirect loop.
return redirect(route('showAccountSettings'));
}
return $next($request);
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddPasswordLastUpdatedToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('password_last_updated')->after('remember_token')->nullable();
$table->boolean('administratively_locked')->after('email')->default(false)->comment('Account locked by settings changes, e.g. 2fa grace period timeout');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('password_last_updated');
$table->dropColumn('administratively_locked');
});
}
}

View File

@ -100,8 +100,7 @@
<p><b><i class="fas fa-exclamation-triangle"></i> DANGER: </b> Insecure security policy</p>
<p>Your current password security policy is set to <b>off</b>. This allows users to choose potentially unsafe passwords.</p>
<p>We strongly recommend you update this value to <b>Low</b> or <b>Medium</b>.</p>
<p>Your current password security policy is set to <b>off</b>. This allows users to choose potentially unsafe passwords. We strongly recommend you update this value to <b>Medium</b>.</p>
</div>

View File

@ -205,6 +205,23 @@
</div>
@if(session('passwordExpired'))
<div class="row">
<div class="col">
<div class="alert alert-warning">
<p><i class="fas fa-exclamation-triangle"></i><b> Your password has expired</b></p>
<p>
You've been redirected here because your <b>password has expired.</b> All users must change their password every {{ \App\Facades\Options::getOption('password_expiry') }} days.
This is put in place to make sure user accounts remain secure.
</p>
<p>Please change update your password now. You won't be able to use the application until you do this.</p>
</div>
</div>
</div>
@endif
<div class="row">
<div class="col">

View File

@ -67,45 +67,50 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
Route::get('/accounts/danger-zone/{ID}/{action}/{token}', [UserController::class, 'processDeleteConfirmation'])
->name('processDeleteConfirmation');
Route::group(['middleware' => ['auth', 'forcelogout', '2fa', 'verified']], function () {
Route::get('/dashboard', [DashboardController::class, 'index'])
Route::group(['middleware' => ['auth', 'forcelogout', 'passwordexpiration', '2fa', 'verified']], function () {
Route::group(['middleware' => ['passwordredirect']], function(){
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard')
->middleware('eligibility');
Route::get('users/directory', [ProfileController::class, 'index'])
->name('directory');
Route::get('users/directory', [ProfileController::class, 'index'])
->name('directory');
Route::resource('teams', TeamController::class);
Route::resource('teams', TeamController::class);
Route::post('teams/{team}/invites/send', [TeamController::class, 'invite'])
->name('sendInvite');
Route::post('teams/{team}/invites/send', [TeamController::class, 'invite'])
->name('sendInvite');
Route::get('teams/{team}/switch', [TeamController::class, 'switchTeam'])
->name('switchTeam');
Route::get('teams/{team}/switch', [TeamController::class, 'switchTeam'])
->name('switchTeam');
Route::patch('teams/{team}/vacancies/update', [TeamController::class, 'assignVacancies'])
->name('assignVacancies');
Route::patch('teams/{team}/vacancies/update', [TeamController::class, 'assignVacancies'])
->name('assignVacancies');
Route::get('teams/invites/{action}/{token}', [TeamController::class, 'processInviteAction'])
->name('processInvite');
Route::get('teams/invites/{action}/{token}', [TeamController::class, 'processInviteAction'])
->name('processInvite');
Route::get('team/files', [TeamFileController::class, 'index'])
->name('showTeamFiles');
Route::get('team/files', [TeamFileController::class, 'index'])
->name('showTeamFiles');
Route::post('team/files/upload', [TeamFileController::class, 'store'])
->name('uploadTeamFile');
Route::post('team/files/upload', [TeamFileController::class, 'store'])
->name('uploadTeamFile');
Route::delete('team/files/{teamFile}/delete', [TeamFileController::class, 'destroy'])
->name('deleteTeamFile');
Route::delete('team/files/{teamFile}/delete', [TeamFileController::class, 'destroy'])
->name('deleteTeamFile');
Route::get('team/files/{teamFile}/download', [TeamFileController::class, 'download'])
->name('downloadTeamFile');
Route::get('team/files/{teamFile}/download', [TeamFileController::class, 'download'])
->name('downloadTeamFile');
});
Route::group(['prefix' => '/applications'], function () {
Route::group(['prefix' => '/applications', 'middleware' => ['passwordredirect']], function () {
Route::get('/my-applications', [ApplicationController::class, 'showUserApps'])
->name('showUserApps')
->middleware('eligibility');
@ -136,7 +141,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
->name('voteApplication');
});
Route::group(['prefix' => 'appointments'], function () {
Route::group(['prefix' => 'appointments', 'middleware' => ['passwordredirect']], function () {
Route::post('schedule/appointments/{application}', [AppointmentController::class, 'saveAppointment'])
->name('scheduleAppointment');
@ -144,7 +149,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
->name('updateAppointment');
});
Route::group(['prefix' => 'apply', 'middleware' => ['eligibility']], function () {
Route::group(['prefix' => 'apply', 'middleware' => ['eligibility', 'passwordredirect']], function () {
Route::get('positions/{vacancySlug}', [ApplicationController::class, 'renderApplicationForm'])
->name('renderApplicationForm');
@ -152,15 +157,21 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
->name('saveApplicationForm');
});
// Further locking down the profile section by adding the middleware to everything but the required routes
Route::group(['prefix' => '/profile'], function () {
Route::get('/settings', [ProfileController::class, 'showProfile'])
->name('showProfileSettings');
->name('showProfileSettings')
->middleware('passwordredirect');
Route::patch('/settings/save', [ProfileController::class, 'saveProfile'])
->name('saveProfileSettings');
->name('saveProfileSettings')
->middleware('passwordredirect');
Route::get('user/{user}', [ProfileController::class, 'showSingleProfile'])
->name('showSingleProfile');
->name('showSingleProfile')
->middleware('passwordredirect');
Route::get('/settings/account', [UserController::class, 'showAccount'])
->name('showAccountSettings');
@ -169,23 +180,30 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
Route::patch('/settings/account/change-password', [UserController::class, 'changePassword'])
->name('changePassword');
Route::patch('/settings/account/change-email', [UserController::class, 'changeEmail'])
->name('changeEmail');
->name('changeEmail')
->middleware('passwordredirect');
Route::post('/settings/account/flush-sessions', [UserController::class, 'flushSessions'])
->name('flushSessions');
->name('flushSessions')
->middleware('passwordredirect');
Route::patch('/settings/account/twofa/enable', [UserController::class, 'add2FASecret'])
->name('enable2FA');
->name('enable2FA')
->middleware('passwordredirect');
Route::patch('/settings/account/twofa/disable', [UserController::class, 'remove2FASecret'])
->name('disable2FA');
->name('disable2FA')
->middleware('passwordredirect');
Route::patch('/settings/account/dg/delete', [UserController::class, 'userDelete'])
->name('userDelete');
->name('userDelete')
->middleware('passwordredirect');
});
Route::group(['prefix' => '/hr'], function () {
Route::group(['prefix' => '/hr', 'middleware' => ['passwordredirect']], function () {
Route::get('staff-members', [UserController::class, 'showStaffMembers'])
->name('staffMemberList');
@ -199,7 +217,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo
->name('terminateStaffMember');
});
Route::group(['prefix' => 'admin'], function () {
Route::group(['prefix' => 'admin', 'middleware' => ['passwordredirect']], function () {
Route::get('settings', [OptionsController::class, 'index'])
->name('showSettings');