From b5b4d5005a0e558c78b66af5abbbd811e60fd994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Wed, 22 Nov 2023 14:08:24 +0000 Subject: [PATCH 1/8] FOUR-11415 Password Policy Configuration --- .../Http/Controllers/Auth/LoginController.php | 3 +++ ProcessMaker/Models/User.php | 27 ++++++++++++++----- config/password-policies.php | 11 ++++++++ resources/views/admin/users/edit.blade.php | 6 ----- .../views/auth/passwords/change.blade.php | 13 +-------- resources/views/profile/edit.blade.php | 6 ----- .../views/shared/users/sidebar.blade.php | 6 ++--- 7 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 config/password-policies.php diff --git a/ProcessMaker/Http/Controllers/Auth/LoginController.php b/ProcessMaker/Http/Controllers/Auth/LoginController.php index d4012a6877..45e8f64f58 100644 --- a/ProcessMaker/Http/Controllers/Auth/LoginController.php +++ b/ProcessMaker/Http/Controllers/Auth/LoginController.php @@ -38,6 +38,8 @@ class LoginController extends Controller */ protected $redirectTo = '/'; + protected $maxAttempts; + /** * Create a new controller instance. * @@ -46,6 +48,7 @@ class LoginController extends Controller public function __construct() { $this->middleware('guest')->except(['logout', 'beforeLogout', 'keepAlive']); + $this->maxAttempts = (int) config('password-policies.login_attempts'); } /** diff --git a/ProcessMaker/Models/User.php b/ProcessMaker/Models/User.php index 8e2cd4e8ef..541e91ba8c 100644 --- a/ProcessMaker/Models/User.php +++ b/ProcessMaker/Models/User.php @@ -8,12 +8,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; use Laravel\Passport\HasApiTokens; use ProcessMaker\Models\EmptyModel; use ProcessMaker\Notifications\ResetPassword as ResetPasswordNotification; use ProcessMaker\Query\Traits\PMQL; use ProcessMaker\Rules\StringHasAtLeastOneUpperCaseCharacter; -use ProcessMaker\Rules\StringHasNumberOrSpecialCharacter; use ProcessMaker\Traits\Exportable; use ProcessMaker\Traits\HasAuthorization; use ProcessMaker\Traits\HideSystemResources; @@ -183,13 +183,28 @@ public static function rules(self $existing = null) */ public static function passwordRules(self $existing = null) { - return array_filter([ + // Mandatory policies + $passwordPolicies = [ 'required', $existing ? 'sometimes' : '', - 'min:8', - new StringHasNumberOrSpecialCharacter(), - new StringHasAtLeastOneUpperCaseCharacter(), - ]); + ]; + // Configurable policies + $passwordRules = Password::min(config('password-policies.minimum_length')); + if (config('password-policies.maximum_length')) { + $passwordPolicies[] = 'max:' . config('password-policies.maximum_length'); + } + if (config('password-policies.numbers')) { + $passwordRules->numbers(); + } + if (config('password-policies.uppercase')) { + $passwordPolicies[] = new StringHasAtLeastOneUpperCaseCharacter(); + } + if (config('password-policies.special')) { + $passwordRules->symbols(); + } + $passwordPolicies[] = $passwordRules; + + return $passwordPolicies; } /** diff --git a/config/password-policies.php b/config/password-policies.php new file mode 100644 index 0000000000..1bb8d9cefa --- /dev/null +++ b/config/password-policies.php @@ -0,0 +1,11 @@ + env('PASSWORD_POLICY_MINIMUM_LENGTH', 8), + 'maximum_length' => env('PASSWORD_POLICY_MAXIMUM_LENGTH', null), + 'numbers' => env('PASSWORD_POLICY_NUMBERS', true), + 'uppercase' => env('PASSWORD_POLICY_UPPERCASE', true), + 'special' => env('PASSWORD_POLICY_SPECIAL', true), + //'expiration_days' => env('PASSWORD_POLICY_EXPIRATION_DAYS', 0), // 0 never expires + 'login_attempts' => env('PASSWORD_POLICY_LOGIN_ATTEMPTS', 5), +]; diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index f00ae382df..c34292b5cb 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -396,12 +396,6 @@ delete this.formData.password; return true } - if (this.formData.password.trim().length > 0 && this.formData.password.trim().length < 8) { - this.errors.password = ['Password must be at least 8 characters'] - this.password = '' - this.submitted = false - return false - } if (this.formData.password !== this.formData.confPassword) { this.errors.password = ['Passwords must match'] this.password = '' diff --git a/resources/views/auth/passwords/change.blade.php b/resources/views/auth/passwords/change.blade.php index 0627e07982..5b5ddca593 100644 --- a/resources/views/auth/passwords/change.blade.php +++ b/resources/views/auth/passwords/change.blade.php @@ -45,7 +45,7 @@ 'v-bind:class' => '{\'form-control\':true, \'is-invalid\':errors.password}']) !!} - @{{ errors.password[0] }} + @{{ error }}
{!!Form::label('confpassword', __('Confirm Password'))!!}* @@ -94,11 +94,6 @@ }); }, validatePassword() { - if (this.$refs.passwordStrength.strength.score < 2) { - this.errors.password = ['Password is too weak'] - return false - } - if (!this.formData.password && !this.formData.confpassword) { return false; } @@ -107,12 +102,6 @@ return false } - if (this.formData.password.trim().length > 0 && this.formData.password.trim().length < 8) { - this.errors.password = ['Password must be at least 8 characters'] - this.formData.password = '' - return false - } - if (this.formData.password !== this.formData.confpassword) { this.errors.password = ['Passwords must match'] return false diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index 56b227325b..994ce50737 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -151,12 +151,6 @@ delete this.formData.password; return true } - if (this.formData.password.trim().length > 0 && this.formData.password.trim().length < 8) { - this.errors.password = ['Password must be at least 8 characters'] - this.password = '' - this.submitted = false - return false - } if (this.formData.password !== this.formData.confPassword) { this.errors.password = ['Passwords must match'] this.password = '' diff --git a/resources/views/shared/users/sidebar.blade.php b/resources/views/shared/users/sidebar.blade.php index cb97917c33..513b4d10c1 100644 --- a/resources/views/shared/users/sidebar.blade.php +++ b/resources/views/shared/users/sidebar.blade.php @@ -22,8 +22,8 @@
-
- +
+
{{__('Login Information')}}
@@ -54,7 +54,7 @@ {!! Form::label('confPassword', __('Confirm Password')) !!} {!! Form::password('confPassword', ['id' => 'confPassword', 'rows' => 4, 'class'=> 'form-control', 'v-model' => 'formData.confPassword', 'autocomplete' => 'new-password', 'v-bind:class' => '{\'form-control\':true, \'is-invalid\':errors.password}']) !!} - +
@cannot('edit-user-and-password')
From 6a660a6d8d9700c81822cff185afff3f40300487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Fri, 24 Nov 2023 14:11:03 +0000 Subject: [PATCH 2/8] FOUR-11415 Password Policy Configuration - Fix observations made by Dave --- resources/views/shared/users/sidebar.blade.php | 3 ++- tests/Feature/Api/UsersTest.php | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/resources/views/shared/users/sidebar.blade.php b/resources/views/shared/users/sidebar.blade.php index 513b4d10c1..361ce4d42a 100644 --- a/resources/views/shared/users/sidebar.blade.php +++ b/resources/views/shared/users/sidebar.blade.php @@ -54,7 +54,8 @@ {!! Form::label('confPassword', __('Confirm Password')) !!} {!! Form::password('confPassword', ['id' => 'confPassword', 'rows' => 4, 'class'=> 'form-control', 'v-model' => 'formData.confPassword', 'autocomplete' => 'new-password', 'v-bind:class' => '{\'form-control\':true, \'is-invalid\':errors.password}']) !!} - +
@cannot('edit-user-and-password')
diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index 686f6ac374..45fe563df8 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -61,7 +61,7 @@ public function getUpdatedData() 'timezone' => $faker->timezone(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), 'birthdate' => $faker->dateTimeThisCentury()->format('Y-m-d'), - 'password' => $faker->sentence(10), + 'password' => $faker->sentence(8) . 'A_' . '1', ]; } @@ -100,7 +100,7 @@ public function testCreateUser() 'lastname' => 'name', 'email' => $faker->email(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), - 'password' => $faker->sentence(10), + 'password' => $faker->password(8) . 'A_' . '1', ]); // Validate the header status code @@ -602,9 +602,9 @@ public function testCreateWithoutPassword() $response = $this->apiCall('POST', self::API_TEST_URL, $payload); $response->assertStatus(422); $json = $response->json(); - $this->assertEquals('The Password field must be at least 8 characters.', $json['errors']['password'][0]); + $this->assertTrue(in_array('The Password field must be at least 8 characters.', $json['errors']['password'])); - $payload['password'] = 'Abc12345'; + $payload['password'] = 'Abc12345_'; $response = $this->apiCall('POST', self::API_TEST_URL, $payload); $response->assertStatus(201); $json = $response->json(); @@ -616,9 +616,9 @@ public function testCreateWithoutPassword() $response = $this->apiCall('PUT', route('api.users.update', $userId), $payload); $response->assertStatus(422); $json = $response->json(); - $this->assertEquals('The Password field must be at least 8 characters.', $json['errors']['password'][0]); + $this->assertTrue(in_array('The Password field must be at least 8 characters.', $json['errors']['password'])); - $payload['password'] = 'Abc12345'; + $payload['password'] = 'Abc12345_'; $response = $this->apiCall('PUT', route('api.users.update', $userId), $payload); $response->assertStatus(204); @@ -673,7 +673,7 @@ public function testCreateUserValidateUsername() 'lastname' => $faker->lastName(), 'email' => $faker->email(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), - 'password' => $faker->sentence(10), + 'password' => $faker->sentence(8) . 'A_' . '1', ]); // Validate the header status code $response->assertStatus(201); From 91a198ce4ed307e43ed8fe76ffa4a14f575e31c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Fri, 24 Nov 2023 15:11:50 +0000 Subject: [PATCH 3/8] FOUR-11415 Password Policy Configuration - Fix unit test ChangePassword --- tests/Feature/Api/ChangePasswordTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Api/ChangePasswordTest.php b/tests/Feature/Api/ChangePasswordTest.php index b30b08644a..80aa9e3ffe 100644 --- a/tests/Feature/Api/ChangePasswordTest.php +++ b/tests/Feature/Api/ChangePasswordTest.php @@ -45,8 +45,9 @@ public function testUserPasswordChangeWithInvalidPassword() 'confpassword' => 'ProcessMaker', ]); - $response->assertStatus(422) - ->assertSeeText('The password must contain either a number or a special character'); + $response->assertStatus(422); + $json = $response->json(); + $this->assertTrue(in_array('The Password field must contain at least one symbol.', $json['errors']['password'])); // Validate updated user password changed $updatedUser = User::where('id', $user->id)->first(); @@ -72,8 +73,8 @@ public function testUserChangePasswordMustSetFlagToFalse() // Post data with new password $response = $this->apiCall('PUT', self::API_TEST_URL, [ - 'password' => 'ProcessMaker1', - 'confpassword' => 'ProcessMaker1', + 'password' => 'ProcessMaker1_', + 'confpassword' => 'ProcessMaker1_', ]); // Validate the header status code From 6557413b667c1ebf558c25f27401ec476195b404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Wed, 22 Nov 2023 14:08:24 +0000 Subject: [PATCH 4/8] FOUR-11415 Password Policy Configuration --- .../Http/Controllers/Auth/LoginController.php | 3 +++ ProcessMaker/Models/User.php | 27 ++++++++++++++----- config/password-policies.php | 11 ++++++++ resources/views/admin/users/edit.blade.php | 6 ----- .../views/auth/passwords/change.blade.php | 13 +-------- resources/views/profile/edit.blade.php | 6 ----- .../views/shared/users/sidebar.blade.php | 6 ++--- 7 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 config/password-policies.php diff --git a/ProcessMaker/Http/Controllers/Auth/LoginController.php b/ProcessMaker/Http/Controllers/Auth/LoginController.php index d4012a6877..45e8f64f58 100644 --- a/ProcessMaker/Http/Controllers/Auth/LoginController.php +++ b/ProcessMaker/Http/Controllers/Auth/LoginController.php @@ -38,6 +38,8 @@ class LoginController extends Controller */ protected $redirectTo = '/'; + protected $maxAttempts; + /** * Create a new controller instance. * @@ -46,6 +48,7 @@ class LoginController extends Controller public function __construct() { $this->middleware('guest')->except(['logout', 'beforeLogout', 'keepAlive']); + $this->maxAttempts = (int) config('password-policies.login_attempts'); } /** diff --git a/ProcessMaker/Models/User.php b/ProcessMaker/Models/User.php index 8e2cd4e8ef..541e91ba8c 100644 --- a/ProcessMaker/Models/User.php +++ b/ProcessMaker/Models/User.php @@ -8,12 +8,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; use Laravel\Passport\HasApiTokens; use ProcessMaker\Models\EmptyModel; use ProcessMaker\Notifications\ResetPassword as ResetPasswordNotification; use ProcessMaker\Query\Traits\PMQL; use ProcessMaker\Rules\StringHasAtLeastOneUpperCaseCharacter; -use ProcessMaker\Rules\StringHasNumberOrSpecialCharacter; use ProcessMaker\Traits\Exportable; use ProcessMaker\Traits\HasAuthorization; use ProcessMaker\Traits\HideSystemResources; @@ -183,13 +183,28 @@ public static function rules(self $existing = null) */ public static function passwordRules(self $existing = null) { - return array_filter([ + // Mandatory policies + $passwordPolicies = [ 'required', $existing ? 'sometimes' : '', - 'min:8', - new StringHasNumberOrSpecialCharacter(), - new StringHasAtLeastOneUpperCaseCharacter(), - ]); + ]; + // Configurable policies + $passwordRules = Password::min(config('password-policies.minimum_length')); + if (config('password-policies.maximum_length')) { + $passwordPolicies[] = 'max:' . config('password-policies.maximum_length'); + } + if (config('password-policies.numbers')) { + $passwordRules->numbers(); + } + if (config('password-policies.uppercase')) { + $passwordPolicies[] = new StringHasAtLeastOneUpperCaseCharacter(); + } + if (config('password-policies.special')) { + $passwordRules->symbols(); + } + $passwordPolicies[] = $passwordRules; + + return $passwordPolicies; } /** diff --git a/config/password-policies.php b/config/password-policies.php new file mode 100644 index 0000000000..1bb8d9cefa --- /dev/null +++ b/config/password-policies.php @@ -0,0 +1,11 @@ + env('PASSWORD_POLICY_MINIMUM_LENGTH', 8), + 'maximum_length' => env('PASSWORD_POLICY_MAXIMUM_LENGTH', null), + 'numbers' => env('PASSWORD_POLICY_NUMBERS', true), + 'uppercase' => env('PASSWORD_POLICY_UPPERCASE', true), + 'special' => env('PASSWORD_POLICY_SPECIAL', true), + //'expiration_days' => env('PASSWORD_POLICY_EXPIRATION_DAYS', 0), // 0 never expires + 'login_attempts' => env('PASSWORD_POLICY_LOGIN_ATTEMPTS', 5), +]; diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index f00ae382df..c34292b5cb 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -396,12 +396,6 @@ delete this.formData.password; return true } - if (this.formData.password.trim().length > 0 && this.formData.password.trim().length < 8) { - this.errors.password = ['Password must be at least 8 characters'] - this.password = '' - this.submitted = false - return false - } if (this.formData.password !== this.formData.confPassword) { this.errors.password = ['Passwords must match'] this.password = '' diff --git a/resources/views/auth/passwords/change.blade.php b/resources/views/auth/passwords/change.blade.php index 0627e07982..5b5ddca593 100644 --- a/resources/views/auth/passwords/change.blade.php +++ b/resources/views/auth/passwords/change.blade.php @@ -45,7 +45,7 @@ 'v-bind:class' => '{\'form-control\':true, \'is-invalid\':errors.password}']) !!}
- @{{ errors.password[0] }} + @{{ error }}
{!!Form::label('confpassword', __('Confirm Password'))!!}* @@ -94,11 +94,6 @@ }); }, validatePassword() { - if (this.$refs.passwordStrength.strength.score < 2) { - this.errors.password = ['Password is too weak'] - return false - } - if (!this.formData.password && !this.formData.confpassword) { return false; } @@ -107,12 +102,6 @@ return false } - if (this.formData.password.trim().length > 0 && this.formData.password.trim().length < 8) { - this.errors.password = ['Password must be at least 8 characters'] - this.formData.password = '' - return false - } - if (this.formData.password !== this.formData.confpassword) { this.errors.password = ['Passwords must match'] return false diff --git a/resources/views/profile/edit.blade.php b/resources/views/profile/edit.blade.php index 56b227325b..994ce50737 100644 --- a/resources/views/profile/edit.blade.php +++ b/resources/views/profile/edit.blade.php @@ -151,12 +151,6 @@ delete this.formData.password; return true } - if (this.formData.password.trim().length > 0 && this.formData.password.trim().length < 8) { - this.errors.password = ['Password must be at least 8 characters'] - this.password = '' - this.submitted = false - return false - } if (this.formData.password !== this.formData.confPassword) { this.errors.password = ['Passwords must match'] this.password = '' diff --git a/resources/views/shared/users/sidebar.blade.php b/resources/views/shared/users/sidebar.blade.php index cb97917c33..513b4d10c1 100644 --- a/resources/views/shared/users/sidebar.blade.php +++ b/resources/views/shared/users/sidebar.blade.php @@ -22,8 +22,8 @@
-
- +
+
{{__('Login Information')}}
@@ -54,7 +54,7 @@ {!! Form::label('confPassword', __('Confirm Password')) !!} {!! Form::password('confPassword', ['id' => 'confPassword', 'rows' => 4, 'class'=> 'form-control', 'v-model' => 'formData.confPassword', 'autocomplete' => 'new-password', 'v-bind:class' => '{\'form-control\':true, \'is-invalid\':errors.password}']) !!} - +
@cannot('edit-user-and-password')
From b68a2b93cfeaf6de51c60786cc5670f7fa3f28d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Fri, 24 Nov 2023 14:11:03 +0000 Subject: [PATCH 5/8] FOUR-11415 Password Policy Configuration - Fix observations made by Dave --- resources/views/shared/users/sidebar.blade.php | 3 ++- tests/Feature/Api/UsersTest.php | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/resources/views/shared/users/sidebar.blade.php b/resources/views/shared/users/sidebar.blade.php index 513b4d10c1..361ce4d42a 100644 --- a/resources/views/shared/users/sidebar.blade.php +++ b/resources/views/shared/users/sidebar.blade.php @@ -54,7 +54,8 @@ {!! Form::label('confPassword', __('Confirm Password')) !!} {!! Form::password('confPassword', ['id' => 'confPassword', 'rows' => 4, 'class'=> 'form-control', 'v-model' => 'formData.confPassword', 'autocomplete' => 'new-password', 'v-bind:class' => '{\'form-control\':true, \'is-invalid\':errors.password}']) !!} - +
@cannot('edit-user-and-password')
diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index 686f6ac374..45fe563df8 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -61,7 +61,7 @@ public function getUpdatedData() 'timezone' => $faker->timezone(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), 'birthdate' => $faker->dateTimeThisCentury()->format('Y-m-d'), - 'password' => $faker->sentence(10), + 'password' => $faker->sentence(8) . 'A_' . '1', ]; } @@ -100,7 +100,7 @@ public function testCreateUser() 'lastname' => 'name', 'email' => $faker->email(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), - 'password' => $faker->sentence(10), + 'password' => $faker->password(8) . 'A_' . '1', ]); // Validate the header status code @@ -602,9 +602,9 @@ public function testCreateWithoutPassword() $response = $this->apiCall('POST', self::API_TEST_URL, $payload); $response->assertStatus(422); $json = $response->json(); - $this->assertEquals('The Password field must be at least 8 characters.', $json['errors']['password'][0]); + $this->assertTrue(in_array('The Password field must be at least 8 characters.', $json['errors']['password'])); - $payload['password'] = 'Abc12345'; + $payload['password'] = 'Abc12345_'; $response = $this->apiCall('POST', self::API_TEST_URL, $payload); $response->assertStatus(201); $json = $response->json(); @@ -616,9 +616,9 @@ public function testCreateWithoutPassword() $response = $this->apiCall('PUT', route('api.users.update', $userId), $payload); $response->assertStatus(422); $json = $response->json(); - $this->assertEquals('The Password field must be at least 8 characters.', $json['errors']['password'][0]); + $this->assertTrue(in_array('The Password field must be at least 8 characters.', $json['errors']['password'])); - $payload['password'] = 'Abc12345'; + $payload['password'] = 'Abc12345_'; $response = $this->apiCall('PUT', route('api.users.update', $userId), $payload); $response->assertStatus(204); @@ -673,7 +673,7 @@ public function testCreateUserValidateUsername() 'lastname' => $faker->lastName(), 'email' => $faker->email(), 'status' => $faker->randomElement(['ACTIVE', 'INACTIVE']), - 'password' => $faker->sentence(10), + 'password' => $faker->sentence(8) . 'A_' . '1', ]); // Validate the header status code $response->assertStatus(201); From d05e95c8db7b3e3355333ac29d045bd58a201d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Fri, 24 Nov 2023 15:11:50 +0000 Subject: [PATCH 6/8] FOUR-11415 Password Policy Configuration - Fix unit test ChangePassword --- tests/Feature/Api/ChangePasswordTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Api/ChangePasswordTest.php b/tests/Feature/Api/ChangePasswordTest.php index b30b08644a..80aa9e3ffe 100644 --- a/tests/Feature/Api/ChangePasswordTest.php +++ b/tests/Feature/Api/ChangePasswordTest.php @@ -45,8 +45,9 @@ public function testUserPasswordChangeWithInvalidPassword() 'confpassword' => 'ProcessMaker', ]); - $response->assertStatus(422) - ->assertSeeText('The password must contain either a number or a special character'); + $response->assertStatus(422); + $json = $response->json(); + $this->assertTrue(in_array('The Password field must contain at least one symbol.', $json['errors']['password'])); // Validate updated user password changed $updatedUser = User::where('id', $user->id)->first(); @@ -72,8 +73,8 @@ public function testUserChangePasswordMustSetFlagToFalse() // Post data with new password $response = $this->apiCall('PUT', self::API_TEST_URL, [ - 'password' => 'ProcessMaker1', - 'confpassword' => 'ProcessMaker1', + 'password' => 'ProcessMaker1_', + 'confpassword' => 'ProcessMaker1_', ]); // Validate the header status code From 85c9f3fd2c57a46cc32473670a2817ee5909f092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Wed, 29 Nov 2023 19:15:09 +0000 Subject: [PATCH 7/8] FOUR-12572 Password Policy Configuration - Password expiration --- .../Api/ChangePasswordController.php | 5 ++++ .../Http/Controllers/Api/UserController.php | 9 ++++++ .../Middleware/VerifyChangePasswordNeeded.php | 18 ++++++++++++ ProcessMaker/Models/User.php | 1 + config/password-policies.php | 2 +- ...sword_changed_at_column_to_users_table.php | 28 +++++++++++++++++++ resources/lang/de.json | 3 +- resources/lang/en.json | 1 + resources/lang/es.json | 3 +- resources/lang/fr.json | 3 +- 10 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2023_11_28_200725_add_password_changed_at_column_to_users_table.php diff --git a/ProcessMaker/Http/Controllers/Api/ChangePasswordController.php b/ProcessMaker/Http/Controllers/Api/ChangePasswordController.php index e14ea0f0d7..4083cabcaf 100644 --- a/ProcessMaker/Http/Controllers/Api/ChangePasswordController.php +++ b/ProcessMaker/Http/Controllers/Api/ChangePasswordController.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Http\Controllers\Api; +use Carbon\Carbon; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -35,6 +36,10 @@ public function update(Request $request) $user->setAttribute('password', Hash::make($request->json('password'))); $user->setAttribute('force_change_password', 0); + $user->setAttribute('password_changed_at', Carbon::now()->toDateTimeString()); + + // Remove login error message related to password expired if exists + session()->forget('login-error'); try { $user = $user->save(); diff --git a/ProcessMaker/Http/Controllers/Api/UserController.php b/ProcessMaker/Http/Controllers/Api/UserController.php index 59aedb7a08..caf2c72aca 100644 --- a/ProcessMaker/Http/Controllers/Api/UserController.php +++ b/ProcessMaker/Http/Controllers/Api/UserController.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Http\Controllers\Api; +use Carbon\Carbon; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -167,6 +168,10 @@ public function store(Request $request) if (isset($fields['password'])) { $fields['password'] = Hash::make($fields['password']); + $fields['password_changed_at'] = Carbon::now()->toDateTimeString(); + + // Remove login error message related to password expired if exists + session()->forget('login-error'); } $user->fill($fields); @@ -301,6 +306,10 @@ public function update(User $user, Request $request) $fields = $request->json()->all(); if (isset($fields['password'])) { $fields['password'] = Hash::make($fields['password']); + $fields['password_changed_at'] = Carbon::now()->toDateTimeString(); + + // Remove login error message related to password expired if exists + session()->forget('login-error'); } $original = $user->getOriginal(); $user->fill($fields); diff --git a/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php b/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php index 018d853a70..92d720aeca 100644 --- a/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php +++ b/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Http\Middleware; +use Carbon\Carbon; use Closure; use Illuminate\Support\Facades\Auth; @@ -20,6 +21,14 @@ public function handle($request, Closure $next) return redirect()->route('password.change'); } + if ($this->checkPasswordExpiration()) { + // Set the error message + session()->put('login-error', _('Your password has expired.')); + + // Redirect to change password screen + return redirect()->route('password.change'); + } + return $next($request); } @@ -27,4 +36,13 @@ public function checkForForceChangePassword() { return Auth::user() && Auth::user()->force_change_password; } + + public function checkPasswordExpiration() + { + $validationRequired = config('password-policies.expiration_days') && + Auth::user() && Auth::user()->password_changed_at; + + return $validationRequired && + (Carbon::now()->diffInDays(Auth::user()->password_changed_at) >= config('password-policies.expiration_days')); + } } diff --git a/ProcessMaker/Models/User.php b/ProcessMaker/Models/User.php index 541e91ba8c..fdbb48213b 100644 --- a/ProcessMaker/Models/User.php +++ b/ProcessMaker/Models/User.php @@ -122,6 +122,7 @@ class User extends Authenticatable implements HasMedia 'manager_id', 'schedule', 'force_change_password', + 'password_changed_at', ]; protected $appends = [ diff --git a/config/password-policies.php b/config/password-policies.php index 1bb8d9cefa..a0bbb07490 100644 --- a/config/password-policies.php +++ b/config/password-policies.php @@ -6,6 +6,6 @@ 'numbers' => env('PASSWORD_POLICY_NUMBERS', true), 'uppercase' => env('PASSWORD_POLICY_UPPERCASE', true), 'special' => env('PASSWORD_POLICY_SPECIAL', true), - //'expiration_days' => env('PASSWORD_POLICY_EXPIRATION_DAYS', 0), // 0 never expires + 'expiration_days' => env('PASSWORD_POLICY_EXPIRATION_DAYS', null), 'login_attempts' => env('PASSWORD_POLICY_LOGIN_ATTEMPTS', 5), ]; diff --git a/database/migrations/2023_11_28_200725_add_password_changed_at_column_to_users_table.php b/database/migrations/2023_11_28_200725_add_password_changed_at_column_to_users_table.php new file mode 100644 index 0000000000..c40f6fa2c9 --- /dev/null +++ b/database/migrations/2023_11_28_200725_add_password_changed_at_column_to_users_table.php @@ -0,0 +1,28 @@ +timestamp('password_changed_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('password_changed_at'); + }); + } +}; diff --git a/resources/lang/de.json b/resources/lang/de.json index b8a0e92c0a..106ee7b5c9 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -1801,5 +1801,6 @@ "This environment already contains an older version of the {{ item }} named '{{ name }}.'": "Diese Umgebung enthält bereits eine ältere Version des {{ item }} namens '{{ name }}'.", "This environment already contains the same version of the {{ item }} named '{{ name }}.'": "Diese Umgebung enthält bereits die gleiche Version des {{ item }} namens '{{ name }}'.", "Visit our Gallery for more Templates": "Besuchen Sie unsere Galerie für mehr Vorlagen", - "Start a new process from a blank canvas, a text description, or a preset template.": "Starten Sie einen neuen Prozess von einer leeren Leinwand, einer Textbeschreibung oder einer voreingestellten Vorlage." + "Start a new process from a blank canvas, a text description, or a preset template.": "Starten Sie einen neuen Prozess von einer leeren Leinwand, einer Textbeschreibung oder einer voreingestellten Vorlage.", + "Your password has expired.": "Your password has expired." } \ No newline at end of file diff --git a/resources/lang/en.json b/resources/lang/en.json index b7e37e07ca..4c361672e6 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1870,6 +1870,7 @@ "Your account has been timed out for security.": "Your account has been timed out for security.", "Your password has been reset!": "Your password has been reset!", "Your password has been updated.": "Your password has been updated.", + "Your password has expired.": "Your password has expired.", "Your PMQL contains invalid syntax.": "Your PMQL contains invalid syntax.", "Your PMQL search could not be completed.": "Your PMQL search could not be completed.", "Your profile was saved.": "Your profile was saved.", diff --git a/resources/lang/es.json b/resources/lang/es.json index 7ce552475a..ccc3d6ceb5 100644 --- a/resources/lang/es.json +++ b/resources/lang/es.json @@ -1802,5 +1802,6 @@ "This environment already contains an older version of the {{ item }} named '{{ name }}.'": "Este entorno ya contiene una versión más antigua del {{ item }} llamado '{{ name }}'.", "This environment already contains the same version of the {{ item }} named '{{ name }}.'": "Este entorno ya contiene la misma versión del {{ item }} llamado '{{ name }}'.", "Visit our Gallery for more Templates": "Visita nuestra Galería para más Plantillas", - "Start a new process from a blank canvas, a text description, or a preset template.": "Inicie un nuevo proceso desde un lienzo en blanco, una descripción de texto o una plantilla preestablecida." + "Start a new process from a blank canvas, a text description, or a preset template.": "Inicie un nuevo proceso desde un lienzo en blanco, una descripción de texto o una plantilla preestablecida.", + "Your password has expired.": "Tu contraseña ha expirado." } \ No newline at end of file diff --git a/resources/lang/fr.json b/resources/lang/fr.json index e00faf5a98..4786805050 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -1801,5 +1801,6 @@ "This environment already contains an older version of the {{ item }} named '{{ name }}.'": "Cet environnement contient déjà une version plus ancienne de {{ item }} nommé '{{ name }}'.", "This environment already contains the same version of the {{ item }} named '{{ name }}.'": "Cet environnement contient déjà la même version de l'{{ item }} nommé '{{ name }}'.", "Visit our Gallery for more Templates": "Visitez notre Galerie pour plus de Modèles", - "Start a new process from a blank canvas, a text description, or a preset template.": "Démarrez un nouveau processus à partir d'une toile vierge, d'une description textuelle ou d'un modèle prédéfini." + "Start a new process from a blank canvas, a text description, or a preset template.": "Démarrez un nouveau processus à partir d'une toile vierge, d'une description textuelle ou d'un modèle prédéfini.", + "Your password has expired.": "Your password has expired." } \ No newline at end of file From 7aba536d6bd18bc567bf3f08812be477127eeb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20Cesar=20Laura=20Avenda=C3=B1o?= Date: Wed, 29 Nov 2023 19:30:04 +0000 Subject: [PATCH 8/8] FOUR-12572 Password Policy Configuration - Password expiration - Fix code smell --- ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php b/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php index 92d720aeca..08754c3849 100644 --- a/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php +++ b/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php @@ -43,6 +43,7 @@ public function checkPasswordExpiration() Auth::user() && Auth::user()->password_changed_at; return $validationRequired && - (Carbon::now()->diffInDays(Auth::user()->password_changed_at) >= config('password-policies.expiration_days')); + (Carbon::now()->diffInDays(Auth::user()->password_changed_at) >= + config('password-policies.expiration_days')); } }