Login access code via email

This commit is contained in:
dignajar 2015-10-20 00:14:28 -03:00
parent fca3ff9ab0
commit c4d8825151
26 changed files with 237 additions and 47 deletions

View File

@ -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);

View File

@ -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').'<br>'.$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}}'=>'<a href="'.$link.'">'.$link.'</a>'
),
$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').'<br>'.$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
// ============================================================================

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('id'=>'jsformplugin'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -1,19 +1,19 @@
<div class="login-form">
<form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>" class="uk-form" autocomplete="off">
<form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login-email' ?>" class="uk-form" autocomplete="off">
<input type="hidden" id="jstoken" name="token" value="<?php $Security->printToken() ?>">
<input type="hidden" id="jstoken" name="tokenCSRF" value="<?php $Security->printToken() ?>">
<div class="uk-form-row">
<input name="email" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Email') ?>" type="text">
</div>
<div class="uk-form-row">
<button type="submit" class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Send me the email</button>
<button type="submit" class="uk-width-1-1 uk-button uk-button-primary uk-button-large"><?php $L->p('Get login access code') ?></button>
</div>
</form>
</div>
<a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>"> Back to login form</a>
<a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>"><i class="uk-icon-chevron-left"></i> <?php $L->p('Back to login form') ?></a>

View File

@ -2,14 +2,14 @@
<form method="post" action="<?php echo HTML_PATH_ADMIN_ROOT.'login' ?>" class="uk-form" autocomplete="off">
<input type="hidden" id="jstoken" name="token" value="<?php $Security->printToken() ?>">
<input type="hidden" id="jstoken" name="tokenCSRF" value="<?php $Security->printToken() ?>">
<div class="uk-form-row">
<input name="username" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Username') ?>" type="text">
</div>
<div class="uk-form-row">
<input name="password" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Password') ?>" type="text">
<input name="password" class="uk-width-1-1 uk-form-large" placeholder="<?php $L->p('Password') ?>" type="password">
</div>
<div class="uk-form-row">
@ -20,4 +20,4 @@
</div>
<a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login-email' ?>"><i class="uk-icon-envelope-o"></i> Log in with email</a>
<a class="login-email" href="<?php echo HTML_PATH_ADMIN_ROOT.'login-email' ?>"><i class="uk-icon-envelope-o"></i> <?php $L->p('Send me a login access code') ?></a>

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-stacked'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -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'=>''

View File

@ -6,7 +6,7 @@ HTML::formOpen(array('class'=>'uk-form-horizontal'));
// Security token
HTML::formInputHidden(array(
'name'=>'token',
'name'=>'tokenCSRF',
'value'=>$Security->getToken()
));

View File

@ -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()
));

View File

@ -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');

View File

@ -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']);
}
}

View File

@ -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()
{

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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 = '<html>
<head>
<title>Bludit</title>
<title>BLUDIT</title>
</head>
<body>
<div style="margin: 0px auto; border: 1px solid #2672ec; padding: 10px;">
<div style="font-size: 26px; padding: 10px; background-color: #2672ec;">Bludit</div>
<p>'.$args['message'].'</p>
<div style="font-size: 26px; padding: 10px; background-color: #2672ec; color: #FFFFFF;">BLUDIT</div>
'.$args['message'].'
</div>
</body>
</html>';

View File

@ -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) );

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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');
}
// ====================================================

View File

@ -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": "<p>This is a notification from your website {{WEBSITE_NAME}}</p><p>You request a login access code, follow the next link:</p><p>{{LINK}}</p>"
}