Merge branch '5726' into 2.4

pull/5841/head
iglocska 2020-04-29 15:50:01 +02:00
commit 6ec8391e46
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
5 changed files with 213 additions and 28 deletions

View File

@ -363,7 +363,7 @@ class AppController extends Controller
}
}
} else {
if ($this->params['controller'] !== 'users' || !in_array($this->params['action'], array('login', 'register'))) {
if ($this->params['controller'] !== 'users' || !in_array($this->params['action'], array('login', 'register', 'email_otp'))) {
if (!$this->request->is('ajax')) {
$this->Session->write('pre_login_requested_url', $this->here);
}

View File

@ -569,6 +569,7 @@ class ACLComponent extends Component
'discardRegistrations' => array('perm_site_admin'),
'downloadTerms' => array('*'),
'edit' => array('*'),
'email_otp' => array('*'),
'searchGpgKey' => array('*'),
'fetchGpgKey' => array('*'),
'histogram' => array('*'),

View File

@ -31,6 +31,9 @@ class UsersController extends AppController
// what pages are allowed for non-logged-in users
$allowedActions = array('login', 'logout');
if(!empty(Configure::read('Security.email_otp_enabled'))) {
$allowedActions[] = 'email_otp';
}
if (!empty(Configure::read('Security.allow_self_registration'))) {
$allowedActions[] = 'register';
}
@ -1116,33 +1119,15 @@ class UsersController extends AppController
$this->Auth->constructAuthenticate();
}
}
if ($this->request->is('post') && Configure::read('Security.email_otp_enabled')) {
$user = $this->Auth->identify($this->request, $this->response);
if ($user) {
$this->Session->write('email_otp_user', $user);
return $this->redirect('email_otp');
}
}
if ($this->Auth->login()) {
$this->User->extralog($this->Auth->user(), "login");
$this->User->Behaviors->disable('SysLogLogable.SysLogLogable');
$this->User->id = $this->Auth->user('id');
$user = $this->User->find('first', array(
'conditions' => array(
'User.id' => $this->Auth->user('id')
),
'recursive' => -1
));
$lastUserLogin = $user['User']['last_login'];
unset($user['User']['password']);
$user['User']['action'] = 'login';
$user['User']['last_login'] = $this->Auth->user('current_login');
$user['User']['current_login'] = time();
$this->User->save($user['User'], true, array('id', 'last_login', 'current_login'));
if (empty($this->Auth->authenticate['Form']['passwordHasher']) && !empty($passwordToSave)) {
$this->User->saveField('password', $passwordToSave);
}
$this->User->Behaviors->enable('SysLogLogable.SysLogLogable');
if ($lastUserLogin) {
$readableDatetime = (new DateTime())->setTimestamp($lastUserLogin)->format('D, d M y H:i:s O'); // RFC822
$this->Flash->info(sprintf('Welcome! Last login was on %s', $readableDatetime));
}
// no state changes are ever done via GET requests, so it is safe to return to the original page:
$this->redirect($this->Auth->redirectUrl());
// $this->redirect(array('controller' => 'events', 'action' => 'index'));
$this->_postlogin();
} else {
$dataSourceConfig = ConnectionManager::getDataSource('default')->config;
$dataSource = $dataSourceConfig['datasource'];
@ -1224,6 +1209,35 @@ class UsersController extends AppController
}
}
private function _postlogin()
{
$this->User->extralog($this->Auth->user(), "login");
$this->User->Behaviors->disable('SysLogLogable.SysLogLogable');
$this->User->id = $this->Auth->user('id');
$user = $this->User->find('first', array(
'conditions' => array(
'User.id' => $this->Auth->user('id')
),
'recursive' => -1
));
$lastUserLogin = $user['User']['last_login'];
unset($user['User']['password']);
$user['User']['action'] = 'login';
$user['User']['last_login'] = $this->Auth->user('current_login');
$user['User']['current_login'] = time();
$this->User->save($user['User'], true, array('id', 'last_login', 'current_login'));
if (empty($this->Auth->authenticate['Form']['passwordHasher']) && !empty($passwordToSave)) {
$this->User->saveField('password', $passwordToSave);
}
$this->User->Behaviors->enable('SysLogLogable.SysLogLogable');
if ($lastUserLogin) {
$readableDatetime = (new DateTime())->setTimestamp($lastUserLogin)->format('D, d M y H:i:s O'); // RFC822
$this->Flash->info(__('Welcome! Last login was on %s', $readableDatetime));
}
// no state changes are ever done via GET requests, so it is safe to return to the original page:
$this->redirect($this->Auth->redirectUrl());
}
public function routeafterlogin()
{
// Events list
@ -1656,6 +1670,90 @@ class UsersController extends AppController
}
}
public function email_otp()
{
$user = $this->Session->read('email_otp_user');
if(empty($user)) {
$this->redirect('login');
}
$redis = $this->User->setupRedis();
$user_id = $user['id'];
if ($this->request->is('post') && isset($this->request->data['User']['otp'])) {
$stored_otp = $redis->get('misp:otp:'.$user_id);
if (!empty($stored_otp) && $this->request->data['User']['otp'] == $stored_otp) {
// we invalidate the previously generated OTP
$redis->delete('misp:otp:'.$user_id);
// We login the user with CakePHP
$this->Auth->login($user);
$this->_postlogin();
} else {
$this->Flash->error(__("The OTP is incorrect or has expired"));
}
} else {
// GET Request
// We check for exceptions
$exception_list = Configure::read('Security.email_otp_exceptions');
if (!empty($exception_list)) {
$exceptions = explode(",", $exception_list);
foreach ($exceptions as &$exception) {
if ($user['email'] == trim($exception)) {
// We login the user with CakePHP
$this->Auth->login($user);
$this->_postlogin();
}
}
}
$this->loadModel('Server');
// Generating the OTP
$digits = !empty(Configure::read('Security.email_otp_length')) ? Configure::read('Security.email_otp_length') : $this->Server->serverSettings['Security']['email_otp_length']['value'];
$otp = "";
for ($i=0; $i<$digits; $i++) {
$otp.= random_int(0,9);
}
// We use Redis to cache the OTP
$redis->set('misp:otp:'.$user_id, $otp);
$validity = !empty(Configure::read('Security.email_otp_validity')) ? Configure::read('Security.email_otp_validity') : $this->Server->serverSettings['Security']['email_otp_validity']['value'];
$redis->expire('misp:otp:'.$user_id, (int) $validity * 60);
// Email construction
$body = !empty(Configure::read('Security.email_otp_text')) ? Configure::read('Security.email_otp_text') : $this->Server->serverSettings['Security']['email_otp_text']['value'];
$body = str_replace('$misp', Configure::read('MISP.baseurl'), $body);
$body = str_replace('$org', Configure::read('MISP.org'), $body);
$body = str_replace('$contact', Configure::read('MISP.contact'), $body);
$body = str_replace('$validity', $validity, $body);
$body = str_replace('$otp', $otp, $body);
$body = str_replace('$ip', $this->__getClientIP(), $body);
$body = str_replace('$username', $user['email'], $body);
$result = $this->User->sendEmail(array('User' => $user), $body, false, "[MISP] Email OTP");
if ( $result ) {
$this->Flash->success(__("An email containing a OTP has been sent."));
} else {
$this->Flash->error(__("The email couldn't be sent, please reach out to your administrator."));
}
}
}
/**
* Helper function to determine the IP of a client (proxy aware)
*/
private function __getClientIP() {
$x_forwarded = filter_input(INPUT_SERVER, 'HTTP_X_FORWARDED_FOR', FILTER_SANITIZE_STRING);
$client_ip = filter_input(INPUT_SERVER, 'HTTP_CLIENT_IP', FILTER_SANITIZE_STRING);
if (!empty($x_forwarded)) {
$x_forwarded = explode(",", $x_forwarded);
return $x_forwarded[0];
} elseif(!empty($client_ip)){
return $_client_ip;
} else {
return filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_SANITIZE_STRING);
}
}
// shows some statistics about the instance
public function statistics($page = 'data')
{

View File

@ -1261,6 +1261,54 @@ class Server extends AppModel
'type' => 'boolean',
'null' => true
),
'email_otp_enabled' => array(
'level'=> 2,
'description' => __('Enable two step authentication with a OTP sent by email. Requires e-mailing to be enabled. Warning: You cannot use it in combination with external authentication plugins.'),
'value' => false,
'errorMessage' => '',
'test' => 'testBool',
'beforeHook' => 'otpBeforeHook',
'type' => 'boolean',
'null' => true
),
'email_otp_length' => array (
'level' => 2,
'description' => __('Define the length of the OTP code sent by email'),
'value' => '6',
'errorMessage' => '',
'type' => 'numeric',
'test' => 'testForNumeric',
'null' => true,
),
'email_otp_validity' => array (
'level' => 2,
'description' => __('Define the validity (in minutes) of the OTP code sent by email'),
'value' => '5',
'errorMessage' => '',
'type' => 'numeric',
'test' => 'testForNumeric',
'null' => true,
),
'email_otp_text' => array(
'level' => 2,
'bigField' => true,
'description' => __('The message sent to the user when a new OTP is requested. Use \\n for line-breaks. The following variables will be automatically replaced in the text: $otp = the new OTP generated by MISP, $username = the user\'s e-mail address, $org the Organisation managing the instance, $misp = the url of this instance, $contact = the e-mail address used to contact the support team (as set in MISP.contact), $ip the IP used to complete the first step of the login and $validity the validity time in minutes.'),
'value' => 'Dear MISP user,\n\nYou have attempted to login to MISP ($misp) from $ip with username $username.\n\n Use the following OTP to log into MISP: $otp\n This code is valid for the next $validity minutes.\n\nIf you have any questions, don\'t hesitate to contact us at: $contact.\n\nBest regards,\nYour $org MISP support team',
'errorMessage' => '',
'test' => 'testForEmpty',
'type' => 'string',
'null' => true,
),
'email_otp_exceptions' => array(
'level' => 2,
'bigField' => true,
'description' => __('A comma separated list of emails for which the OTP is disabled. Note that if you remove someone from this list, the OTP will only be asked at next login.'),
'value' => '',
'errorMessage' => '',
'test' => 'testForEmpty',
'type' => 'string',
'null' => true,
),
'allow_self_registration' => array(
'level' => 1,
'description' => __('Enabling this setting will allow users to have access to the pre-auth registration form. This will create an inbox entry for administrators to review.'),
@ -3541,7 +3589,7 @@ class Server extends AppModel
if ($errorMessage) {
return $errorMessage;
}
return 'Value is not a boolean, make sure that you convert \'true\' to true for example.';
return __('Value is not a boolean, make sure that you convert \'true\' to true for example.');
}
return true;
}
@ -3731,6 +3779,14 @@ class Server extends AppModel
return true;
}
public function otpBeforeHook($setting, $value)
{
if ($value && !empty(Configure::read('MISP.disable_emailing'))) {
return __('Emailing is currently disabled. Enabling OTP without e-mailing being configured would lock all users out.');
}
return true;
}
public function testForRPZSerial($value)
{
if ($this->testForEmpty($value) !== true) {

View File

@ -0,0 +1,30 @@
<?php echo $this->Flash->render(); ?>
<div class="actions sideMenu">
<div style="padding: 10px;">
<p> <?php echo __("Your administrator has turned on an additional authentication step which
requires you to enter a OTP (one time password) you have received via email.");?>
</p>
<p> <?php echo __("Make sure to check your SPAM folder.");?> </p>
<a href='<?php echo $baseurl; ?>/users/email_otp'> <button class='btn'> <?php echo __("Resend"); ?> </button></a>
</div>
</div>
<?php
echo $this->element('/genericElements/Form/genericForm', array(
"form" => $this->Form,
"data" => array(
"title" => __("Validate your OTP"),
"fields" => array(
array(
"field" => "otp",
"label" => __("One Time Password"),
"type" => "text",
"placeholder" => __("Enter your OTP here"),
),
),
"submit" => array (
"action" => "EmailOtp",
),
)));
?>