diff --git a/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php b/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php index 28a3088ffc..6857df5da8 100644 --- a/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php +++ b/ProcessMaker/Http/Controllers/Auth/ResetPasswordController.php @@ -4,6 +4,8 @@ use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Http\Request; +use Illuminate\Validation\Rules\Password; +use Illuminate\Validation\ValidationException; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Models\User; @@ -76,7 +78,9 @@ public function showResetForm(Request $request, $token) */ public function reset(Request $request) { - $user = User::where('email', $request->input('email'))->first(); + $user = User::where('email', $request->input('email')) + ->where('username', $request->input('username')) + ->first(); if ($user && $user->status === 'BLOCKED') { return $this->sendResetFailedResponse($request, 'passwords.blocked'); @@ -86,6 +90,56 @@ public function reset(Request $request) return $this->sendResetFailedResponse($request, 'passwords.inactive'); } + if (!$user) { + return redirect()->back() + ->withInput($request->only('email', 'username')) + ->withErrors(['email' => __('passwords.account_not_found')]); + } + return $this->performPasswordReset($request); } + + /** + * Get the password reset validation rules. + */ + protected function rules(): array + { + return [ + 'token' => 'required', + 'email' => 'required|email', + 'username' => 'required|string', + 'password' => ['required', 'confirmed', Password::defaults()], + ]; + } + + /** + * Get the password reset credentials from the request. + * Include username so the broker resolves the same user as email+username (not email alone). + */ + protected function credentials(Request $request): array + { + return $request->only( + 'email', + 'username', + 'password', + 'password_confirmation', + 'token' + ); + } + + /** + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + protected function sendResetFailedResponse(Request $request, $response) + { + if ($request->wantsJson()) { + throw ValidationException::withMessages([ + 'email' => [trans($response)], + ]); + } + + return redirect()->back() + ->withInput($request->only('email', 'username')) + ->withErrors(['email' => trans($response)]); + } } diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php index 7e77c49c87..85fee78f68 100644 --- a/resources/lang/en/passwords.php +++ b/resources/lang/en/passwords.php @@ -20,5 +20,6 @@ 'user' => "We can't find a user with that email address.", 'blocked' => 'Your account has been blocked. Please contact your administrator.', 'inactive' => 'Your account is inactive. Please contact your administrator.', + 'account_not_found' => 'We could not find an account with the data provided.', ]; diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index 0059a8aa13..69d6f0b67c 100644 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -24,6 +24,10 @@ @endif +
+ + +
diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index 528323de78..ccf3a8fbcf 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -123,6 +123,7 @@ public function testResetPasswordRejectsBlockedUser(): void $response = $this->from(route('password.request'))->post('/password/reset', [ 'token' => 'will-not-be-used', 'email' => $user->email, + 'username' => $user->username, 'password' => 'NewPassword123!', 'password_confirmation' => 'NewPassword123!', ]); @@ -142,6 +143,7 @@ public function testResetPasswordRejectsInactiveUser(): void $response = $this->from(route('password.request'))->post('/password/reset', [ 'token' => 'will-not-be-used', 'email' => $user->email, + 'username' => $user->username, 'password' => 'NewPassword123!', 'password_confirmation' => 'NewPassword123!', ]); @@ -151,6 +153,35 @@ public function testResetPasswordRejectsInactiveUser(): void ]); } + public function testResetPasswordRejectsWrongUsername(): void + { + /** @var User $user */ + $user = User::factory()->create([ + 'email' => 'wrong-username-reset@example.com', + 'username' => 'correct_username', + 'status' => 'ACTIVE', + ]); + + /** @var ConcretePasswordBroker $broker */ + $broker = Password::broker(); + $token = $broker->createToken($user); + + $response = $this->from(route('password.reset', ['token' => $token]))->post('/password/reset', [ + 'token' => $token, + 'email' => $user->email, + 'username' => 'some_other_username', + 'password' => 'NewSecurePass123!', + 'password_confirmation' => 'NewSecurePass123!', + ]); + + $response->assertSessionHasErrors([ + 'email' => __('passwords.account_not_found'), + ]); + + $user->refresh(); + $this->assertTrue(Hash::check('oneOnlyPassword', $user->password)); + } + public function testResetPasswordUpdatesPasswordForActiveUser(): void { /** @var User $user */ @@ -167,6 +198,7 @@ public function testResetPasswordUpdatesPasswordForActiveUser(): void $response = $this->post('/password/reset', [ 'token' => $token, 'email' => $user->email, + 'username' => $user->username, 'password' => $plaintextSecret, 'password_confirmation' => $plaintextSecret, ]);