From d29b76917044d64baa08aca4ade8b730e7b9f1a3 Mon Sep 17 00:00:00 2001 From: Matthias Schuhmayer <38959016+mattamon@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:52:49 +0200 Subject: [PATCH] Reset password (#79) * Inital password reset commit * Revert composer.json * Add headers * Apply php-cs-fixer changes * Consistency check * Try to reformat login mail text * Align at column 24 * move text to const * Apply php-cs-fixer changes * Do not return json response, just return response --------- Co-authored-by: mattamon --- config/authorization.yaml | 12 ++- .../Request/ResetPasswordRequestBody.php | 34 +++++++ .../Controller/ResetPasswordController.php | 68 ++++++++++++++ src/Authorization/Event/LostPasswordEvent.php | 59 ++++++++++++ src/Authorization/RateLimiter/RateLimiter.php | 51 ++++++++++ .../RateLimiter/RateLimiterInterface.php | 27 ++++++ src/Authorization/Schema/ResetPassword.php | 42 +++++++++ src/Authorization/Service/MailService.php | 88 +++++++++++++++++ .../Service/MailServiceInterface.php | 27 ++++++ src/Authorization/Service/UserService.php | 94 +++++++++++++++++++ .../Service/UserServiceInterface.php | 33 +++++++ .../DomainConfigurationException.php | 31 ++++++ src/Exception/RateLimitException.php | 31 ++++++ src/Exception/SendMailException.php | 31 ++++++ src/Util/Constants/HttpResponseCodes.php | 1 + 15 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 src/Authorization/Attributes/Request/ResetPasswordRequestBody.php create mode 100644 src/Authorization/Controller/ResetPasswordController.php create mode 100644 src/Authorization/Event/LostPasswordEvent.php create mode 100644 src/Authorization/RateLimiter/RateLimiter.php create mode 100644 src/Authorization/RateLimiter/RateLimiterInterface.php create mode 100644 src/Authorization/Schema/ResetPassword.php create mode 100644 src/Authorization/Service/MailService.php create mode 100644 src/Authorization/Service/MailServiceInterface.php create mode 100644 src/Authorization/Service/UserService.php create mode 100644 src/Authorization/Service/UserServiceInterface.php create mode 100644 src/Exception/DomainConfigurationException.php create mode 100644 src/Exception/RateLimitException.php create mode 100644 src/Exception/SendMailException.php diff --git a/config/authorization.yaml b/config/authorization.yaml index d346910cf..9df4a9f24 100644 --- a/config/authorization.yaml +++ b/config/authorization.yaml @@ -14,4 +14,14 @@ services: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\TokenServiceInterface: - class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\TokenService \ No newline at end of file + class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\TokenService + + Pimcore\Bundle\StudioBackendBundle\Authorization\RateLimiter\RateLimiterInterface: + class: Pimcore\Bundle\StudioBackendBundle\Authorization\RateLimiter\RateLimiter + + Pimcore\Bundle\StudioBackendBundle\Authorization\Service\UserServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\UserService + + Pimcore\Bundle\StudioBackendBundle\Authorization\Service\MailServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\MailService + arguments: ['@Pimcore\Bundle\StudioBackendBundle\Setting\Provider\SystemSettingsProvider'] diff --git a/src/Authorization/Attributes/Request/ResetPasswordRequestBody.php b/src/Authorization/Attributes/Request/ResetPasswordRequestBody.php new file mode 100644 index 000000000..f84858a6f --- /dev/null +++ b/src/Authorization/Attributes/Request/ResetPasswordRequestBody.php @@ -0,0 +1,34 @@ +name] + )] + #[ResetPasswordRequestBody] + #[SuccessResponse] + #[DefaultResponses([ + HttpResponseCodes::TOO_MANY_REQUESTS + ])] + public function resetPassword(#[MapRequestPayload] ResetPassword $resetPassword): Response + { + $this->userService->resetPassword($resetPassword); + return new Response(); + } +} diff --git a/src/Authorization/Event/LostPasswordEvent.php b/src/Authorization/Event/LostPasswordEvent.php new file mode 100644 index 000000000..1c66b78de --- /dev/null +++ b/src/Authorization/Event/LostPasswordEvent.php @@ -0,0 +1,59 @@ +user; + } + + public function getLoginUrl(): string + { + return $this->loginUrl; + } + + /** + * Determines if lost password mail should be sent + */ + public function getSendMail(): bool + { + return $this->sendMail; + } + + /** + * Sets flag whether to send lost password mail or not + */ + public function setSendMail(bool $sendMail): LostPasswordEvent + { + $this->sendMail = $sendMail; + + return $this; + } +} diff --git a/src/Authorization/RateLimiter/RateLimiter.php b/src/Authorization/RateLimiter/RateLimiter.php new file mode 100644 index 000000000..09a7a363e --- /dev/null +++ b/src/Authorization/RateLimiter/RateLimiter.php @@ -0,0 +1,51 @@ +getCurrentRequest($this->requestStack); + + $limiter = $this->resetPasswordLimiter->create($request->getClientIp()); + + try { + $limiter->consume()->ensureAccepted(); + } catch (RateLimitExceededException) { + throw new RateLimitException(); + } + + } +} diff --git a/src/Authorization/RateLimiter/RateLimiterInterface.php b/src/Authorization/RateLimiter/RateLimiterInterface.php new file mode 100644 index 000000000..a80821a51 --- /dev/null +++ b/src/Authorization/RateLimiter/RateLimiterInterface.php @@ -0,0 +1,27 @@ +username; + } +} diff --git a/src/Authorization/Service/MailService.php b/src/Authorization/Service/MailService.php new file mode 100644 index 000000000..8e838790b --- /dev/null +++ b/src/Authorization/Service/MailService.php @@ -0,0 +1,88 @@ +systemSettingsProvider->getSettings(); + $this->domain = $settings['main_domain']; + } + + /** + * @throws DomainConfigurationException|SendMailException + */ + public function sendResetPasswordMail(UserInterface $user, string $token): void + { + if (!$this->domain) { + throw new DomainConfigurationException(); + } + + $context = $this->router->getContext(); + $context->setHost($this->domain); + + $loginUrl = $this->router->generate( + 'pimcore_admin_login', + [ + 'token' => $token, + 'reset' => 'true', + ], + UrlGeneratorInterface::ABSOLUTE_URL + ); + + /** @var User $user */ + $event = new LostPasswordEvent($user, $loginUrl); + $this->eventDispatcher->dispatch($event, LostPasswordEvent::EVENT_NAME); + + // only send mail if it wasn't prevented in event + if ($event->getSendMail()) { + try { + $mail = $this->toolResolver->getMail([$user->getEmail()], 'Pimcore lost password service'); + $mail->setIgnoreDebugMode(true); + $mail->text(sprintf(self::RESET_MAIL_TEXT, $loginUrl)); + $mail->send(); + } catch (Exception $exception) { + throw new SendMailException($exception->getMessage()); + } + } + } +} diff --git a/src/Authorization/Service/MailServiceInterface.php b/src/Authorization/Service/MailServiceInterface.php new file mode 100644 index 000000000..f7c54383d --- /dev/null +++ b/src/Authorization/Service/MailServiceInterface.php @@ -0,0 +1,27 @@ +rateLimiter->check(); + + $user = $this->userResolver->getByName($resetPassword->getUsername()); + + $userChecks = $this->userChecks($user); + + if (!$user || !$userChecks['success']) { + $this->pimcoreLogger->error('Reset password failed', ['error' => $userChecks['error']]); + return; + } + + $token = $this->authenticationResolver->generateTokenByUser($user); + + try { + $this->mailService->sendResetPasswordMail($user, $token); + } catch (DomainConfigurationException|SendMailException $exception) { + $this->pimcoreLogger->error('Error sending password recovery email', ['error' => $exception->getMessage()]); + throw $exception; + } + + } + + /** + * @return array + */ + private function userChecks(?UserInterface $user): array + { + if (!$user) { + return ['success' => false, 'error' => 'user_unknown']; + } + + if (!$user->getEmail() || !filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL)) { + return ['success' => false, 'error' => 'user_no_email_address']; + } + + if (!$user->isActive()) { + return ['success' => false, 'error' => 'user_inactive']; + } + + if (!$user->getPassword()) { + return ['success' => false, 'error' => 'user_no_password']; + } + + return ['success' => true, 'error' => '']; + } +} diff --git a/src/Authorization/Service/UserServiceInterface.php b/src/Authorization/Service/UserServiceInterface.php new file mode 100644 index 000000000..3b07c4885 --- /dev/null +++ b/src/Authorization/Service/UserServiceInterface.php @@ -0,0 +1,33 @@ +