mirror of https://github.com/MISP/MISP
chg: [security] OTP support for HOTP
parent
afbb9fab95
commit
cb74ad507f
|
@ -310,7 +310,7 @@ class AppController extends Controller
|
|||
$this->__accessMonitor($user);
|
||||
|
||||
} else {
|
||||
$preAuthActions = array('login', 'register', 'getGpgPublicKey', 'logout401', 'totp');
|
||||
$preAuthActions = array('login', 'register', 'getGpgPublicKey', 'logout401', 'otp');
|
||||
if (!empty(Configure::read('Security.email_otp_enabled'))) {
|
||||
$preAuthActions[] = 'email_otp';
|
||||
}
|
||||
|
@ -602,7 +602,7 @@ class AppController extends Controller
|
|||
}
|
||||
|
||||
// Check if user must create TOTP secret, force them to be on that page as long as needed.
|
||||
if (!$user['totp'] && Configure::read('Security.totp_required') && !$this->_isControllerAction(['users' => ['terms', 'change_pw', 'logout', 'login', 'totp_new']])) { // TOTP is mandatory for users, prevent login until the user has configured their TOTP
|
||||
if (!$user['totp'] && Configure::read('Security.otp_required') && !$this->_isControllerAction(['users' => ['terms', 'change_pw', 'logout', 'login', 'totp_new']])) { // TOTP is mandatory for users, prevent login until the user has configured their TOTP
|
||||
$this->redirect(array('controller' => 'users', 'action' => 'totp_new', 'admin' => false));
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -745,7 +745,8 @@ class ACLComponent extends Component
|
|||
'downloadTerms' => array('*'),
|
||||
'edit' => array('self_management_enabled'),
|
||||
'email_otp' => array('*'),
|
||||
'totp' => array('*'),
|
||||
'otp' => array('*'),
|
||||
'hotp' => array('*'),
|
||||
'totp_new' => array('*'),
|
||||
'totp_delete' => array('perm_admin'),
|
||||
'searchGpgKey' => array('*'),
|
||||
|
|
|
@ -29,7 +29,7 @@ class UsersController extends AppController
|
|||
parent::beforeFilter();
|
||||
|
||||
// what pages are allowed for non-logged-in users
|
||||
$allowedActions = array('login', 'logout', 'getGpgPublicKey', 'logout401', 'totp');
|
||||
$allowedActions = array('login', 'logout', 'getGpgPublicKey', 'logout401', 'otp');
|
||||
if(!empty(Configure::read('Security.email_otp_enabled'))) {
|
||||
$allowedActions[] = 'email_otp';
|
||||
}
|
||||
|
@ -1185,7 +1185,7 @@ class UsersController extends AppController
|
|||
}
|
||||
$unauth_user = $this->User->find('first', [
|
||||
'conditions' => ['User.email' => $this->request->data['User']['email']],
|
||||
'fields' => ['User.password', 'User.totp'],
|
||||
'fields' => ['User.password', 'User.totp', 'User.hotp_counter'],
|
||||
'recursive' => -1,
|
||||
]);
|
||||
if ($unauth_user) {
|
||||
|
@ -1200,8 +1200,8 @@ class UsersController extends AppController
|
|||
if ($unauth_user['User']['totp'] && !$unauth_user['User']['disabled'] && class_exists('\OTPHP\TOTP')) {
|
||||
$user = $this->Auth->identify($this->request, $this->response);
|
||||
if ($user && !$user['disabled']) {
|
||||
$this->Session->write('totp_user', $user);
|
||||
return $this->redirect('totp');
|
||||
$this->Session->write('otp_user', $user);
|
||||
return $this->redirect('otp');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1757,9 +1757,9 @@ class UsersController extends AppController
|
|||
}
|
||||
}
|
||||
|
||||
public function totp()
|
||||
public function otp()
|
||||
{
|
||||
$user = $this->Session->read('totp_user');
|
||||
$user = $this->Session->read('otp_user');
|
||||
if (empty($user)) {
|
||||
$this->redirect('login');
|
||||
}
|
||||
|
@ -1770,22 +1770,50 @@ class UsersController extends AppController
|
|||
throw new ForbiddenException('You have reached the maximum number of login attempts. Please wait ' . $expire . ' seconds and try again.');
|
||||
}
|
||||
$secret = $user['totp'];
|
||||
$otp = \OTPHP\TOTP::create($secret);
|
||||
$otp_now = $otp->now();
|
||||
if (trim($this->request->data['User']['otp']) == $otp_now) {
|
||||
// we invalidate the previously generated OTP
|
||||
// We login the user with CakePHP
|
||||
$totp = \OTPHP\TOTP::create($secret);
|
||||
$hotp = \OTPHP\HOTP::create($secret);
|
||||
if ($totp->verify(trim($this->request->data['User']['otp']))) {
|
||||
// OTP is correct, we login the user with CakePHP
|
||||
$this->Auth->login($user);
|
||||
$this->_postlogin();
|
||||
} elseif (isset($user['hotp_counter']) && $hotp->verify(trim($this->request->data['User']['otp']), $user['hotp_counter'])) {
|
||||
// HOTP is correct, update the counter and login
|
||||
$this->User->id = $user['id'];
|
||||
$this->User->saveField('hotp_counter', $user['hotp_counter']+1);
|
||||
$this->Auth->login($user);
|
||||
$this->_postlogin();
|
||||
} else {
|
||||
$this->Flash->error(__("The OTP is incorrect or has expired"));
|
||||
$fieldsDescrStr = 'User (' . $user['id'] . '): ' . $user['email']. ' wrong TOTP token';
|
||||
$fieldsDescrStr = 'User (' . $user['id'] . '): ' . $user['email']. ' wrong OTP token';
|
||||
$this->User->extralog($user, "login_fail", $fieldsDescrStr, '');
|
||||
$this->Bruteforce->insert($user['email']);
|
||||
}
|
||||
} else {
|
||||
// GET Request, just show the form
|
||||
}
|
||||
// GET Request or wrong OTP, just show the form
|
||||
$this->set('totp', $user['totp']? true : false);
|
||||
$this->set('hotp_counter', $user['hotp_counter']);
|
||||
}
|
||||
|
||||
public function hotp()
|
||||
{
|
||||
if (!class_exists('\OTPHP\HOTP')) {
|
||||
$this->Flash->error(__("The required PHP libraries to support OTP are not installed. Please contact your administrator to address this."));
|
||||
$this->redirect($this->referer());
|
||||
}
|
||||
|
||||
$user = $this->User->find('first', array(
|
||||
'recursive' => -1,
|
||||
'conditions' => array('User.id' => $this->Auth->user('id')),
|
||||
'fields' => array(
|
||||
'totp', 'email', 'id', 'hotp_counter'
|
||||
)
|
||||
));
|
||||
$hotp = \OTPHP\HOTP::create($user['User']['totp'], $user['User']['hotp_counter']);
|
||||
$hotp_codes = [];
|
||||
for ($i=$user['User']['hotp_counter']; $i < $user['User']['hotp_counter']+50 ; $i++) {
|
||||
$hotp_codes[$i] = $hotp->at($i);
|
||||
}
|
||||
$this->set('hotp_codes', $hotp_codes);
|
||||
}
|
||||
|
||||
public function totp_new()
|
||||
|
@ -1816,25 +1844,26 @@ class UsersController extends AppController
|
|||
$this->redirect($this->referer());
|
||||
}
|
||||
|
||||
$secret = $this->Session->read('totp_secret'); // Reload secret from session.
|
||||
$secret = $this->Session->read('otp_secret'); // Reload secret from session.
|
||||
if ($secret) {
|
||||
$otp = \OTPHP\TOTP::create($secret);
|
||||
$totp = \OTPHP\TOTP::create($secret);
|
||||
} else {
|
||||
$otp = \OTPHP\TOTP::create();
|
||||
$secret = $otp->getSecret();
|
||||
$this->Session->write('totp_secret', $secret); // Store in session, this is to keep the same QR code even if the page refreshes.
|
||||
$totp = \OTPHP\TOTP::create();
|
||||
$secret = $totp->getSecret();
|
||||
$this->Session->write('otp_secret', $secret); // Store in session, this is to keep the same QR code even if the page refreshes.
|
||||
}
|
||||
if ($this->request->is('post') && isset($this->request->data['User']['otp'])) {
|
||||
$otp_now = $otp->now();
|
||||
if (trim($this->request->data['User']['otp']) == $otp_now) {
|
||||
if ($totp->verify(trim($this->request->data['User']['otp']))) {
|
||||
// we know the user can generate TOTP tokens, save the new TOTP to the database
|
||||
$this->User->id = $user['User']['id'];
|
||||
$this->User->saveField('totp', $secret);
|
||||
$this->_refreshAuth();
|
||||
$this->User->saveField('hotp_counter', 0);
|
||||
$this->_refreshAuth();
|
||||
$this->Flash->info(__('The OTP is correct and now active for your account.'));
|
||||
$fieldsDescrStr = 'User (' . $user['User']['id'] . '): ' . $user['User']['email']. ' TOTP token created';
|
||||
$this->User->extralog($this->Auth->user(), "update", $fieldsDescrStr, '');
|
||||
$this->redirect(array('controller' => 'events', 'action'=> 'index'));
|
||||
// redirect to a page that gives the next 50 HOTP
|
||||
$this->redirect(array('controller' => 'users', 'action'=> 'hotp'));
|
||||
} else {
|
||||
$this->Flash->error(__("The OTP is incorrect or has expired."));
|
||||
}
|
||||
|
@ -1847,8 +1876,10 @@ class UsersController extends AppController
|
|||
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
|
||||
);
|
||||
$writer = new \BaconQrCode\Writer($renderer);
|
||||
$qrcode = $writer->writeString('otpauth://totp/' . Configure::read('MISP.org') . ' MISP (' . $user['User']['email'] . ')?secret=' . $secret);
|
||||
$writer = preg_replace('/^.+\n/', '', $qrcode); // ignore first <?xml version line
|
||||
$totp->setLabel($user['User']['email']);
|
||||
$totp->setIssuer(Configure::read('MISP.org') . ' MISP');
|
||||
$qrcode = $writer->writeString($totp->getProvisioningUri());
|
||||
$qrcode = preg_replace('/^.+\n/', '', $qrcode); // ignore first <?xml version line
|
||||
|
||||
$this->set('qrcode', $qrcode);
|
||||
$this->set('secret', $secret);
|
||||
|
|
|
@ -1954,6 +1954,7 @@ class AppModel extends Model
|
|||
$sqlArray[] = "UPDATE `over_correlating_values` SET `value` = LOWER(`value`) COLLATE utf8mb4_unicode_ci;";
|
||||
case 110:
|
||||
$sqlArray[] = "ALTER TABLE `users` ADD `totp` varchar(255) DEFAULT NULL;";
|
||||
$sqlArray[] = "ALTER TABLE `users` ADD `hotp_counter` int(11) DEFAULT NULL;";
|
||||
break;
|
||||
case 'fixNonEmptySharingGroupID':
|
||||
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
|
||||
|
|
|
@ -2154,7 +2154,7 @@ class Server extends AppModel
|
|||
return true;
|
||||
}
|
||||
|
||||
public function otpBeforeHook($setting, $value)
|
||||
public function email_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.');
|
||||
|
@ -2162,7 +2162,7 @@ class Server extends AppModel
|
|||
return true;
|
||||
}
|
||||
|
||||
public function totpBeforeHook($setting, $value)
|
||||
public function otpBeforeHook($setting, $value)
|
||||
{
|
||||
if ($value && (!class_exists('\OTPHP\TOTP') || !class_exists('\BaconQrCode\Writer'))) {
|
||||
return __('The TOTP and QR code generation libraries are not installed. Enabling OTP without those libraries installed would lock all users out.');
|
||||
|
@ -6397,12 +6397,12 @@ class Server extends AppModel
|
|||
'type' => 'boolean',
|
||||
'null' => true,
|
||||
],
|
||||
'totp_required' => array(
|
||||
'otp_required' => array(
|
||||
'level' => 2,
|
||||
'description' => __('Require authentication with TOTP. Users that do not have TOTP configured will be forced to create a token at first login. You cannot use it in combination with external authentication plugins.'),
|
||||
'description' => __('Require authentication with OTP. Users that do not have (T/H)OTP configured will be forced to create a token at first login. You cannot use it in combination with external authentication plugins.'),
|
||||
'value' => false,
|
||||
'test' => 'testBool',
|
||||
'beforeHook' => 'totpBeforeHook',
|
||||
'beforeHook' => 'otpBeforeHook',
|
||||
'type' => 'boolean',
|
||||
'null' => true
|
||||
),
|
||||
|
@ -6411,7 +6411,7 @@ class Server extends AppModel
|
|||
'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,
|
||||
'test' => 'testBool',
|
||||
'beforeHook' => 'otpBeforeHook',
|
||||
'beforeHook' => 'email_otpBeforeHook',
|
||||
'type' => 'boolean',
|
||||
'null' => true
|
||||
),
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<div class="users form">
|
||||
<h2><?php echo __('Paper based Single Use Tokens');?></h2>
|
||||
<p><?php echo __('The following list contains the next tokens in case you do not have your phone/software. <br />Make sure you print these out.');?></p>
|
||||
<pre><?php
|
||||
$count = count($hotp_codes);
|
||||
$rows = round($count / 5); // 5 rows
|
||||
$i = 1;
|
||||
foreach ($hotp_codes as $key => $value) {
|
||||
if ($key < 10) print(" ");
|
||||
print("$key: $value");
|
||||
if ($i == 5) {
|
||||
print("\n");
|
||||
$i = 1;
|
||||
} else {
|
||||
print(" ");
|
||||
$i++;
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
</pre>
|
||||
</div>
|
||||
<?= $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'globalActions', 'menuItem' => 'view'));
|
||||
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
<div class="actions sideMenu">
|
||||
<div style="padding: 10px;">
|
||||
<p><?php echo __("Your account requires an TOTP token to login. (Time-Based One-Time Password)");?></p>
|
||||
<p><?php echo __("Your account requires an OTP token to login. (One-Time Password)");?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$label = __("Enter either your TOTP or paper based Single Use Token number ") . $hotp_counter;
|
||||
|
||||
echo $this->element('/genericElements/Form/genericForm', array(
|
||||
"form" => $this->Form,
|
||||
"data" => array(
|
||||
|
@ -14,13 +16,13 @@ echo $this->element('/genericElements/Form/genericForm', array(
|
|||
"fields" => array(
|
||||
array(
|
||||
"field" => "otp",
|
||||
"label" => __("One Time Password"),
|
||||
"label" => $label,
|
||||
"type" => "text",
|
||||
"placeholder" => __("Enter your OTP here"),
|
||||
),
|
||||
)
|
||||
),
|
||||
"submit" => array (
|
||||
"action" => "totp",
|
||||
"action" => "otp",
|
||||
),
|
||||
)));
|
||||
?>
|
|
@ -24,7 +24,8 @@ $boolean = sprintf(
|
|||
$isTotp ? 'label label-success label-padding' : 'label label-important label-padding',
|
||||
$isTotp ? __('Yes') : __('No'));
|
||||
$totpHtml = $boolean;
|
||||
$totpHtml .= ($isTotp ? '' : $this->Html->link(__('Generate'), array('action' => 'totp_new', $user['User']['id'])));
|
||||
$totpHtml .= (!$isTotp && !$admin_view ? $this->Html->link(__('Generate'), array('action' => 'totp_new')) : '');
|
||||
$totpHtml .= ($isTotp && !$admin_view ? $this->Html->link(__('View paper tokens'), array('action' => 'hotp', $user['User']['id'])): '');
|
||||
$totpHtml .= ($admin_view && $isTotp ? ' ' . $this->Form->postLink(__('Delete'), array('action' => 'totp_delete', $user['User']['id'])) : '');
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue