From cd5f1cb66e256554ed425e3e7b9423bd6a52914c Mon Sep 17 00:00:00 2001 From: Mohammad Hafijul Islam Date: Tue, 17 Oct 2023 01:57:45 +0600 Subject: [PATCH] LP-5 Reset password using otp and temporary password and reset link is successful --- config/auth.php | 4 +- lang/en/messages.php | 4 + .../Controllers/PasswordResetController.php | 72 +++++++++-------- src/Http/Requests/LoginRequest.php | 2 +- src/Http/Requests/PasswordResetRequest.php | 6 +- src/Interfaces/OneTimePinRepository.php | 11 ++- .../Eloquent/OneTimePinRepository.php | 22 +++-- src/Services/PasswordResetService.php | 81 ++++++++++++++----- 8 files changed, 128 insertions(+), 74 deletions(-) diff --git a/config/auth.php b/config/auth.php index fefe4cd..d5abfb7 100644 --- a/config/auth.php +++ b/config/auth.php @@ -24,8 +24,8 @@ | Exclude auth fields | Example: reset_link, otp, temporary_password */ - 'self_password_reset' => false, - 'password_reset_method' => 'temporary_password', + 'self_password_reset' => true, + 'password_reset_method' => 'otp', 'temporary_password_length' => 8, diff --git a/lang/en/messages.php b/lang/en/messages.php index df92eac..8cd0cb0 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -20,9 +20,13 @@ 'warning' => 'Sorry, You entered wrong credentials! You already attempt :attempt. times out of :threshold', 'lockup' => 'Sorry, Your Account is has been Locked. Please contact support!', 'reset' => [ + 'success' => 'Your account password reset successful.', 'temporary_password' => 'We have send you a temporary password. Please log into you account with credentials.', 'reset_link' => 'We have send you a password reset link. Please follow that instruction to proceed.', 'otp' => 'We have send you a verification code. Please verify your account with given code.', 'notification_failed' => 'There is a error while processing your request. Please try again later', + 'invalid_token' => 'The reset link token is invalid. Please try again later.', + 'expired_token' => 'The password reset token has expired. Please try again later.', + 'user_not_found' => 'Unable to find valid user associated with this token', ] ]; diff --git a/src/Http/Controllers/PasswordResetController.php b/src/Http/Controllers/PasswordResetController.php index 0def1da..9333e1d 100644 --- a/src/Http/Controllers/PasswordResetController.php +++ b/src/Http/Controllers/PasswordResetController.php @@ -5,19 +5,21 @@ use Fintech\Auth\Facades\Auth; use Fintech\Auth\Http\Requests\ForgotPasswordRequest; use Fintech\Auth\Http\Requests\PasswordResetRequest; +use Fintech\Core\Exceptions\UpdateOperationException; use Fintech\Core\Traits\ApiResponseTrait; use Illuminate\Http\JsonResponse; use Illuminate\Routing\Controller; -use Illuminate\Support\Facades\Password; -use Illuminate\Validation\ValidationException; class PasswordResetController extends Controller { use ApiResponseTrait; /** - * Handle an incoming password reset link request. - * + * @lrd:start + * This api receive `login_id` as unique user then as per configuration + * and send temporary password or reset link or One Time Pin verifcation + * to proceed + * @lrd:end * @param ForgotPasswordRequest $request * @return JsonResponse * @throws \Exception @@ -36,7 +38,7 @@ public function store(ForgotPasswordRequest $request): JsonResponse return $this->failed(__('auth::messages.failed')); } - $response = Auth::passwordReset()->notify($attemptUser->first()); + $response = Auth::passwordReset()->notifyUser($attemptUser->first()); if (!$response['status']) { throw new \Exception($response['message']); @@ -51,39 +53,43 @@ public function store(ForgotPasswordRequest $request): JsonResponse } /** - * Handle an incoming new password request. - * - * @throws ValidationException + * @LRDparam password_confirmation string|required|min:8 + * @lrd:start + * This api receive `token`, `password` & `password_confirmation` to reset + * user with given password. If otp or token didn't match throws exception + * to proceed + * @lrd:end + * @param PasswordResetRequest $request + * @return JsonResponse */ public function update(PasswordResetRequest $request): JsonResponse { - $request->validate([ - 'token' => ['required'], - 'login_id' => ['required', 'email'], - 'password' => ['required', 'confirmed', 'min:8'], - ]); - - // Here we will attempt to reset the user's password. If it is successful we - // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. - $status = Password::reset( - $request->only('email', 'password', 'password_confirmation', 'token'), - function ($user) use ($request) { - $user->forceFill([ - 'password' => Hash::make($request->password), - 'remember_token' => Str::random(60), - ])->save(); - - event(new PasswordReset($user)); + $passwordField = config('fintech.auth.password_field', 'password'); + + $token = $request->input('token'); + + $password = $request->input($passwordField); + + try { + + $activeToken = Auth::passwordReset()->verifyToken($token); + + $targetedUser = Auth::user()->list([config('fintech.auth.auth_field', 'login_id'), $activeToken->email]); + + if ($targetedUser->isEmpty()) { + throw new \ErrorException(__('auth::messages.reset.user_not_found')); } - ); - if ($status != Password::PASSWORD_RESET) { - throw ValidationException::withMessages([ - 'email' => [__($status)], - ]); - } + $targetedUser = $targetedUser->first(); + + if (!Auth::user()->update($targetedUser->getKey(), [$passwordField => $password])) { + throw (new UpdateOperationException)->setModel(config('fintech.auth.user_model'), $targetedUser->getKey()); + } + + return $this->updated(__('auth::messages.reset.success')); - return response()->json(['status' => __($status)]); + } catch (\Exception $exception) { + return $this->failed($exception->getMessage()); + } } } diff --git a/src/Http/Requests/LoginRequest.php b/src/Http/Requests/LoginRequest.php index 64677b5..b338cb0 100644 --- a/src/Http/Requests/LoginRequest.php +++ b/src/Http/Requests/LoginRequest.php @@ -30,7 +30,7 @@ public function rules(): array config('fintech.auth.auth_field', 'login_id') => config('fintech.auth.auth_field_rules', ['required', 'string', 'min:6', 'max:255']), - config('fintech.auth.password_field', 'login_id') + config('fintech.auth.password_field', 'password') => config('fintech.auth.password_field_rules', ['required', 'string', 'min:8']) ]; } diff --git a/src/Http/Requests/PasswordResetRequest.php b/src/Http/Requests/PasswordResetRequest.php index c317594..34f2e16 100644 --- a/src/Http/Requests/PasswordResetRequest.php +++ b/src/Http/Requests/PasswordResetRequest.php @@ -11,7 +11,7 @@ class PasswordResetRequest extends FormRequest */ public function authorize(): bool { - return false; + return true; } /** @@ -22,7 +22,9 @@ public function authorize(): bool public function rules(): array { return [ - // + 'token' => ['required', 'string'], + config('fintech.auth.password_field', 'password') + => [...config('fintech.auth.password_field_rules', ['required', 'string', 'min:8']), 'confirmed'] ]; } } diff --git a/src/Interfaces/OneTimePinRepository.php b/src/Interfaces/OneTimePinRepository.php index 1a0937f..0ff97fa 100644 --- a/src/Interfaces/OneTimePinRepository.php +++ b/src/Interfaces/OneTimePinRepository.php @@ -16,11 +16,10 @@ public function create(string $authField, string $token); /** * Determine if a token record exists and is valid. * - * @param string $authField * @param string $token * @return bool */ - public function exists(string $authField, string $token); + public function exists(string $token); /** * Delete expired tokens. @@ -29,4 +28,12 @@ public function exists(string $authField, string $token); * @return void */ public function deleteExpired(string $authField); + + /** + * Delete existing old tokens. + * + * @param string $authField + * @return void + */ + public function delete(string $authField); } diff --git a/src/Repositories/Eloquent/OneTimePinRepository.php b/src/Repositories/Eloquent/OneTimePinRepository.php index 829e05a..0740797 100644 --- a/src/Repositories/Eloquent/OneTimePinRepository.php +++ b/src/Repositories/Eloquent/OneTimePinRepository.php @@ -12,9 +12,6 @@ */ class OneTimePinRepository implements InterfacesOneTimePinRepository { - /** - * @var \Illuminate\Contracts\Foundation\Application|Model|\Illuminate\Foundation\Application - */ private $model; public function __construct() @@ -60,11 +57,10 @@ public function create(string $authField, string $token) * * @param string $authField * @param string $token - * @return bool */ - public function exists(string $authField, string $token) + public function exists(string $token) { - return $this->model->where(['email' => $authField, 'token' => $token])->first(); + return $this->model->where(['token' => $token])->first(); } /** @@ -74,9 +70,9 @@ public function exists(string $authField, string $token) * @param string $token * @return void */ - private function recentlyCreatedToken(string $authField, string $token) + private function recentlyCreatedToken(string $token) { - $token = $this->exists($authField, $token); + $token = $this->exists($token); $expireInSeconds = config('auth.passwords.users.expire', 5) * 60; @@ -87,12 +83,14 @@ private function recentlyCreatedToken(string $authField, string $token) /** * Delete a token record. * - * @param CanResetPasswordContract $user + * @param string $authField * @return void */ - public function delete(CanResetPasswordContract $user) + public function delete(string $authField) { - + $this->model->where('email', $authField)->get()->each(function ($entry) { + $entry->delete(); + }); } /** @@ -103,7 +101,7 @@ public function delete(CanResetPasswordContract $user) */ public function deleteExpired(string $authField) { - $this->model->where('email', $authField)->get()->each(function ($entry) { + $this->model->where('created_at', '<', now()->subMinutes(config('auth.passwords.users.expire')))->get()->each(function ($entry) { $entry->delete(); }); } diff --git a/src/Services/PasswordResetService.php b/src/Services/PasswordResetService.php index 26d0149..3012a56 100644 --- a/src/Services/PasswordResetService.php +++ b/src/Services/PasswordResetService.php @@ -26,7 +26,7 @@ class PasswordResetService /** * @var string|null */ - private string $resetMethod; + private ?string $resetMethod; /** * OneTimePinService constructor. @@ -45,7 +45,7 @@ public function __construct(OneTimePinRepository $oneTimePinRepository) * @param $user * @return array */ - public function notify($user) + public function notifyUser($user) { try { @@ -79,6 +79,10 @@ public function notify($user) } } + /** + * @param $user + * @return array + */ private function viaTemporaryPassword($user): array { $password = Str::random(config('fintech.auth.temporary_password_length', 8)); @@ -104,6 +108,11 @@ private function viaTemporaryPassword($user): array ]; } + /** + * @param $user + * @return array + * @throws \Exception + */ private function viaResetLink($user) { $authField = $user->authField(); @@ -136,11 +145,16 @@ private function viaResetLink($user) ]; } + /** + * @param $user + * @return array + * @throws \Exception + */ private function viaOneTimePin($user) { $authField = $user->authField(); - $this->oneTimePinRepository->deleteExpired($authField); + $this->oneTimePinRepository->delete($authField); $min = (int)str_pad('1', config('fintech.auth.otp_length', 4), "0"); $max = (int)str_pad('9', config('fintech.auth.otp_length', 4), "9"); @@ -169,23 +183,46 @@ private function viaOneTimePin($user) ]; } - // public function find($id, $onlyTrashed = false) - // { - // return $this->oneTimePinRepository->find($id, $onlyTrashed); - // } - // - // public function update($id, array $inputs = []) - // { - // return $this->oneTimePinRepository->update($id, $inputs); - // } - // - // public function destroy($id) - // { - // return $this->oneTimePinRepository->delete($id); - // } - // - // public function restore($id) - // { - // return $this->oneTimePinRepository->restore($id); - // } + /** + * @param string $token + * @return bool + * @throws \Exception + */ + public function verifyToken(string $token) + { + + if ($this->resetMethod == 'reset_link') { + try { + $token = json_decode(base64_decode($token), true, 512, JSON_THROW_ON_ERROR); + + if (!is_array($token)) { + throw new \JsonException(__('auth::messages.reset.invalid_token')); + } + + $token = array_key_first($token); + + } catch (\Exception $exception) { + throw new \Exception($exception->getMessage()); + } + } + + + if ($passwordResetToken = $this->oneTimePinRepository->exists($token)) { + + $expireInSeconds = config('auth.passwords.users.expire', 5) * 60; + + $duration = now()->diffInSeconds($passwordResetToken->created_at); + + if ($expireInSeconds < $duration) { + + $this->oneTimePinRepository->delete($passwordResetToken->email); + + throw new \Exception(__('auth::messages.reset.expired_token')); + } + + return $passwordResetToken; + } + + throw new \Exception(__('auth::messages.reset.invalid_token')); + } }