From 82157cad7627169b2910f8affae94d17afa3f59f Mon Sep 17 00:00:00 2001 From: Anonymous Contributor Date: Wed, 23 Aug 2023 18:16:36 +0200 Subject: [PATCH] Add e-mail-based password reset feature --- app/utils.php | 58 ++++++ config.example.php | 4 +- pages/forgot.php | 67 +++++++ pages/login.php | 12 +- pages/reset-password.php | 71 +++++++ templates/error.html | 54 ++++++ templates/forgot.html | 65 +++++++ templates/login.html | 4 +- templates/mail.html | 336 ++-------------------------------- templates/reset-password.html | 66 +++++++ 10 files changed, 407 insertions(+), 330 deletions(-) create mode 100644 pages/forgot.php create mode 100644 pages/reset-password.php create mode 100644 templates/error.html create mode 100644 templates/forgot.html create mode 100644 templates/reset-password.html diff --git a/app/utils.php b/app/utils.php index c3feb0c..ed221a4 100644 --- a/app/utils.php +++ b/app/utils.php @@ -1,4 +1,7 @@ ', "\n", $message); + $messageNew = preg_replace('/(.*)<\\/a>/', "$2: $1", $messageNew); + return $messageNew; +} + +function sendMail($message, $subject, $title, $preheader): bool { + include_once('../lib/phpmailer/Exception.php'); + include_once('../lib/phpmailer/PHPMailer.php'); + include_once('../lib/phpmailer/SMTP.php'); + include_once('app/HTML.php'); + global $RUNTIME; + + $mailer = new PHPMailer(true); + + try { + $mailer->isSMTP(); + $mailer->Host = $RUNTIME['SMTP']['SERVER']; + $mailer->Port = $RUNTIME['SMTP']['PORT']; + $mailer->Username = $RUNTIME['SMTP']['ADDRESS']; + $mailer->Password = $RUNTIME['SMTP']['PASS']; + $mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + + $mailer->setFrom($RUNTIME['SMTP']['ADDRESS'], $RUNTIME['SMTP']['NAME']); + $mailer->addAddress($email); + + $mailer->isHTML(true); + $mailer->Subject = $subject; + $mailHtml = new HTML(); + $mailHtml->importHTML("email.html"); + $mailHtml->setHTMLTitle($title); + $mailHtml->ReplaceLayoutInhalt('%%MESSAGE%%', $message); + $mailHtml->ReplaceLayoutInhalt('%%PREHEADER%%', $preheader); + $mailHtml->build(); + $mailer->Body = $mailHtml->ausgabe(); + $mailer->AltBody = htmlToPlain($message); + + $mailer->send(); + return true; + } catch(Exception $e) { + error_log('Could not send email: '.$mailer->ErrorInfo); + return false; + } +} + function getDataFromHTTP($URL, $contend = "", $requestTyp = "application/text") { try diff --git a/config.example.php b/config.example.php index 8633c11..e3f60bb 100644 --- a/config.example.php +++ b/config.example.php @@ -7,8 +7,8 @@ $RUNTIME['GRID']['HOMEURL'] = "http://...:8002"; $RUNTIME['SMTP']['SERVER'] = "localhost"; $RUNTIME['SMTP']['PORT'] = 25; -$RUNTIME['SMTP']['ADRESS'] = "noreplay@localhost"; -$RUNTIME['SMTP']['USER'] = "noreplay@localhost"; +$RUNTIME['SMTP']['ADDRESS'] = "noreply@localhost"; +$RUNTIME['SMTP']['NAME'] = "4Creative"; $RUNTIME['SMTP']['PASS'] = "..."; $RUNTIME['TOOLS']['IMAGESERVICE'] = "https://image-service.4creative.net/"; diff --git a/pages/forgot.php b/pages/forgot.php new file mode 100644 index 0000000..940ee1c --- /dev/null +++ b/pages/forgot.php @@ -0,0 +1,67 @@ +
wir haben soeben eine Anfrage zur Zurücksetzung des Passworts für deinen 4Creative-Account erhalten.

Klicke
hier, um ein neues Passwort festzulegen. Dieser Link läuft in 24 Stunden ab.

Falls du diese Anfrage nicht gesendet hast, ignoriere sie einfach. Bei weiteren Fragen kannst du uns unter info@4creative.net oder per Discord über @ikeytan erreichen.'; + + $HTML = new HTML(); + $HTML->setHTMLTitle("Passwort vergessen"); + $HTML->importHTML("forgot.html"); + + if($_SERVER['REQUEST_METHOD'] == 'POST') { + include_once 'app/FormValidator.php'; + $validator = new FormValidator(array( + 'username' => array('required' => true, 'regex' => '/^[^\\/<>\s]{1,64} [^\\/<>\s]{1,64}$/'), + 'email' => array('required' => true, 'regex' => '/^\S{1,64}@\S{1,250}.\S{2,64}$/') + )); + + if(!$validator->isValid($_POST)) { + $HTML->ReplaceLayoutInhalt('%%MESSAGE%%', 'Bitte gebe deinen Benutzernamen (Vor- und Nachname) und die dazugehörige E-Mail-Adresse ein'); + $HTML->ReplaceLayoutInhalt('%%MESSAGECOLOR%%', 'red'); + } + else { + $nameParts = explode(" ", $_POST['username']); + $email = strtolower(trim($_POST['email'])); + + $getAccount = $RUNTIME['pdo']->prepare('SELECT Email,FirstName,LastName,PrincipalID FROM UserAccounts WHERE FirstName = ? AND LastName = ? AND Email = ?'); + $getAccount->execute([trim($nameParts[0]), trim($nameParts[1]), $email]); + $validRequest = $getAccount->rowCount() == 1; + if($res = $getAccount->fetch()) { + $email = $res['Email']; + $uuid = $res['PrincipalID']; + $name = $res['FirstName'].' '.$res['LastName']; + } + + foreach($blockedDomains as $domain) { + if(str_ends_with($email, $domain)) { + $validRequest = false; + } + } + + $HTML->ReplaceLayoutInhalt('%%MESSAGE%%', 'Falls Name und E-Mail-Adresse bei uns registriert sind, erhältst du in Kürze eine E-Mail mit weiteren Informationen.'); + $HTML->ReplaceLayoutInhalt('%%MESSAGECOLOR%%', 'green'); + $HTML->build(); + echo $HTML->ausgabe(); + fastcgi_finish_request(); + + if($validRequest) { + $getReqTime = $RUNTIME['pdo']->prepare('SELECT RequestTime FROM PasswordResetTokens WHERE PrincipalID=?'); + $getReqTime->execute([$uuid]); + if(($res = $getReqTime->fetch()) && time() - $res['RequestTime'] < 900) { + return; + } + + require_once 'app/utils.php'; + $token = generateToken(32); + $setToken = $RUNTIME['pdo']->prepare('REPLACE INTO PasswordResetTokens(PrincipalID,Token,RequestTime) VALUES(?,?,?)'); + $setToken->execute([$uuid, $token, time()]); + + sendMail(str_replace('%%NAME%%', $name, str_replace('%%RESET_LINK%%', 'https://'.$RUNTIME['DOMAIN'].'/index.php?page=reset-password&token='.$token, MESSAGE)), "Zurücksetzung des Passworts für ".$name, 'Dein Passwort zurücksetzen', 'Folge diesen Anweisungen, um ein neues Passwort für deinen 4Creative-Account festzulegen'); + } + } + } + else { + $HTML->ReplaceLayoutInhalt('%%MESSAGE%%', ''); + $HTML->ReplaceLayoutInhalt('%%MESSAGECOLOR%%', 'red'); + $HTML->build(); + echo $HTML->ausgabe(); + } + +?> \ No newline at end of file diff --git a/pages/login.php b/pages/login.php index f0e6444..d330678 100644 --- a/pages/login.php +++ b/pages/login.php @@ -38,7 +38,7 @@ $_SESSION['SALT'] = $rowAuth['passwordSalt']; $_SESSION['UUID'] = $rowUser['PrincipalID']; $_SESSION['LEVEL'] = $rowUser['UserLevel']; - $_SESSION['DISPLAYNAME'] = strtoupper(trim($_POST['username'])); + $_SESSION['DISPLAYNAME'] = strtoupper($rowUser['FirstName'].' '.$rowUser['LastName']); $_SESSION['LOGIN'] = 'true'; header("Location: index.php?page=".urlencode($_REQUEST['page'])); @@ -47,15 +47,21 @@ } } - $HTML->ReplaceLayoutInhalt("%%LOGINMESSAGE%%", "Benutzername und/oder Passwort falsch."); - $HTML->ReplaceLayoutInhalt("%%LASTUSERNAME%%", htmlspecialchars($_POST['username'])); + $HTML->ReplaceLayoutInhalt("%%LOGINMESSAGE%%", "Benutzername und/oder Passwort falsch."); + $HTML->ReplaceLayoutInhalt("%%LASTUSERNAME%%", htmlspecialchars($_POST['username'])); } } + else if(isset($_SESSION) && isset($_SESSION['resetMessage'])) { + unset($_SESSION['resetMessage']); + $HTML->ReplaceLayoutInhalt('%%LOGINMESSAGE%%', 'Du kannst dich jetzt mit deinem neuen Passwort einloggen!'); + $HTML->ReplaceLayoutInhalt("%%MESSAGECOLOR%%", "darkgreen"); + } if(isset($_REQUEST['page']) && preg_match('/^[0-9a-zA-Z]{1-100}$/', $_REQUEST['page']) && file_exists("./pages/".$_REQUEST['page'].".php")) $HTML->ReplaceLayoutInhalt("%%PAGENAME%%", urlencode($_REQUEST['page'])); $HTML->ReplaceLayoutInhalt("%%LOGINMESSAGE%%", ""); + $HTML->ReplaceLayoutInhalt("%%MESSAGECOLOR%%", "red"); $HTML->ReplaceLayoutInhalt("%%LASTUSERNAME%%", ""); $HTML->ReplaceLayoutInhalt("%%PAGENAME%%", "dashboard"); diff --git a/pages/reset-password.php b/pages/reset-password.php new file mode 100644 index 0000000..71ff68c --- /dev/null +++ b/pages/reset-password.php @@ -0,0 +1,71 @@ +
das Passwort für deinen 4Creative-Account wurde soeben über die Funktion "Passwort vergessen" geändert.

Solltest du diese Änderung nicht selbst durchgeführt haben, wende dich bitte umgehend per E-Mail (info@4creative.net) oder Discord (@ikeytan) an uns.'; + + function displayTokenError() { + $HTML = new HTML(); + $HTML->importHTML("error.html"); + $HTML->ReplaceLayoutInhalt('%%MESSAGE%%', 'Dieser Link zur Passwortzurücksetzung ist nicht gültig. Bitte klicke oder kopiere den Link aus der E-Mail, die du erhalten hast.'); + $HTML->build(); + exit(); + } + + function displayPage($err) { + $HTML = new HTML(); + $HTML->setHTMLTitle(""); + $HTML->importHTML("reset-password.html"); + $HTML->ReplaceLayoutInhalt('%%MESSAGE%%', $err); + $HTML->build(); + echo $HTML->ausgabe(); + exit(); + } + + if($_SERVER['REQUEST_METHOD'] == 'POST') { + include_once 'app/FormValidator.php'; + $validator = new FormValidator(array( + 'password' => array('required' => true, 'regex' => '/^.{1,1000}$/'), + 'passwordRepeat' => array('required' => true, 'regex' => '/^.{1,1000}$/'), + 'resetToken' => array('required' => true, 'regex' => '/^[a-zA-Z0-9]{32}$/') + )); + + if($validator->isValid($_POST)) { + if($_POST['password'] !== $_POST['passwordRepeat']) { + displayPage('Du musst in beiden Feldern das gleiche Passwort eingeben'); + } + + if(strlen($_POST['password']) < $RUNTIME['PASSWORD_MIN_LENGTH']) { + displayPage('Dein Passwort muss mindestens '.$RUNTIME['PASSWORD_MIN_LENGTH'].' Zeichen lang sein.'); + } + + $getUuid = $RUNTIME['PDO']->prepare('SELECT PrincipalID,FirstName,LastName FROM PasswordResetTokens JOIN UserAccounts ON PasswordResetTokens.PrincipalID = PasswordResetTokens.PrincipalID WHERE Token = ?'); + if($getUuid->rowCount() == 0) { + displayTokenError(); + } + + $res = $getUuid->fetch(); + $uuid = $res['PrincipalID']; + $name = $res['FirstName'].' '.$res['LastName']; + $getToken = $RUNTIME['PDO']->prepare('DELETE FROM PasswordResetTokens WHERE Token = ?'); + $getToken->execute([$_POST['resetToken']]); + + $salt = bin2hex(random_bytes(16)); + $hash = md5(md5(trim($_POST['password'])).':'.$salt); + $statement = $RUNTIME['PDO']->prepare('UPDATE auth SET passwordHash = :PasswordHash, passwordSalt = :PasswordSalt WHERE UUID = :PrincipalID'); + $statement->execute(['PasswordHash' => $hash, 'PasswordSalt' => $salt, 'PrincipalID' => $uuid]); + + session_unset(); + $_SESSION['resetPassword'] = true; + + require_once 'app/utils.php'; + sendMail(str_replace('%%NAME%%', $name, MESSAGE), 'Passwort für '.$name.' zurückgesetzt', 'Passwort geändert', 'Das Passwort für deinen 4Creative-Account wurde soeben zurückgesetzt'); + + header('Location: index.php?page=login'); + exit(); + } + } + + displayPage(''); + + if(!isset($_GET['token']) || !preg_match('/^[a-z0-9A-Z]{32}$/', $_GET['token'])) { + displayTokenError(); + } +?> \ No newline at end of file diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..c037e70 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,54 @@ + + + + + + Fehler - MCP + + + + + + + + + + + + + + + +
+
+
+
+ + Fehler + + +
+ %%MESSAGE%% +
+ + +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/forgot.html b/templates/forgot.html new file mode 100644 index 0000000..71489a8 --- /dev/null +++ b/templates/forgot.html @@ -0,0 +1,65 @@ + + + + + + Passwort vergessen + + + + + + + + + + + + + + + +
+
+
+
+ + Passwort vergessen + + +
+ %%MESSAGE%% +
+ +
+ + +
+ +
+ + +
+ +
+ %%CSRF%% + +
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index b2db1a6..633059a 100644 --- a/templates/login.html +++ b/templates/login.html @@ -27,7 +27,7 @@ Login -
+
%%LOGINMESSAGE%%
@@ -47,7 +47,7 @@
- Passwort Vergessen? + Passwort vergessen?
diff --git a/templates/mail.html b/templates/mail.html index a99fc53..232d9d6 100644 --- a/templates/mail.html +++ b/templates/mail.html @@ -3,7 +3,7 @@ - Simple Transactional Email + + - - This is preheader text. Some clients will show this text as a preview. - - - - - - - + + +
+
+ Logo +

%%EchoTitle%%

+
+
+ %%MESSAGE%% +
+
\ No newline at end of file diff --git a/templates/reset-password.html b/templates/reset-password.html new file mode 100644 index 0000000..97277bf --- /dev/null +++ b/templates/reset-password.html @@ -0,0 +1,66 @@ + + + + + + Neues Password festlegen + + + + + + + + + + + + + + + +
+
+
+
+ + Neues Passwort festlegen + + +
+ %%MESSAGE%% +
+ +
+ + +
+ +
+ + +
+ +
+ %%CSRF%% + + +
+
+
+
+
+ + + + + + + + + + + + \ No newline at end of file