mirror of
https://github.com/typemill/typemill.git
synced 2025-08-05 21:57:31 +02:00
Version 1.4.9: Password recovery and security middleware
This commit is contained in:
@@ -8,6 +8,7 @@ use Slim\Http\Response;
|
||||
use Typemill\Models\Validation;
|
||||
use Typemill\Models\User;
|
||||
use Typemill\Models\WriteYaml;
|
||||
use Typemill\Extensions\ParsedownExtension;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
@@ -34,36 +35,10 @@ class AuthController extends Controller
|
||||
*/
|
||||
|
||||
public function show(Request $request, Response $response, $args)
|
||||
{
|
||||
$data = array();
|
||||
{
|
||||
$settings = $this->c->get('settings');
|
||||
|
||||
/* check previous login attemps */
|
||||
$yaml = new WriteYaml();
|
||||
$logins = $yaml->getYaml('settings/users', '.logins');
|
||||
$userIP = $this->getUserIP();
|
||||
$userLogins = isset($logins[$userIP]) ? count($logins[$userIP]) : false;
|
||||
|
||||
if($userLogins)
|
||||
{
|
||||
/* get the latest */
|
||||
$lastLogin = intval($logins[$userIP][$userLogins-1]);
|
||||
|
||||
/* if last login is longer than 60 seconds ago, clear it. */
|
||||
if(time() - $lastLogin > 60)
|
||||
{
|
||||
unset($logins[$userIP]);
|
||||
$yaml->updateYaml('settings/users', '.logins', $logins);
|
||||
}
|
||||
|
||||
/* Did the user made three login attemps that failed? */
|
||||
elseif($userLogins >= 3)
|
||||
{
|
||||
$timeleft = 60 - (time() - $lastLogin);
|
||||
$data['messages'] = array('time' => $timeleft, 'error' => array( 'Too many bad logins. Please wait.'));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($response, '/auth/login.twig', $data);
|
||||
return $this->render($response, '/auth/login.twig', ['settings' => $settings]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,41 +54,13 @@ class AuthController extends Controller
|
||||
if( ( null !== $request->getattribute('csrf_result') ) OR ( $request->getattribute('csrf_result') === false ) )
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
/* log user attemps to authenticate */
|
||||
$yaml = new WriteYaml();
|
||||
$logins = $yaml->getYaml('settings/users', '.logins');
|
||||
$userIP = $this->getUserIP();
|
||||
$userLogins = isset($logins[$userIP]) ? count($logins[$userIP]) : false;
|
||||
|
||||
/* if there have been user logins before. You have to do this again, because user does not always refresh the login page and old login attemps are stored. */
|
||||
if($userLogins)
|
||||
{
|
||||
/* get the latest */
|
||||
$lastLogin = intval($logins[$userIP][$userLogins-1]);
|
||||
|
||||
/* if last login is longer than 60 seconds ago, clear it and add this attempt */
|
||||
if(time() - $lastLogin > 60)
|
||||
{
|
||||
unset($logins[$userIP]);
|
||||
$yaml->updateYaml('settings/users', '.logins', $logins);
|
||||
}
|
||||
|
||||
/* Did the user made three login attemps that failed? */
|
||||
elseif($userLogins >= 2)
|
||||
{
|
||||
$logins[$userIP][] = time();
|
||||
$yaml->updateYaml('settings/users', '.logins', $logins);
|
||||
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
}
|
||||
|
||||
/* authentication */
|
||||
/* authentication */
|
||||
$params = $request->getParams();
|
||||
$validation = new Validation();
|
||||
$settings = $this->c->get('settings');
|
||||
|
||||
if($validation->signin($params))
|
||||
{
|
||||
@@ -131,13 +78,6 @@ class AuthController extends Controller
|
||||
|
||||
$user->login($userdata['username']);
|
||||
|
||||
/* clear the user login attemps */
|
||||
if($userLogins)
|
||||
{
|
||||
unset($logins[$userIP]);
|
||||
$yaml->updateYaml('settings/users', '.logins', $logins);
|
||||
}
|
||||
|
||||
# if user is allowed to view content-area
|
||||
if($this->c->acl->hasRole($userdata['userrole']) && $this->c->acl->isAllowed($userdata['userrole'], 'content', 'view'))
|
||||
{
|
||||
@@ -149,11 +89,12 @@ class AuthController extends Controller
|
||||
return $response->withRedirect($this->c->router->pathFor('user.account'));
|
||||
}
|
||||
}
|
||||
|
||||
/* if authentication failed, add attempt to log file */
|
||||
$logins[$userIP][] = time();
|
||||
$yaml->updateYaml('settings/users', '.logins', $logins);
|
||||
|
||||
if(isset($this->settings['securitylog']) && $this->settings['securitylog'])
|
||||
{
|
||||
\Typemill\Models\Helpers::addLogEntry('wrong login');
|
||||
}
|
||||
|
||||
$this->c->flash->addMessage('error', 'Ups, wrong password or username, please try again.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
@@ -176,25 +117,225 @@ class AuthController extends Controller
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
private function getUserIP()
|
||||
public function showRecoverPassword(Request $request, Response $response, $args)
|
||||
{
|
||||
$client = @$_SERVER['HTTP_CLIENT_IP'];
|
||||
$forward = @$_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
$remote = $_SERVER['REMOTE_ADDR'];
|
||||
$data = array();
|
||||
|
||||
return $this->render($response, '/auth/recoverpw.twig', $data);
|
||||
}
|
||||
|
||||
if(filter_var($client, FILTER_VALIDATE_IP))
|
||||
public function recoverPassword(Request $request, Response $response, $args)
|
||||
{
|
||||
$params = $request->getParams();
|
||||
$validation = new Validation();
|
||||
$settings = $this->c->get('settings');
|
||||
$uri = $request->getUri()->withUserInfo('');
|
||||
$base_url = $uri->getBaseUrl();
|
||||
|
||||
if(!isset($params['email']) OR filter_var($params['email'], \FILTER_VALIDATE_EMAIL) === false )
|
||||
{
|
||||
$ip = $client;
|
||||
$this->c->flash->addMessage('error', 'Please enter a valid email.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.recoverpwshow'));
|
||||
}
|
||||
elseif(filter_var($forward, FILTER_VALIDATE_IP))
|
||||
|
||||
$user = new User();
|
||||
$requiredUser = $user->findUsersByEmail($params['email']);
|
||||
|
||||
if(!$requiredUser)
|
||||
{
|
||||
$ip = $forward;
|
||||
$this->c->flash->addMessage('error', 'The email address is unknown.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.recoverpwshow'));
|
||||
}
|
||||
|
||||
$requiredUser = $user->getSecureUser($requiredUser[0]);
|
||||
|
||||
$requiredUser['recoverdate'] = date("Y-m-d H:i:s");
|
||||
$requiredUser['recovertoken'] = bin2hex(random_bytes(32));
|
||||
|
||||
$url = $base_url . '/tm/recoverpwnew?username=' . $requiredUser['username'] . '&recovertoken=' . $requiredUser['recovertoken'];
|
||||
$link = '<a href="'. $url . '">' . $url . '</a>';
|
||||
|
||||
# define the headers
|
||||
$headers = 'Content-Type: text/html; charset=utf-8' . "\r\n";
|
||||
$headers .= 'Content-Transfer-Encoding: base64' . "\r\n";
|
||||
if(isset($settings['recoverfrom']) && $settings['recoverfrom'] != '')
|
||||
{
|
||||
$headers .= 'From: ' . $settings['recoverfrom'];
|
||||
}
|
||||
|
||||
$subjectline = (isset($settings['recoversubject']) && ($settings['recoversubject'] != '') ) ? $settings['recoversubject'] : 'Recover your password';
|
||||
$subject = '=?UTF-8?B?' . base64_encode($subjectline) . '?=';
|
||||
|
||||
$messagetext = "Dear user,<br/><br/>please use the following link to set a new password:";
|
||||
if(isset($settings['recovermessage']) && ($settings['recovermessage'] != ''))
|
||||
{
|
||||
$parsedown = new ParsedownExtension($base_url);
|
||||
$parsedown->setSafeMode(true);
|
||||
|
||||
$contentArray = $parsedown->text($settings['recovermessage']);
|
||||
$messagetext = $parsedown->markup($contentArray);
|
||||
}
|
||||
|
||||
$message = base64_encode($messagetext . "<br/><br/>" . $link);
|
||||
|
||||
# store user
|
||||
$user->updateUser($requiredUser);
|
||||
|
||||
$send = mail($requiredUser['email'], $subject, $message, $headers);
|
||||
|
||||
if(!$send)
|
||||
{
|
||||
$data = [
|
||||
'title' => 'We could not send the email',
|
||||
'message' => 'Dear ' . $requiredUser['username'] . ', we could not send the email with the password instructions to your address. You can try it again but chances are low that it will work next time. Please contact the website owner and ask for help.',
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$ip = $remote;
|
||||
# store user
|
||||
$user->updateUser($requiredUser);
|
||||
|
||||
$data = [
|
||||
'title' => 'Please check your inbox',
|
||||
'message' => 'Dear ' . $requiredUser['username'] . ', please check the inbox of your email account. We have sent you some short instructions how to recover your password. Do not forget to check your spam-folder if your inbox is empty.',
|
||||
];
|
||||
}
|
||||
|
||||
return $this->render($response, '/auth/recoverpwsend.twig', $data);
|
||||
}
|
||||
|
||||
public function showRecoverPasswordNew(Request $request, Response $response, $args)
|
||||
{
|
||||
$params = $request->getParams();
|
||||
|
||||
if(!isset($params['username']) OR !isset($params['recovertoken']))
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Ups, you tried to open the password recovery page but the link was invalid.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
return $ip;
|
||||
$user = new user();
|
||||
|
||||
$requiredUser = $user->getSecureUser($params['username']);
|
||||
|
||||
if(!$requiredUser)
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Ups, you tried to open the password recovery page but the link was invalid.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
if(!isset($requiredUser['recovertoken']) OR $requiredUser['recovertoken'] != $params['recovertoken'] )
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Ups, you tried to open the password recovery page but the link was invalid.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$recoverdate = isset($requiredUser['recoverdate']) ? $requiredUser['recoverdate'] : false;
|
||||
|
||||
if(!$recoverdate )
|
||||
{
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken']);
|
||||
|
||||
$this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$now = new \DateTime('NOW');
|
||||
$recoverdate = new \DateTime($recoverdate);
|
||||
|
||||
if(!$recoverdate)
|
||||
{
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']);
|
||||
|
||||
$this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$validDate = $recoverdate->add(new \DateInterval('P1D'));
|
||||
|
||||
if($validDate <= $now)
|
||||
{
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']);
|
||||
|
||||
$this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
return $this->render($response, '/auth/recoverpwnew.twig', ['recovertoken' => $params['recovertoken'],'username' => $requiredUser['username']]);
|
||||
}
|
||||
|
||||
public function createRecoverPasswordNew(Request $request, Response $response, $args)
|
||||
{
|
||||
$params = $request->getParams();
|
||||
|
||||
if(!isset($params['username']) OR !isset($params['recovertoken']))
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Ups, you tried to set a new password but username or token was invalid.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$validation = new Validation();
|
||||
|
||||
if(!$validation->recoverPassword($params))
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Please check your input.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.recoverpwshownew',[], ['username' => $params['username'], 'recovertoken' => $params['recovertoken']]));
|
||||
}
|
||||
|
||||
$user = new user();
|
||||
|
||||
$requiredUser = $user->getSecureUser($params['username']);
|
||||
|
||||
if(!$requiredUser)
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Ups, you tried to create a new password but the username was invalid.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
if(!isset($requiredUser['recovertoken']) OR $requiredUser['recovertoken'] != $params['recovertoken'] )
|
||||
{
|
||||
$this->c->flash->addMessage('error', 'Ups, you tried to create a new password but the token was invalid.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$recoverdate = isset($requiredUser['recoverdate']) ? $requiredUser['recoverdate'] : false;
|
||||
|
||||
if(!$recoverdate )
|
||||
{
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken']);
|
||||
|
||||
$this->c->flash->addMessage('error', 'The date for the password reset was invalid. Please create a new one.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$now = new \DateTime('NOW');
|
||||
$recoverdate = new \DateTime($recoverdate);
|
||||
|
||||
if(!$recoverdate)
|
||||
{
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']);
|
||||
|
||||
$this->c->flash->addMessage('error', 'The date for the password reset was too old. Please create a new one.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$validDate = $recoverdate->add(new \DateInterval('P1D'));
|
||||
|
||||
if($validDate <= $now)
|
||||
{
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']);
|
||||
|
||||
$this->c->flash->addMessage('error', 'The link to recover the password was too old. Please create a new one.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
|
||||
$requiredUser['password'] = $params['password'];
|
||||
$user->updateUser($requiredUser);
|
||||
$user->unsetFromUser($requiredUser['username'], ['recovertoken', 'recoverdate']);
|
||||
|
||||
unset($_SESSION['old']);
|
||||
|
||||
$this->c->flash->addMessage('info', 'A new password has been created. Please login.');
|
||||
return $response->withRedirect($this->c->router->pathFor('auth.show'));
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ class FormController extends Controller
|
||||
*************************************/
|
||||
|
||||
public function savePublicForm($request, $response, $args)
|
||||
{
|
||||
{
|
||||
if($request->isPost())
|
||||
{
|
||||
$params = $request->getParams();
|
||||
@@ -26,46 +26,6 @@ class FormController extends Controller
|
||||
$this->c->flash->addMessage('error', 'The form has a timeout. Please try again.');
|
||||
return $response->withRedirect($referer[0]);
|
||||
}
|
||||
|
||||
# simple bot check with honeypot
|
||||
if(isset($params[$pluginName]['personal-mail']))
|
||||
{
|
||||
if($params[$pluginName]['personal-mail'] != '')
|
||||
{
|
||||
$this->c->flash->addMessage('publicform', 'bot');
|
||||
return $response->withRedirect($referer[0]);
|
||||
}
|
||||
unset($params[$pluginName]['personal-mail']);
|
||||
}
|
||||
|
||||
#recaptcha check
|
||||
if(isset($params['g-recaptcha-response']))
|
||||
{
|
||||
$recaptchaApi = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
$settings = $this->c->get('settings');
|
||||
$secret = isset($settings['plugins'][$pluginName]['recaptcha_secretkey']) ? $settings['plugins'][$pluginName]['recaptcha_secretkey'] : false;
|
||||
$recaptchaRequest = ['secret' => $secret, 'response' => $params['g-recaptcha-response']];
|
||||
|
||||
# use key 'http' even if you send the request to https://...
|
||||
$options = array(
|
||||
'http' => array(
|
||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||
'method' => 'POST',
|
||||
'content' => http_build_query($recaptchaRequest),
|
||||
'timeout' => 5
|
||||
)
|
||||
);
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$result = file_get_contents($recaptchaApi, false, $context);
|
||||
$result = json_decode($result);
|
||||
|
||||
if ($result === FALSE || $result->success === FALSE)
|
||||
{
|
||||
$this->c->flash->addMessage('publicform', 'bot');
|
||||
return $response->withRedirect($referer[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($params[$pluginName]))
|
||||
{
|
||||
|
@@ -113,6 +113,11 @@ class SettingsController extends Controller
|
||||
'headersoff' => isset($newSettings['headersoff']) ? true : null,
|
||||
'urlschemes' => $newSettings['urlschemes'],
|
||||
'svg' => isset($newSettings['svg']) ? true : null,
|
||||
'recoverpw' => isset($newSettings['recoverpw']) ? true : null,
|
||||
'recoverfrom' => $newSettings['recoverfrom'],
|
||||
'recoversubject' => $newSettings['recoversubject'],
|
||||
'recovermessage' => $newSettings['recovermessage'],
|
||||
'securitylog' => isset($newSettings['securitylog']) ? true : null,
|
||||
);
|
||||
|
||||
# https://www.slimframework.com/docs/v3/cookbook/uploading-files.html;
|
||||
@@ -348,15 +353,7 @@ class SettingsController extends Controller
|
||||
|
||||
/* if the plugin defines forms and fields, so that the user can edit the plugin settings in the frontend */
|
||||
if(isset($pluginOriginalSettings['forms']['fields']))
|
||||
{
|
||||
# if the plugin defines frontend fields
|
||||
if(isset($pluginOriginalSettings['public']))
|
||||
{
|
||||
$pluginOriginalSettings['forms']['fields']['recaptcha'] = ['type' => 'checkbox', 'label' => 'Google Recaptcha', 'checkboxlabel' => 'Activate Recaptcha' ];
|
||||
$pluginOriginalSettings['forms']['fields']['recaptcha_webkey'] = ['type' => 'text', 'label' => 'Recaptcha Website Key', 'help' => 'Add the recaptcha website key here. You can get the key from the recaptcha website.', 'description' => 'The website key is mandatory if you activate the recaptcha field'];
|
||||
$pluginOriginalSettings['forms']['fields']['recaptcha_secretkey'] = ['type' => 'text', 'label' => 'Recaptcha Secret Key', 'help' => 'Add the recaptcha secret key here. You can get the key from the recaptcha website.', 'description' => 'The secret key is mandatory if you activate the recaptcha field'];
|
||||
}
|
||||
|
||||
{
|
||||
/* get all the fields and prefill them with the dafault-data, the user-data or old input data */
|
||||
$fields = $fieldsModel->getFields($userSettings, 'plugins', $pluginName, $pluginOriginalSettings);
|
||||
|
||||
@@ -1132,14 +1129,6 @@ class SettingsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
# if the plugin defines frontend fields
|
||||
if(isset($originalSettings['public']))
|
||||
{
|
||||
$originalFields['recaptcha'] = ['type' => 'checkbox', 'label' => 'Google Recaptcha', 'checkboxlabel' => 'Activate Recaptcha' ];
|
||||
$originalFields['recaptcha_webkey'] = ['type' => 'text', 'label' => 'Recaptcha Website Key', 'help' => 'Add the recaptcha website key here. You can get the key from the recaptcha website.', 'description' => 'The website key is mandatory if you activate the recaptcha field'];
|
||||
$originalFields['recaptcha_secretkey'] = ['type' => 'text', 'label' => 'Recaptcha Secret Key', 'help' => 'Add the recaptcha secret key here. You can get the key from the recaptcha website.', 'description' => 'The secret key is mandatory if you activate the recaptcha field'];
|
||||
}
|
||||
|
||||
# if plugin is not active, then skip required
|
||||
$skiprequired = false;
|
||||
if($objectType == 'plugins' && !isset($userInput['active']))
|
||||
|
Reference in New Issue
Block a user