mirror of https://github.com/MISP/MISP
Merge branch '8042' into develop
commit
f905eef8f0
|
@ -139,6 +139,7 @@ CakePlugin::load('SysLogLogable');
|
|||
*/
|
||||
// CakePlugin::load('CertAuth');
|
||||
// CakePlugin::load('ShibbAuth');
|
||||
// CakePlugin::load('LinOTPAuth');
|
||||
/**
|
||||
* You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters:
|
||||
*
|
||||
|
|
|
@ -20,6 +20,7 @@ $config = array(
|
|||
//'auth' => array('CertAuth.Certificate'), // additional authentication methods
|
||||
//'auth' => array('ShibbAuth.ApacheShibb'),
|
||||
//'auth' => array('AadAuth.AadAuthenticate'),
|
||||
//'auth' => array('LinOTPAuth.LinOTP'),
|
||||
),
|
||||
'MISP' => array(
|
||||
'baseurl' => '',
|
||||
|
@ -203,6 +204,8 @@ $config = array(
|
|||
'realm' => 'lino', // the (default) realm of all the users logging in through this system
|
||||
'userModel' => 'User', // name of the User class (MISP class) to check if the user exists
|
||||
'userModelKey' => 'email', // User field that will be used for querying.
|
||||
'verifyssl' => true, // Verify TLS Certificate or not
|
||||
'mixedauth' => false, // false=>Query only LinOTP or true=>OTP from LinOTP, Password from MISP
|
||||
),
|
||||
*/
|
||||
// Warning: The following is a 3rd party contribution and still untested (including security) by the MISP-project team.
|
||||
|
|
|
@ -2306,7 +2306,7 @@ class Server extends AppModel
|
|||
'Security', 'Session.defaults', 'Session.timeout', 'Session.cookieTimeout',
|
||||
'Session.autoRegenerate', 'Session.checkAgent', 'site_admin_debug',
|
||||
'Plugin', 'CertAuth', 'ApacheShibbAuth', 'ApacheSecureAuth', 'OidcAuth',
|
||||
'AadAuth', 'SimpleBackgroundJobs'
|
||||
'AadAuth', 'SimpleBackgroundJobs', 'LinOTPAuth'
|
||||
);
|
||||
$settingsArray = array();
|
||||
foreach ($settingsToSave as $setting) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
App::uses('BaseAuthenticate', 'Controller/Component/Auth');
|
||||
App::uses('LinOTP', 'LinOTPAuth.Lib');
|
||||
App::uses('HttpSocket', 'Network/Http');
|
||||
|
||||
/**
|
||||
* @package Controller.Component.Auth
|
||||
|
@ -10,6 +10,13 @@ App::uses('LinOTP', 'LinOTPAuth.Lib');
|
|||
*/
|
||||
class LinOTPAuthenticate extends BaseAuthenticate
|
||||
{
|
||||
/**
|
||||
* Holds the user information
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $user = false;
|
||||
|
||||
/*
|
||||
* Try to authenticate the incoming request against the LinOTP backend.
|
||||
* The function may redirect the user if there are more authentication steps required that do not fit the standard function signature.
|
||||
|
@ -21,6 +28,79 @@ class LinOTPAuthenticate extends BaseAuthenticate
|
|||
return $user;
|
||||
}
|
||||
|
||||
/*
|
||||
* Query LinOTP
|
||||
*/
|
||||
private static function _linotp_verify($baseUrl, $realm, $user, $password, $verifyssl)
|
||||
{
|
||||
$params = array();
|
||||
$params['ssl_allow_self_signed'] = !$verifyssl;
|
||||
$params['ssl_verify_peer_name'] = $verifyssl;
|
||||
$params['ssl_verify_peer'] = $verifyssl;
|
||||
|
||||
$HttpSocket = new HttpSocket($params);
|
||||
|
||||
// POST data
|
||||
$data = array(
|
||||
"user" => $user,
|
||||
"pass" => $password,
|
||||
"realm" => $realm,
|
||||
);
|
||||
|
||||
$url = "$baseUrl/validate/check";
|
||||
|
||||
CakeLog::debug( "Sending POST request to ${url}");
|
||||
$results = $HttpSocket->post($url, $data);
|
||||
if ($results->code != "200") {
|
||||
return false;
|
||||
}
|
||||
$response = json_decode($results->body());
|
||||
|
||||
if ($response == false) {
|
||||
CakeLog::error("LinOTP request for user ${user} failed.");
|
||||
return false;
|
||||
} else {
|
||||
if (gettype($response) !== "object") {
|
||||
CakeLog::error("Response from LinOTP is not an JSON dictionary/array. Got an " .gettype($response). ": ".$response);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!property_exists($response,"result")) {
|
||||
CakeLog::error("Missing 'result' key in LinOTP response.");
|
||||
return false;
|
||||
}
|
||||
$result = $response->result;
|
||||
|
||||
if (!property_exists($result,"status")) {
|
||||
CakeLog::error("Missing 'status' key in result envelope from LinOTP.");
|
||||
return false;
|
||||
}
|
||||
$status = $result->status;
|
||||
|
||||
if (!property_exists($result, "value")) {
|
||||
CakeLog::error("Missing 'value' key in result envelop from LinOTP.");
|
||||
return false;
|
||||
}
|
||||
$value = $result->value;
|
||||
|
||||
$ret = array(
|
||||
"status" => $status,
|
||||
"value" => $value,
|
||||
);
|
||||
|
||||
if (property_exists($result, 'detail')) {
|
||||
$ret['detail'] = $result->detail;
|
||||
}
|
||||
|
||||
CakeLog::debug("user: ${user} - status: ${status} value: ${value}");
|
||||
// CakeLog::debug(var_dump($ret));
|
||||
return $ret;
|
||||
}
|
||||
// If bad things happens
|
||||
CakeLog::debug("LinOTP-Plugin couldn't parse results from LinOTP API. Check logs.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Retrieve a user by validating the request data
|
||||
*/
|
||||
|
@ -33,21 +113,61 @@ class LinOTPAuthenticate extends BaseAuthenticate
|
|||
$userFields = $request->data['User'];
|
||||
$email = $userFields['email'];
|
||||
$password = $userFields['password'];
|
||||
$otp = $userFields['otp'];
|
||||
|
||||
CakeLog::debug("getUser email: ${email}");
|
||||
|
||||
$linotp = new LinOTP(
|
||||
Configure::read("LinOTPAuth.baseUrl"),
|
||||
Configure::read("LinOTPAuth.realm")
|
||||
);
|
||||
$linOTP_baseUrl = rtrim(Configure::read("LinOTPAuth.baseUrl"), "/");
|
||||
$linOTP_realm = Configure::read("LinOTPAuth.realm");
|
||||
$linOTP_verifyssl = Configure::read("LinOTPAuth.verifyssl");
|
||||
$mixedauth = Configure::read("LinOTPAuth.mixedauth");
|
||||
|
||||
$response = $linotp->validate_check($email, $password);
|
||||
if (!$linOTP_baseUrl || $linOTP_baseUrl === "") {
|
||||
CakeLog::error("LinOTP: Please configure baseUrl.");
|
||||
if ($mixedauth) {
|
||||
throw new CakeException(__d('cake_dev', 'LinOTP: Missing "baseUrl" configuration - access denied!', 'authenticate()'));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If not mixed auth mode - concat password with otp
|
||||
if (!$mixedauth) {
|
||||
$password = $password . $otp;
|
||||
$response = $this->_linotp_verify(
|
||||
$linOTP_baseUrl,
|
||||
$linOTP_realm,
|
||||
$email,
|
||||
$password,
|
||||
$linOTP_verifyssl
|
||||
);
|
||||
} else {
|
||||
// Enforce OTP token by Authentication Form
|
||||
if (!$otp || $otp === "") {
|
||||
throw new CakeException(__d('cake_dev', 'Missing OTP Token.', 'authenticate()'));
|
||||
}
|
||||
|
||||
$response = $this->_linotp_verify(
|
||||
$linOTP_baseUrl,
|
||||
$linOTP_realm,
|
||||
$email,
|
||||
$otp,
|
||||
$linOTP_verifyssl
|
||||
);
|
||||
}
|
||||
|
||||
// If LinOTP didn't reject the request we can go on to further authentication steps, user login or creation
|
||||
if ($response !== false) {
|
||||
if ($response['value'] === true) { // user can be logged in, authentication successful
|
||||
$this->settings['fields'] = array('username' => "email");
|
||||
|
||||
$user = $this->_findUser($email);
|
||||
if ($mixedauth) {
|
||||
$this->settings['fields'] += array('password' => "password");
|
||||
$this->settings['passwordHasher'] = "BlowfishConstant";
|
||||
$user = $this->_findUser($email, $password);
|
||||
} else {
|
||||
$user = $this->_findUser($email);
|
||||
}
|
||||
if ($user) {
|
||||
// When the user logs in for the first time a password prompt will appear
|
||||
// To avoid that very prompt we are changing the `change_pw` value to '0'.
|
||||
|
@ -63,14 +183,19 @@ class LinOTPAuthenticate extends BaseAuthenticate
|
|||
$user = $this->_findUser($email);
|
||||
}
|
||||
|
||||
return $user;
|
||||
// Set instance user to prevent OTP lookup twice
|
||||
self::$user = $user;
|
||||
} else {
|
||||
CakeLog::error("User ${email} authenticated but not found in database.");
|
||||
return false;
|
||||
self::$user = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
// Don't fall back to FormAuthenticate in mixedauth mode.
|
||||
// This enforces the second factor.
|
||||
if ($mixedauth && !self::$user) {
|
||||
throw new CakeException(__d('cake_dev', 'User could not be authenticated by LinOTP.', 'authenticate()'));
|
||||
}
|
||||
return self::$user;
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
<?php
|
||||
|
||||
class LinOTP {
|
||||
/**
|
||||
* LinOTP client library class
|
||||
*
|
||||
* Provides an abstraction of the common authentication methods of LinOTP.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @var string base url at which the LinOTP instance can be found
|
||||
*/
|
||||
protected $base_url;
|
||||
|
||||
/**
|
||||
* @var int Timeout for HTTP requests in milliseconds
|
||||
*/
|
||||
protected $request_timeout;
|
||||
|
||||
/**
|
||||
* @var string Path to the CA certificate to use, or null for default (system) CAs.
|
||||
*/
|
||||
protected $ca_path = null;
|
||||
|
||||
/**
|
||||
* @var string|null default authentication realm
|
||||
*/
|
||||
protected $realm = null;
|
||||
|
||||
/**
|
||||
* LinOTP constructor.
|
||||
* @param $base_url string base URL of LinOTP e.g. https://linotp1.corp.local.example.com/
|
||||
* @param $request_timeout int timeout in milliseconds before pending HTTP requests shall be canceled
|
||||
* @param $ca_path string|null path to the CA bundle or null for system default.
|
||||
*/
|
||||
public function __construct($base_url, $realm=null, int $request_timeout=30000, $ca_path=null)
|
||||
{
|
||||
$this->base_url = $this->_normalize_url($base_url);
|
||||
$this->realm = $realm;
|
||||
$this->request_timeout = $request_timeout;
|
||||
$this->ca_path = $ca_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip trailing slashes (from URLs)
|
||||
* @param $url
|
||||
* @return bool|stringS
|
||||
*/
|
||||
protected function _normalize_url($url) {
|
||||
return rtrim($url, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Check
|
||||
* Performa a /validate/check call against the given LinOTP instance.
|
||||
* @param user the username (opt. including the realm)
|
||||
* @param password the password or OTPPin to validate
|
||||
* @return bool|mixed returns true or false if the validation was successful, if more information are required (e.g. an OTP) an array is return that contains details.
|
||||
*/
|
||||
public function validate_check($user, $password) {
|
||||
CakeLog::debug("Calling /validate/check for ${user}");
|
||||
$data = array(
|
||||
"user" => $user,
|
||||
"pass" => $password,
|
||||
);
|
||||
|
||||
if ($this->realm != null) {
|
||||
$data['realm'] = $this->realm;
|
||||
}
|
||||
|
||||
$response = $this->_post("/validate/check", $data);
|
||||
|
||||
if ($response == false) {
|
||||
CakeLog::error("LinOTP request for user ${user} failed.");
|
||||
return false;
|
||||
} else {
|
||||
if (gettype($response) !== "object") {
|
||||
CakeLog::error("Response from LinOTP is not an JSON dictionary/array. Got an " .gettype($response). ": ".$response);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!property_exists($response,"result")) {
|
||||
CakeLog::error("Missing 'result' key in LinOTP response.");
|
||||
return false;
|
||||
}
|
||||
$result = $response->result;
|
||||
|
||||
if (!property_exists($result,"status")) {
|
||||
CakeLog::error("Missing 'status' key in result envelope from LinOTP.");
|
||||
return false;
|
||||
}
|
||||
$status = $result->status;
|
||||
|
||||
if (!property_exists($result, "value")) {
|
||||
CakeLog::error("Missing 'value' key in result envelop from LinOTP.");
|
||||
return false;
|
||||
}
|
||||
$value = $result->value;
|
||||
|
||||
$ret = array(
|
||||
"status" => $status,
|
||||
"value" => $value,
|
||||
);
|
||||
|
||||
if (property_exists($result, 'detail')) {
|
||||
$ret['detail'] = $result->detail;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a POST request to the given path on the configured LinOTP instance.
|
||||
* @param $path string path part of the request URL
|
||||
* @param $data array the post data
|
||||
* @return bool|mixed false if the request failed otherwise the request body or decoded json may be returned.
|
||||
*/
|
||||
protected function _post($path, $data) {
|
||||
$ch = curl_init();
|
||||
|
||||
$url = $this->base_url . $path;
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->request_timeout);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'MISP LinOTPAuth');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
// if there is a ca_path set tell curl about it.
|
||||
if ($this->ca_path != null) {
|
||||
curl_setopt($ch, CURLOPT_CAPATH, $this->ca_path);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1);
|
||||
}
|
||||
|
||||
CakeLog::debug( "Sending POST request to ${url}");
|
||||
$response = curl_exec($ch);
|
||||
|
||||
$content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$content_length = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
|
||||
$status_code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
|
||||
CakeLog::debug("Response status: ${status_code}");
|
||||
|
||||
if ($status_code >= 300 || $status_code < 200) {
|
||||
CakeLog::debug("Status Code out of range: ${status_code}");
|
||||
}
|
||||
|
||||
$curl_errno = curl_errno($ch);
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// if the request failed return false
|
||||
if ($curl_errno !== 0) {
|
||||
CakeLog::error("curl error: ${curl_error}");
|
||||
return false;
|
||||
} else {
|
||||
// if the response content type hints towards JSON try to deserialize it
|
||||
if ($content_length > 0 && $content_type === 'application/json') {
|
||||
$json_data = json_decode($response);
|
||||
return $json_data;
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -39,6 +39,9 @@
|
|||
<?php
|
||||
echo $this->Form->input('email', array('autocomplete' => 'off', 'autofocus'));
|
||||
echo $this->Form->input('password', array('autocomplete' => 'off'));
|
||||
if (!empty(Configure::read('LinOTPAuth'))) {
|
||||
echo $this->Form->input('otp', array('autocomplete' => 'off', 'type' => 'password', 'label' => 'OTP'));
|
||||
}
|
||||
?>
|
||||
<div class="clear">
|
||||
<?php
|
||||
|
@ -82,6 +85,9 @@ function submitLoginForm() {
|
|||
var url = $form.attr('action')
|
||||
var email = $form.find('#UserEmail').val()
|
||||
var password = $form.find('#UserPassword').val()
|
||||
if (!empty(Configure::read('LinOTPAuth'))) {
|
||||
var otp = $form.find('#UserOtp').val()
|
||||
}
|
||||
if (!$form[0].checkValidity()) {
|
||||
$form[0].reportValidity()
|
||||
} else {
|
||||
|
@ -94,6 +100,9 @@ function submitLoginForm() {
|
|||
var $tmpForm = $('#temp form#UserLoginForm')
|
||||
$tmpForm.find('#UserEmail').val(email)
|
||||
$tmpForm.find('#UserPassword').val(password)
|
||||
if (!empty(Configure::read('LinOTPAuth'))) {
|
||||
$tmpForm.find('#UserOtp').val(otp)
|
||||
}
|
||||
$tmpForm.submit()
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue