From c4d88251518740fcc40af16d7d732eb8fa32c5e4 Mon Sep 17 00:00:00 2001 From: dignajar Date: Tue, 20 Oct 2015 00:14:28 -0300 Subject: [PATCH] Login access code via email --- admin/controllers/edit-user.php | 8 ++-- admin/controllers/login-email.php | 77 +++++++++++++++++++++++++++++-- admin/views/add-user.php | 2 +- admin/views/configure-plugin.php | 2 +- admin/views/edit-page.php | 2 +- admin/views/edit-post.php | 2 +- admin/views/edit-user.php | 2 +- admin/views/login-email.php | 8 ++-- admin/views/login.php | 6 +-- admin/views/new-page.php | 2 +- admin/views/new-post.php | 2 +- admin/views/settings-advanced.php | 14 +++++- admin/views/settings-general.php | 2 +- admin/views/settings-regional.php | 2 +- kernel/boot/init.php | 4 ++ kernel/boot/rules/99.security.php | 6 +-- kernel/dbsite.class.php | 8 +++- kernel/dbusers.class.php | 45 +++++++++++++++--- kernel/helpers/date.class.php | 7 +++ kernel/helpers/email.class.php | 8 ++-- kernel/helpers/sanitize.class.php | 1 + kernel/helpers/text.class.php | 5 ++ kernel/helpers/valid.class.php | 1 + kernel/login.class.php | 48 ++++++++++++++++++- kernel/security.class.php | 8 ++-- languages/en_US.json | 12 ++++- 26 files changed, 237 insertions(+), 47 deletions(-) diff --git a/admin/controllers/edit-user.php b/admin/controllers/edit-user.php index 59cc3bad..5b535be8 100644 --- a/admin/controllers/edit-user.php +++ b/admin/controllers/edit-user.php @@ -17,14 +17,14 @@ function editUser($args) } } -function setPassword($new, $confirm) +function setPassword($username, $new_password, $confirm_password) { global $dbUsers; global $Language; - if( ($new===$confirm) && !Text::isEmpty($new) ) + if( ($new_password===$confirm_password) && !Text::isEmpty($new_password) ) { - if( $dbUsers->setPassword($new) ) { + if( $dbUsers->setPassword($username, $new_password) ) { Alert::set($Language->g('The changes have been saved')); } else { @@ -93,7 +93,7 @@ if( $_SERVER['REQUEST_METHOD'] == 'POST' ) deleteUser($_POST, false); } elseif( !empty($_POST['new-password']) && !empty($_POST['confirm-password']) ) { - setPassword($_POST['new-password'], $_POST['confirm-password']); + setPassword($_POST['username'], $_POST['new-password'], $_POST['confirm-password']); } else { editUser($_POST); diff --git a/admin/controllers/login-email.php b/admin/controllers/login-email.php index a7aca352..32b0f79e 100644 --- a/admin/controllers/login-email.php +++ b/admin/controllers/login-email.php @@ -11,8 +11,67 @@ function checkPost($args) { global $Security; - global $Login; global $Language; + global $dbUsers; + global $Site; + + if($Security->isBlocked()) { + Alert::set($Language->g('IP address has been blocked').'
'.$Language->g('Try again in a few minutes')); + return false; + } + + // Remove illegal characters from email + $email = Sanitize::email($args['email']); + + if(Valid::email($email)) + { + $user = $dbUsers->getByEmail($email); + if($user!=false) + { + // Generate the token and the token expiration date. + $token = $dbUsers->generateTokenEmail($user['username']); + + // ---- EMAIL ---- + $link = $Site->url().'admin/login-email?tokenEmail='.$token.'&username='.$user['username']; + $subject = $Language->g('BLUDIT Login access code'); + $message = Text::replaceAssoc( + array( + '{{WEBSITE_NAME}}'=>$Site->title(), + '{{LINK}}'=>''.$link.'' + ), + $Language->g('email-notification-login-access-code') + ); + + $sent = Email::send(array( + 'from'=>$Site->emailFrom(), + 'to'=>$email, + 'subject'=>$subject, + 'message'=>$message + )); + + if($sent) { + Alert::set($Language->g('check-your-inbox-for-your-login-access-code')); + return true; + } + else { + Alert::set($Language->g('There was a problem sending the email')); + return false; + } + } + } + + // Bruteforce protection, add IP to blacklist. + $Security->addLoginFail(); + Alert::set($Language->g('check-your-inbox-for-your-login-access-code')); + + return false; +} + +function checkGet($args) +{ + global $Security; + global $Language; + global $Login; if($Security->isBlocked()) { Alert::set($Language->g('IP address has been blocked').'
'.$Language->g('Try again in a few minutes')); @@ -20,9 +79,9 @@ function checkPost($args) } // Verify User sanitize the input - if( $Login->verifyUser($_POST['username'], $_POST['password']) ) + if( $Login->verifyUserByToken($args['username'], $args['tokenEmail']) ) { - // Renew the token. This token will be the same inside the session for multiple forms. + // Renew the tokenCRFS. This token will be the same inside the session for multiple forms. $Security->generateToken(); Redirect::page('admin', 'dashboard'); @@ -31,8 +90,6 @@ function checkPost($args) // Bruteforce protection, add IP to blacklist. $Security->addLoginFail(); - Alert::set($Language->g('Username or password incorrect')); - return false; } @@ -40,6 +97,16 @@ function checkPost($args) // Main before POST // ============================================================================ +// ============================================================================ +// GET Method +// ============================================================================ + +if( !empty($_GET['tokenEmail']) && !empty($_GET['username']) ) +{ + checkGet($_GET); +} + + // ============================================================================ // POST Method // ============================================================================ diff --git a/admin/views/add-user.php b/admin/views/add-user.php index 6a2b055d..ff3b871d 100644 --- a/admin/views/add-user.php +++ b/admin/views/add-user.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/configure-plugin.php b/admin/views/configure-plugin.php index 4511bf10..28a4f4f0 100644 --- a/admin/views/configure-plugin.php +++ b/admin/views/configure-plugin.php @@ -6,7 +6,7 @@ HTML::formOpen(array('id'=>'jsformplugin')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/edit-page.php b/admin/views/edit-page.php index 5f5bd18c..1e05de23 100644 --- a/admin/views/edit-page.php +++ b/admin/views/edit-page.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/edit-post.php b/admin/views/edit-post.php index 03d14e37..9a26ec14 100644 --- a/admin/views/edit-post.php +++ b/admin/views/edit-post.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/edit-user.php b/admin/views/edit-user.php index 5e2ecad5..b9afed03 100644 --- a/admin/views/edit-user.php +++ b/admin/views/edit-user.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/login-email.php b/admin/views/login-email.php index fe595e95..0af999e9 100644 --- a/admin/views/login-email.php +++ b/admin/views/login-email.php @@ -1,19 +1,19 @@
-
+ - +
- +
- Back to login form \ No newline at end of file + p('Back to login form') ?> \ No newline at end of file diff --git a/admin/views/login.php b/admin/views/login.php index 60174427..5c4abb87 100644 --- a/admin/views/login.php +++ b/admin/views/login.php @@ -2,14 +2,14 @@
- +
- +
@@ -20,4 +20,4 @@
- \ No newline at end of file + \ No newline at end of file diff --git a/admin/views/new-page.php b/admin/views/new-page.php index 51fb76fe..9f1f00b6 100644 --- a/admin/views/new-page.php +++ b/admin/views/new-page.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/new-post.php b/admin/views/new-post.php index 972077cb..e1a24a91 100644 --- a/admin/views/new-post.php +++ b/admin/views/new-post.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/settings-advanced.php b/admin/views/settings-advanced.php index aa67203a..0e31061f 100644 --- a/admin/views/settings-advanced.php +++ b/admin/views/settings-advanced.php @@ -5,7 +5,7 @@ HTML::title(array('title'=>$L->g('Settings'), 'icon'=>'cogs')); HTML::formOpen(array('class'=>'uk-form-horizontal')); HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); @@ -46,11 +46,21 @@ HTML::formOpen(array('class'=>'uk-form-horizontal')); 'tip'=>$L->g('enable-the-command-line-mode-if-you-add-edit') )); + HTML::legend(array('value'=>$L->g('Email account settings'))); + + HTML::formInputText(array( + 'name'=>'emailFrom', + 'label'=>$L->g('Sender email'), + 'value'=>$Site->emailFrom(), + 'class'=>'uk-width-1-2 uk-form-medium', + 'tip'=>$L->g('Emails will be sent from this address') + )); + HTML::legend(array('value'=>$L->g('URL Filters'))); HTML::formInputText(array( 'name'=>'uriPost', - 'label'=>$L->g('Posts'), + 'label'=>$L->g('Email'), 'value'=>$Site->uriFilters('post'), 'class'=>'uk-width-1-2 uk-form-medium', 'tip'=>'' diff --git a/admin/views/settings-general.php b/admin/views/settings-general.php index 5eac1a34..5172228e 100644 --- a/admin/views/settings-general.php +++ b/admin/views/settings-general.php @@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal')); // Security token HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/admin/views/settings-regional.php b/admin/views/settings-regional.php index 6ee1fd9f..cb0cc354 100644 --- a/admin/views/settings-regional.php +++ b/admin/views/settings-regional.php @@ -5,7 +5,7 @@ HTML::title(array('title'=>$L->g('Settings'), 'icon'=>'cogs')); HTML::formOpen(array('class'=>'uk-form-horizontal')); HTML::formInputHidden(array( - 'name'=>'token', + 'name'=>'tokenCSRF', 'value'=>$Security->getToken() )); diff --git a/kernel/boot/init.php b/kernel/boot/init.php index 73d8fd44..5a8c36b9 100644 --- a/kernel/boot/init.php +++ b/kernel/boot/init.php @@ -69,6 +69,9 @@ define('CLI_STATUS', 'published'); // Database format date define('DB_DATE_FORMAT', 'Y-m-d H:i'); +// Token time to live for login via email. The offset is defined by http://php.net/manual/en/datetime.modify.php +define('TOKEN_TTL', '+1 day'); + // Charset, default UTF-8. define('CHARSET', 'UTF-8'); @@ -112,6 +115,7 @@ include(PATH_HELPERS.'session.class.php'); include(PATH_HELPERS.'redirect.class.php'); include(PATH_HELPERS.'sanitize.class.php'); include(PATH_HELPERS.'valid.class.php'); +include(PATH_HELPERS.'email.class.php'); include(PATH_HELPERS.'filesystem.class.php'); include(PATH_HELPERS.'alert.class.php'); include(PATH_HELPERS.'paginator.class.php'); diff --git a/kernel/boot/rules/99.security.php b/kernel/boot/rules/99.security.php index aada4dbd..54e564d7 100644 --- a/kernel/boot/rules/99.security.php +++ b/kernel/boot/rules/99.security.php @@ -18,11 +18,11 @@ if( $_SERVER['REQUEST_METHOD'] == 'POST' ) { - $token = isset($_POST['token']) ? Sanitize::html($_POST['token']) : false; + $token = isset($_POST['tokenCSRF']) ? Sanitize::html($_POST['tokenCSRF']) : false; if( !$Security->validateToken($token) ) { - Log::set(__METHOD__.LOG_SEP.'Error occurred when trying validate the token. Token ID: '.$token); + Log::set(__METHOD__.LOG_SEP.'Error occurred when trying validate the tokenCSRF. Token CSRF ID: '.$token); // Destroy the session. Session::destroy(); @@ -32,7 +32,7 @@ if( $_SERVER['REQUEST_METHOD'] == 'POST' ) } else { - unset($_POST['token']); + unset($_POST['tokenCSRF']); } } diff --git a/kernel/dbsite.class.php b/kernel/dbsite.class.php index a10e470b..5d12c517 100644 --- a/kernel/dbsite.class.php +++ b/kernel/dbsite.class.php @@ -18,7 +18,8 @@ class dbSite extends dbJSON 'uriPost'=> array('inFile'=>false, 'value'=>'/post/'), 'uriTag'=> array('inFile'=>false, 'value'=>'/tag/'), 'url'=> array('inFile'=>false, 'value'=>''), - 'cliMode'=> array('inFile'=>false, 'value'=>true) + 'cliMode'=> array('inFile'=>false, 'value'=>true), + 'emailFrom'=> array('inFile'=>false, 'value'=>'') ); function __construct() @@ -92,6 +93,11 @@ class dbSite extends dbJSON return $this->db['title']; } + public function emailFrom() + { + return $this->db['emailFrom']; + } + // Returns the site slogan. public function slogan() { diff --git a/kernel/dbusers.class.php b/kernel/dbusers.class.php index 46400be0..d85bafcc 100644 --- a/kernel/dbusers.class.php +++ b/kernel/dbusers.class.php @@ -10,7 +10,9 @@ class dbUsers extends dbJSON 'password'=> array('inFile'=>false, 'value'=>''), 'salt'=> array('inFile'=>false, 'value'=>'!Pink Floyd!Welcome to the machine!'), 'email'=> array('inFile'=>false, 'value'=>''), - 'registered'=> array('inFile'=>false, 'value'=>'1985-03-15 10:00') + 'registered'=> array('inFile'=>false, 'value'=>'1985-03-15 10:00'), + 'tokenEmail'=> array('inFile'=>false, 'value'=>''), + 'tokenEmailTTL'=>array('inFile'=>false, 'value'=>'2009-03-15 14:00') ); function __construct() @@ -18,7 +20,7 @@ class dbUsers extends dbJSON parent::__construct(PATH_DATABASES.'users.php'); } - // Return an array with the username databases + // Return an array with the username databases, filtered by username. public function getDb($username) { if($this->userExists($username)) @@ -31,6 +33,18 @@ class dbUsers extends dbJSON return false; } + // Return an array with the username databases, filtered by email address. + public function getByEmail($email) + { + foreach($this->db as $user) { + if($user['email']==$email) { + return $user; + } + } + + return false; + } + // Return TRUE if the user exists, FALSE otherwise. public function userExists($username) { @@ -42,13 +56,32 @@ class dbUsers extends dbJSON return $this->db; } - public function setPassword($password) + public function generateTokenEmail($username) + { + // Random hash + $token = sha1(Text::randomText(SALT_LENGTH).time()); + $this->db[$username]['tokenEmail'] = $token; + + // Token time to live, defined by TOKEN_TTL + $this->db[$username]['tokenEmailTTL'] = Date::currentOffset(DB_DATE_FORMAT, TOKEN_TTL); + + // Save the database + if( $this->save() === false ) { + Log::set(__METHOD__.LOG_SEP.'Error occurred when trying to save the database file.'); + return false; + } + + return $token; + } + + public function setPassword($username, $password) { $salt = Text::randomText(SALT_LENGTH); $hash = sha1($password.$salt); - $args['salt'] = $salt; - $args['password'] = $hash; + $args['username'] = $username; + $args['salt'] = $salt; + $args['password'] = $hash; return $this->set($args); } @@ -155,4 +188,4 @@ class dbUsers extends dbJSON return true; } -} +} \ No newline at end of file diff --git a/kernel/helpers/date.class.php b/kernel/helpers/date.class.php index bb9a97b4..1a9923a8 100644 --- a/kernel/helpers/date.class.php +++ b/kernel/helpers/date.class.php @@ -15,6 +15,13 @@ class Date { return $Date->format($format); } + public static function currentOffset($format, $offset) + { + $Date = new DateTime(); + $Date->modify($offset); + return $Date->format($format); + } + // Format a local time/date according to locale settings. public static function format($date, $currentFormat, $outputFormat) { diff --git a/kernel/helpers/email.class.php b/kernel/helpers/email.class.php index 46c5bc0b..f7014182 100644 --- a/kernel/helpers/email.class.php +++ b/kernel/helpers/email.class.php @@ -7,18 +7,18 @@ class Email { { $headers = array(); $headers[] = 'MIME-Version: 1.0'; - $headers[] = 'Content-type: text/plain; charset=utf-8'; + $headers[] = 'Content-type: text/html; charset=utf-8'; $headers[] = 'From: '.$args['from']; $headers[] = 'X-Mailer: PHP/'.phpversion(); $message = ' - Bludit + BLUDIT
-
Bludit
-

'.$args['message'].'

+
BLUDIT
+ '.$args['message'].'
'; diff --git a/kernel/helpers/sanitize.class.php b/kernel/helpers/sanitize.class.php index fa39496f..c41134da 100644 --- a/kernel/helpers/sanitize.class.php +++ b/kernel/helpers/sanitize.class.php @@ -55,6 +55,7 @@ class Sanitize { return true; } + // Returns the email without illegal characters. public static function email($email) { return( filter_var($email, FILTER_SANITIZE_EMAIL) ); diff --git a/kernel/helpers/text.class.php b/kernel/helpers/text.class.php index 7ef37c8b..129722aa 100644 --- a/kernel/helpers/text.class.php +++ b/kernel/helpers/text.class.php @@ -111,6 +111,11 @@ class Text { return $text; } + public static function replaceAssoc(array $replace, $text) + { + return str_replace(array_keys($replace), array_values($replace), $text); + } + public static function cleanUrl($string, $separator='-') { // Transliterate characters to ASCII diff --git a/kernel/helpers/valid.class.php b/kernel/helpers/valid.class.php index d6acbcdc..f8329b0f 100644 --- a/kernel/helpers/valid.class.php +++ b/kernel/helpers/valid.class.php @@ -7,6 +7,7 @@ class Valid { return filter_var($ip, FILTER_VALIDATE_IP); } + // Returns the email filtered or FALSE if the filter fails. public static function email($email) { return filter_var($email, FILTER_VALIDATE_EMAIL); diff --git a/kernel/login.class.php b/kernel/login.class.php index 476c09cb..eea653d3 100644 --- a/kernel/login.class.php +++ b/kernel/login.class.php @@ -59,7 +59,7 @@ class Login { $password = trim($password); if(empty($username) || empty($password)) { - Log::set(__METHOD__.LOG_SEP.'Username or Password empty. Username: '.$username.' - Password: '.$password); + Log::set(__METHOD__.LOG_SEP.'Username or password empty. Username: '.$username.' - Password: '.$password); return false; } @@ -75,6 +75,8 @@ class Login { { $this->setLogin($username, $user['role']); + Log::set(__METHOD__.LOG_SEP.'User logged succeeded by username and password - Username: '.$username); + return true; } else { @@ -84,6 +86,50 @@ class Login { return false; } + public function verifyUserByToken($username, $token) + { + $username = Sanitize::html($username); + $token = Sanitize::html($token); + + $username = trim($username); + $token = trim($token); + + if(empty($username) || empty($token)) { + Log::set(__METHOD__.LOG_SEP.'Username or Token-email empty. Username: '.$username.' - Token-email: '.$token); + return false; + } + + $user = $this->dbUsers->getDb($username); + if($user==false) { + Log::set(__METHOD__.LOG_SEP.'Username does not exist: '.$username); + return false; + } + + $currentTime = Date::current(DB_DATE_FORMAT); + if($user['tokenEmailTTL']<$currentTime) { + Log::set(__METHOD__.LOG_SEP.'Token-email expired: '.$username); + return false; + } + + if($token === $user['tokenEmail']) + { + // Set the user loggued. + $this->setLogin($username, $user['role']); + + // Invalidate the current token. + $this->dbUsers->generateTokenEmail($username); + + Log::set(__METHOD__.LOG_SEP.'User logged succeeded by Token-email - Username: '.$username); + + return true; + } + else { + Log::set(__METHOD__.LOG_SEP.'Token-email incorrect.'); + } + + return false; + } + public function fingerPrint($random=false) { // User agent diff --git a/kernel/security.class.php b/kernel/security.class.php index 062bf3f5..2567ac16 100644 --- a/kernel/security.class.php +++ b/kernel/security.class.php @@ -23,13 +23,13 @@ class Security extends dbJSON $token = Text::randomText(8); $token = sha1($token); - Session::set('token', $token); + Session::set('tokenCSRF', $token); } // Validate the token. public function validateToken($token) { - $sessionToken = Session::get('token'); + $sessionToken = Session::get('tokenCSRF'); return ( !empty($sessionToken) && ($sessionToken===$token) ); } @@ -37,12 +37,12 @@ class Security extends dbJSON // Returns the token. public function getToken() { - return Session::get('token'); + return Session::get('tokenCSRF'); } public function printToken() { - echo Session::get('token'); + echo Session::get('tokenCSRF'); } // ==================================================== diff --git a/languages/en_US.json b/languages/en_US.json index f6bed1f5..f0234543 100644 --- a/languages/en_US.json +++ b/languages/en_US.json @@ -179,5 +179,15 @@ "published": "Published", "scheduled-posts": "Scheduled posts", "statics": "Statics", - "name": "Name" + "name": "Name", + "email-account-settings":"Email account settings", + "sender-email": "Sender email", + "emails-will-be-sent-from-this-address":"Emails will be sent from this address.", + "bludit-login-access-code": "BLUDIT - Login access code", + "check-your-inbox-for-your-login-access-code":"Check your inbox for your log in access code", + "there-was-a-problem-sending-the-email":"There was a problem sending the email", + "back-to-login-form": "Back to login form", + "send-me-a-login-access-code": "Send me a login access code", + "get-login-access-code": "Get login access code", + "email-notification-login-access-code": "

This is a notification from your website {{WEBSITE_NAME}}

You request a login access code, follow the next link:

{{LINK}}

" } \ No newline at end of file