mirror of https://github.com/MISP/MISP
Merge branch '5726' into 2.4
commit
6ec8391e46
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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('*'),
|
||||
|
|
|
@ -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')
|
||||
{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
)));
|
||||
?>
|
Loading…
Reference in New Issue