request = $request; $this->pdm = new PersistentDataManager(); $this->userRepository = new UserRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->redirectUrl = $this->request->session()->has('redirect_after_login') ? $this->request->session()->get('redirect_after_login') : \Container::$routeCollection->getRoute('index')->generateLink(); } public function getLoginForm() { if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); } return new HtmlContent('login/login', ['redirectUrl' => '/' . $this->redirectUrl]); } public function getGoogleLoginRedirect(): IRedirect { $state = bin2hex(random_bytes(16)); $nonce = bin2hex(random_bytes(16)); $this->request->session()->set('oauth_state', $state); $this->request->session()->set('oauth_nonce', $nonce); $oAuth = new GoogleOAuth(new Request()); $url = $oAuth->getDialogUrl( $state, $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink(), $nonce ); return new Redirect($url, IRedirect::TEMPORARY); } public function getRequestPasswordResetForm() { if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); } return new HtmlContent('login/password_reset_request', ['email' => $this->request->query('email')]); } public function getRequestPasswordResetSuccess(): IContent { return new HtmlContent('login/password_reset_request_success'); } public function getResetPasswordForm() { if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); } $token = $this->request->query('token'); $resetter = $this->userPasswordResetterRepository->getByToken($token); if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { return new HtmlContent('login/reset_password', ['success' => false]); } $user = $this->userRepository->getById($resetter->getUserId()); return new HtmlContent('login/reset_password', ['success' => true, 'token' => $token, 'email' => $user->getEmail(), 'redirectUrl' => '/' . $this->redirectUrl]); } public function login(): IContent { if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new JsonContent(['success' => true]); } $user = $this->userRepository->getByEmail($this->request->post('email')); if ($user === null || !$user->checkPassword($this->request->post('password'))) { return new JsonContent([ 'error' => [ 'errorText' => 'No user found with the given email address or the given password is wrong. You can request password reset!' ] ]); } $this->request->setUser($user); $this->deleteRedirectUrl(); return new JsonContent(['success' => true]); } public function loginWithGoogle() { $defaultError = 'Authentication with Google failed. Please try again!'; if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); } if ($this->request->query('state') !== $this->request->session()->get('oauth_state')) { return new HtmlContent('login/google_login_error', ['error' => $defaultError]); } $oAuth = new GoogleOAuth(new Request()); $tokenData = $oAuth->getToken( $this->request->query('code'), $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('login-google-action')->generateLink() ); if (!isset($tokenData['id_token'])) { return new HtmlContent('login/google_login_error', ['error' => $defaultError]); } $jwtParser = new JwtParser($tokenData['id_token']); $idToken = $jwtParser->getPayload(); if ($idToken['nonce'] !== $this->request->session()->get('oauth_nonce')) { return new HtmlContent('login/google_login_error', ['error' => $defaultError]); } if (!$idToken['email_verified']) { return new HtmlContent('login/google_login_error', ['error' => $defaultError]); } $user = $this->userRepository->getByGoogleSub($idToken['sub']); if ($user === null) { return new HtmlContent('login/google_login_error', ['error' => 'No user found for this Google account.']); } $this->request->setUser($user); $this->deleteRedirectUrl(); return new Redirect($this->redirectUrl, IRedirect::TEMPORARY); } public function logout(): IRedirect { $this->request->setUser(null); return new Redirect(\Container::$routeCollection->getRoute('index')->generateLink(), IRedirect::TEMPORARY); } public function requestPasswordReset(): IContent { if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new JsonContent([ 'redirect' => [ 'target' => '/' . $this->redirectUrl ] ]); } if (!empty($_ENV['RECAPTCHA_SITEKEY'])) { if (!$this->request->post('g-recaptcha-response')) { return new JsonContent(['error' => ['errorText' => 'Please check "I\'m not a robot" in the reCAPTCHA box!']]); } $captchaValidator = new CaptchaValidator(); $captchaResponse = $captchaValidator->validate($this->request->post('g-recaptcha-response')); if (!$captchaResponse['success']) { return new JsonContent(['error' => ['errorText' => 'reCAPTCHA challenge failed. Please try again!']]); } } $user = $this->userRepository->getByEmail($this->request->post('email')); if ($user === null) { return new JsonContent([ 'error' => [ 'errorText' => 'No user found with the given email address.' ] ]); } $existingResetter = $this->userPasswordResetterRepository->getByUser($user); if ($existingResetter !== null && $existingResetter->getExpiresDate() > new DateTime()) { return new JsonContent([ 'error' => [ 'errorText' => 'Password reset was recently requested for this account. Please check your email, or try again later!' ] ]); } $token = bin2hex(random_bytes(16)); $expires = new DateTime('+1 hour'); $passwordResetter = new UserPasswordResetter(); $passwordResetter->setUser($user); $passwordResetter->setToken($token); $passwordResetter->setExpiresDate($expires); \Container::$dbConnection->startTransaction(); if ($existingResetter !== null) { $this->pdm->deleteFromDb($existingResetter); } $this->pdm->saveToDb($passwordResetter); \Container::$dbConnection->commit(); $this->sendPasswordResetEmail($user->getEmail(), $token, $expires); return new JsonContent(['success' => true]); } public function resetPassword(): IContent { if ($this->request->user() !== null) { $this->deleteRedirectUrl(); return new JsonContent([ 'redirect' => [ 'target' => '/' . $this->redirectUrl ] ]); } $token = $this->request->query('token'); $resetter = $this->userPasswordResetterRepository->getByToken($token); if ($resetter === null || $resetter->getExpiresDate() < new DateTime()) { return new JsonContent([ 'redirect' => [ 'target' => '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]) ] ]); } if (strlen($this->request->post('password')) < 6) { return new JsonContent([ 'error' => [ 'errorText' => 'The given password is too short. Please choose a password that is at least 6 characters long!' ] ]); } if ($this->request->post('password') !== $this->request->post('password_confirm')) { return new JsonContent(['error' => ['errorText' => 'The given passwords do not match.']]); } \Container::$dbConnection->startTransaction(); $this->pdm->deleteFromDb($resetter); $user = $this->userRepository->getById($resetter->getUserId()); $user->setPlainPassword($this->request->post('password')); $this->pdm->saveToDb($user); \Container::$dbConnection->commit(); $this->request->setUser($user); $this->deleteRedirectUrl(); return new JsonContent(['success' => true]); } private function sendPasswordResetEmail(string $email, string $token, DateTime $expires): void { $mail = new Mail(); $mail->addRecipient($email); $mail->setSubject($_ENV['APP_NAME'] . ' - Password reset'); $mail->setBodyFromTemplate('password-reset', [ 'EMAIL' => $email, 'RESET_LINK' => $this->request->getBase() . '/' . \Container::$routeCollection->getRoute('password-reset')->generateLink(['token' => $token]), 'EXPIRES' => $expires->format('Y-m-d H:i T') ]); $mail->send(); } private function deleteRedirectUrl(): void { $this->request->session()->delete('redirect_after_login'); } }