Merge pull request #6214 from JakubOnderka/otp-encryption

fix: [otp] Allow to send encrypted OTP by mail
pull/6262/head
Jakub Onderka 2020-08-30 11:22:05 +02:00 committed by GitHub
commit 8f806c4f1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 80 deletions

View File

@ -1,6 +1,9 @@
<?php
App::uses('AppController', 'Controller');
/**
* @property User $User
*/
class UsersController extends AppController
{
public $newkey;
@ -1678,71 +1681,73 @@ 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"));
$user = $this->Session->read('email_otp_user');
if (empty($user)) {
$this->redirect('login');
}
} else {
// GET Request
$redis = $this->User->setupRedisWithException();
$user_id = $user['id'];
// 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();
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->del('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"));
}
}
}
$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."));
}
}
}
// 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 = 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 = 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 = 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);
// Fetch user that contains also PGP or S/MIME keys for e-mail encryption
$userForSendMail = $this->User->getUserById($user_id);
$result = $this->User->sendEmail($userForSendMail, $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)

View File

@ -366,6 +366,10 @@ class SendEmail
throw new SendEmailException('Emailing is currently disabled on this instance.');
}
if (!isset($user['User'])) {
throw new InvalidArgumentException("Invalid user model provided.");
}
// Check if the e-mail can be encrypted
$canEncryptGpg = isset($user['User']['gpgkey']) && !empty($user['User']['gpgkey']);
$canEncryptSmime = isset($user['User']['certif_public']) && !empty($user['User']['certif_public']) && Configure::read('SMIME.enabled');

View File

@ -584,36 +584,38 @@ class User extends AppModel
return $user;
}
// get the current user and rearrange it to be in the same format as in the auth component
public function getAuthUser($id)
/**
* @param int $id
* @return array|null
*/
public function getUserById($id)
{
if (empty($id)) {
throw new NotFoundException('Invalid user ID.');
}
$conditions = array('User.id' => $id);
$user = $this->find(
return $this->find(
'first',
array(
'conditions' => $conditions,
'conditions' => array('User.id' => $id),
'recursive' => -1,
'contain' => array(
'Organisation',
'Role',
'Server',
'UserSetting'
'UserSetting',
)
)
);
}
// get the current user and rearrange it to be in the same format as in the auth component
public function getAuthUser($id)
{
$user = $this->getUserById($id);
if (empty($user)) {
return $user;
}
// Rearrange it a bit to match the Auth object created during the login
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
$user['User']['UserSetting'] = $user['UserSetting'];
unset($user['Organisation'], $user['Role'], $user['Server']);
return $user['User'];
return $this->rearrangeToAuthForm($user);
}
// get the current user and rearrange it to be in the same format as in the auth component
@ -624,11 +626,7 @@ class User extends AppModel
if (empty($user)) {
return $user;
}
// Rearrange it a bit to match the Auth object created during the login
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
return $user['User'];
return $this->rearrangeToAuthForm($user);
}
public function getAuthUserByExternalAuth($auth_key)
@ -649,11 +647,28 @@ class User extends AppModel
if (empty($user)) {
return $user;
}
// Rearrange it a bit to match the Auth object created during the login
return $this->rearrangeToAuthForm($user);
}
/**
* User model is a mess. Sometimes it is necessary to convert User model to form that is created during the login
* process. This method do that work for you.
*
* @param array $user
* @return array
*/
public function rearrangeToAuthForm(array $user)
{
if (!isset($user['User'])) {
throw new InvalidArgumentException('Invalid user model provided.');
}
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
unset($user['Organisation'], $user['Role'], $user['Server']);
if (isset($user['UserSetting'])) {
$user['User']['UserSetting'] = $user['UserSetting'];
}
return $user['User'];
}