Implement LDAP storage backend for 2-factor-auth (T421).
Allow kolab_auth plugin to populate 'kolab_2fa_factors' config option through 'kolab_auth_role_settings' and hereby define the active authentication factors from the one LDAP query.
This commit is contained in:
parent
8e51918f64
commit
358ac3e33f
8 changed files with 162 additions and 77 deletions
|
@ -20,7 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// available methods/providers. Supported methods are: 'totp','hotp','yubikey'
|
// available methods/providers. Supported methods are: 'totp','hotp','yubikey'
|
||||||
$config['kolab_2fa_drivers'] = array('totp','hotp');
|
$config['kolab_2fa_drivers'] = array('totp');
|
||||||
|
|
||||||
// backend for storing 2-factor-auth related per-user settings
|
// backend for storing 2-factor-auth related per-user settings
|
||||||
// available backends are: 'roundcube', 'ldap', 'sql'
|
// available backends are: 'roundcube', 'ldap', 'sql'
|
||||||
|
@ -36,14 +36,20 @@ $config['kolab_2fa_storage_config'] = array(
|
||||||
'base_dn' => 'ou=People,dc=example,dc=org',
|
'base_dn' => 'ou=People,dc=example,dc=org',
|
||||||
'filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))',
|
'filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))',
|
||||||
'scope' => 'sub',
|
'scope' => 'sub',
|
||||||
|
'debug' => true,
|
||||||
'fieldmap' => array(
|
'fieldmap' => array(
|
||||||
'uid' => 'uid',
|
'active' => 'nsroledn',
|
||||||
'mail' => 'mail',
|
'@totp' => 'kolabAuthTOTP',
|
||||||
'totp' => 'kolabAuthTOTP',
|
'@hotp' => 'kolabAuthHOTP',
|
||||||
'hotp' => 'kolabAuthHOTP',
|
'@yubikey' => 'kolabAuthYubikey',
|
||||||
'yubikey' => 'kolabAuthYubikey',
|
),
|
||||||
|
'valuemap' => array(
|
||||||
|
'nsroledn' => array(
|
||||||
|
'totp' => 'cn=totp-user,dc=example,dc=org',
|
||||||
|
'hotp' => 'cn=hotp-user,dc=example,dc=org',
|
||||||
|
'yubikey' => 'cn=yubikey-user,dc=example,dc=org',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
'debug' => true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// force a two-factor authentication for all users
|
// force a two-factor authentication for all users
|
||||||
|
|
|
@ -96,13 +96,27 @@ class kolab_2fa extends rcube_plugin
|
||||||
$rcmail->config->set_user_prefs($user->get_prefs());
|
$rcmail->config->set_user_prefs($user->get_prefs());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. check if this user/system has 2FA enabled
|
// 2a. let plugins provide the list of active authentication factors
|
||||||
if (($storage = $this->get_storage($args['user'])) && count($factors = (array)$storage->read('active')) > 0) {
|
$lookup = $rcmail->plugins->exec_hook('kolab_2fa_factors', array(
|
||||||
$args['abort'] = true;
|
'user' => $args['user'],
|
||||||
|
'host' => $hostname,
|
||||||
|
'active' => $rcmail->config->get('kolab_2fa_factors'),
|
||||||
|
));
|
||||||
|
if (isset($lookup['active'])) {
|
||||||
|
$factors = (array)$lookup['active'];
|
||||||
|
}
|
||||||
|
// 2b. check storage if this user has 2FA enabled
|
||||||
|
else if ($storage = $this->get_storage($args['user'])) {
|
||||||
|
$factors = (array)$storage->read('active');
|
||||||
|
}
|
||||||
|
|
||||||
// 3. flag session as temporary (no further actions allowed)
|
if (count($factors) > 0) {
|
||||||
$_SESSION['kolab_2fa_time'] = time();
|
$args['abort'] = true;
|
||||||
$_SESSION['kolab_2fa_nonce'] = bin2hex(openssl_random_pseudo_bytes(32));
|
$factors = array_unique($factors);
|
||||||
|
|
||||||
|
// 3. flag session for 2nd factor verification
|
||||||
|
$_SESSION['kolab_2fa_time'] = time();
|
||||||
|
$_SESSION['kolab_2fa_nonce'] = bin2hex(openssl_random_pseudo_bytes(32));
|
||||||
$_SESSION['kolab_2fa_factors'] = $factors;
|
$_SESSION['kolab_2fa_factors'] = $factors;
|
||||||
|
|
||||||
$_SESSION['username'] = $args['user'];
|
$_SESSION['username'] = $args['user'];
|
||||||
|
@ -594,7 +608,6 @@ class kolab_2fa extends rcube_plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!in_array($method, $active)) {
|
if (!in_array($method, $active)) {
|
||||||
$driver->set('active', true);
|
|
||||||
$active[] = $method;
|
$active[] = $method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,7 +226,7 @@ abstract class Base
|
||||||
public function clear()
|
public function clear()
|
||||||
{
|
{
|
||||||
if ($this->storage) {
|
if ($this->storage) {
|
||||||
$this->storage->remove($this->username . ':' . $this->method);
|
$this->storage->remove($this->method);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,6 @@ class HOTP extends Base
|
||||||
'label' => 'created',
|
'label' => 'created',
|
||||||
'generator' => 'time',
|
'generator' => 'time',
|
||||||
),
|
),
|
||||||
'active' => array(
|
|
||||||
'type' => 'boolean',
|
|
||||||
'editable' => false,
|
|
||||||
'hidden' => true,
|
|
||||||
),
|
|
||||||
'counter' => array(
|
'counter' => array(
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
'editable' => false,
|
'editable' => false,
|
||||||
|
@ -89,6 +84,7 @@ class HOTP extends Base
|
||||||
|
|
||||||
if (!strlen($secret)) {
|
if (!strlen($secret)) {
|
||||||
// LOG: "no secret set for user $this->username"
|
// LOG: "no secret set for user $this->username"
|
||||||
|
console("VERIFY HOTP: no secret set for user $this->username");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,11 +47,6 @@ class TOTP extends Base
|
||||||
'label' => 'created',
|
'label' => 'created',
|
||||||
'generator' => 'time',
|
'generator' => 'time',
|
||||||
),
|
),
|
||||||
'active' => array(
|
|
||||||
'type' => 'boolean',
|
|
||||||
'editable' => false,
|
|
||||||
'hidden' => true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
protected $backend;
|
protected $backend;
|
||||||
|
@ -83,6 +78,7 @@ class TOTP extends Base
|
||||||
|
|
||||||
if (!strlen($secret)) {
|
if (!strlen($secret)) {
|
||||||
// LOG: "no secret set for user $this->username"
|
// LOG: "no secret set for user $this->username"
|
||||||
|
console("VERIFY TOTP: no secret set for user $this->username");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,6 @@ class Yubikey extends Base
|
||||||
'label' => 'created',
|
'label' => 'created',
|
||||||
'generator' => 'time',
|
'generator' => 'time',
|
||||||
),
|
),
|
||||||
'active' => array(
|
|
||||||
'type' => 'boolean',
|
|
||||||
'editable' => false,
|
|
||||||
'hidden' => true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
protected $backend;
|
protected $backend;
|
||||||
|
|
|
@ -28,6 +28,7 @@ use \Net_LDAP3;
|
||||||
class LDAP extends Base
|
class LDAP extends Base
|
||||||
{
|
{
|
||||||
private $cache = array();
|
private $cache = array();
|
||||||
|
private $users = array();
|
||||||
private $conn;
|
private $conn;
|
||||||
private $error;
|
private $error;
|
||||||
|
|
||||||
|
@ -56,19 +57,15 @@ class LDAP extends Base
|
||||||
*/
|
*/
|
||||||
public function read($key)
|
public function read($key)
|
||||||
{
|
{
|
||||||
list($username, $method) = $this->split_key($key);
|
if (!isset($this->cache[$key]) && ($rec = $this->get_ldap_record($this->username, $key))) {
|
||||||
|
$pkey = '@' . $key;
|
||||||
if (!$this->config['fieldmap'][$method]) {
|
if (!empty($this->config['fieldmap'][$pkey])) {
|
||||||
$this->cache[$key] = false;
|
$rec = @json_decode($rec[$pkey], true);
|
||||||
// throw new Exception("No LDAP attribute defined for " . $method);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($this->cache[$key]) && ($rec = $this->get_user_record($username))) {
|
|
||||||
$data = false;
|
|
||||||
if (!empty($rec[$method])) {
|
|
||||||
$data = @json_decode($rec[$method], true);
|
|
||||||
}
|
}
|
||||||
$this->cache[$key] = $data;
|
else if ($this->config['fieldmap'][$key]) {
|
||||||
|
$rec = $rec[$key];
|
||||||
|
}
|
||||||
|
$this->cache[$key] = $rec;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->cache[$key];
|
return $this->cache[$key];
|
||||||
|
@ -79,19 +76,45 @@ class LDAP extends Base
|
||||||
*/
|
*/
|
||||||
public function write($key, $value)
|
public function write($key, $value)
|
||||||
{
|
{
|
||||||
list($username, $method) = $this->split_key($key);
|
if ($rec = $this->get_ldap_record($this->username, $key)) {
|
||||||
|
$old_attrs = $rec['_raw'];
|
||||||
|
$new_attrs = $old_attrs;
|
||||||
|
|
||||||
|
// serialize $value into one attribute
|
||||||
|
$pkey = '@' . $key;
|
||||||
|
if ($attr = $this->config['fieldmap'][$pkey]) {
|
||||||
|
$new_attrs[$attr] = $value === null ? '' : json_encode($value);
|
||||||
|
}
|
||||||
|
else if ($attr = $this->config['fieldmap'][$key]) {
|
||||||
|
$new_attrs[$attr] = $this->value_mapping($attr, $value, false);
|
||||||
|
|
||||||
|
// special case nsroledn: keep other roles unknown to us
|
||||||
|
if ($attr == 'nsroledn' && is_array($this->config['valuemap'][$attr])) {
|
||||||
|
$map = $this->config['valuemap'][$attr];
|
||||||
|
$new_attrs[$attr] = array_merge(
|
||||||
|
$new_attrs[$attr],
|
||||||
|
array_filter((array)$old_attrs[$attr], function($f) use ($map) { return !in_array($f, $map); })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (is_array($value)) {
|
||||||
|
foreach ($value as $k => $val) {
|
||||||
|
if ($attr = $this->config['fieldmap'][$k]) {
|
||||||
|
$new_attrs[$attr] = $this->value_mapping($attr, $value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->conn->modify_entry($rec['_dn'], $old_attrs, $new_attrs);
|
||||||
|
|
||||||
|
if (!empty($result)) {
|
||||||
|
$this->cache[$key] = $value;
|
||||||
|
$this->users = array();
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->config['fieldmap'][$method]) {
|
|
||||||
// throw new Exception("No LDAP attribute defined for " . $method);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
if ($rec = $this->get_user_record($username)) {
|
|
||||||
$attrib = $this->config['fieldmap'][$method];
|
|
||||||
$result = $this->conn->modify_entry($rec['dn], ...);
|
|
||||||
return !empty($result);
|
return !empty($result);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,35 +127,41 @@ class LDAP extends Base
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to split the storage key into username and auth-method
|
* Set username to store data for
|
||||||
*/
|
*/
|
||||||
private function split_key($key)
|
public function set_username($username)
|
||||||
{
|
{
|
||||||
return explode(':', $key, 2);
|
parent::set_username($username);
|
||||||
|
|
||||||
|
// reset cached values
|
||||||
|
$this->cache = array();
|
||||||
|
$this->users = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches user data from LDAP addressbook
|
* Fetches user data from LDAP addressbook
|
||||||
*/
|
*/
|
||||||
function get_user_record($user)
|
protected function get_ldap_record($user, $key)
|
||||||
{
|
{
|
||||||
$filter = $this->parse_vars($this->config['filter'], $user);
|
$filter = $this->parse_vars($this->config['filter'], $user, $key);
|
||||||
$base_dn = $this->parse_vars($this->config['base_dn'], $user);
|
$base_dn = $this->parse_vars($this->config['base_dn'], $user, $key);
|
||||||
$scope = $this->config['scope'] ?: 'sub';
|
$scope = $this->config['scope'] ?: 'sub';
|
||||||
|
|
||||||
// get record
|
$cachekey = $base_dn . $filter;
|
||||||
if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array_values($this->config['fieldmap'])))) {
|
if (!isset($this->users[$cachekey])) {
|
||||||
if ($result->count() == 1) {
|
$this->users[$cachekey] = array();
|
||||||
$entries = $result->entries(true);
|
|
||||||
$dn = key($entries);
|
|
||||||
$entry = array_pop($entries);
|
|
||||||
$entry = $this->field_mapping($dn, $entry);
|
|
||||||
|
|
||||||
return $entry;
|
if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array_values($this->config['fieldmap'])))) {
|
||||||
|
if ($result->count() == 1) {
|
||||||
|
$entries = $result->entries(true);
|
||||||
|
$dn = key($entries);
|
||||||
|
$entry = array_pop($entries);
|
||||||
|
$this->users[$cachekey] = $this->field_mapping($dn, $entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return $this->users[$cachekey];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,36 +169,65 @@ class LDAP extends Base
|
||||||
*/
|
*/
|
||||||
protected function field_mapping($dn, $entry)
|
protected function field_mapping($dn, $entry)
|
||||||
{
|
{
|
||||||
$entry['dn'] = $dn;
|
$entry['_dn'] = $dn;
|
||||||
|
$entry['_raw'] = $entry;
|
||||||
|
|
||||||
// fields mapping
|
// fields mapping
|
||||||
foreach ($this->config['fieldmap'] as $field => $attr) {
|
foreach ($this->config['fieldmap'] as $field => $attr) {
|
||||||
$attr_lc = strtolower($attr);
|
$attr_lc = strtolower($attr);
|
||||||
if (isset($entry[$attr_lc])) {
|
if (isset($entry[$attr_lc])) {
|
||||||
$entry[$field] = $entry[$attr_lc];
|
$entry[$field] = $this->value_mapping($attr_lc, $entry[$attr_lc], true);
|
||||||
}
|
}
|
||||||
else if (isset($entry[$attr])) {
|
else if (isset($entry[$attr])) {
|
||||||
$entry[$field] = $entry[$attr];
|
$entry[$field] = $this->value_mapping($attr, $entry[$attr], true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $entry;
|
return $entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function value_mapping($attr, $value, $reverse = false)
|
||||||
|
{
|
||||||
|
if ($map = $this->config['valuemap'][$attr]) {
|
||||||
|
if ($reverse) {
|
||||||
|
$map = array_flip($map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
$value = array_filter(array_map(function($val) use ($map) {
|
||||||
|
return $map[$val];
|
||||||
|
}, $value));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$value = $map[$value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares filter query for LDAP search
|
* Prepares filter query for LDAP search
|
||||||
*/
|
*/
|
||||||
protected function parse_vars($str, $user)
|
protected function parse_vars($str, $user, $key)
|
||||||
{
|
{
|
||||||
// replace variables in filter
|
// replace variables in filter
|
||||||
list($u, $d) = explode('@', $user);
|
list($u, $d) = explode('@', $user);
|
||||||
|
|
||||||
// hierarchal domain string
|
// build hierarchal domain string
|
||||||
if (empty($dc)) {
|
$dc = $this->conn->domain_root_dn($d);
|
||||||
$dc = 'dc=' . strtr($d, array('.' => ',dc='));
|
|
||||||
|
// map key value
|
||||||
|
if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) {
|
||||||
|
$key = $this->config['keymap'][$key];
|
||||||
}
|
}
|
||||||
|
|
||||||
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
|
// TODO: resolve $user into its DN for %udn
|
||||||
|
|
||||||
|
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%k' => $key);
|
||||||
|
|
||||||
return strtr($str, $replaces);
|
return strtr($str, $replaces);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,13 @@ use \rcube_user;
|
||||||
|
|
||||||
class RcubeUser extends Base
|
class RcubeUser extends Base
|
||||||
{
|
{
|
||||||
|
// sefault config
|
||||||
|
protected $config = array(
|
||||||
|
'keymap' => array(
|
||||||
|
'active' => 'kolab_2fa_factors',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
private $cache = array();
|
private $cache = array();
|
||||||
private $user;
|
private $user;
|
||||||
|
|
||||||
|
@ -46,7 +53,7 @@ class RcubeUser extends Base
|
||||||
{
|
{
|
||||||
if (!isset($this->cache[$key]) && ($user = $this->get_user($this->username))) {
|
if (!isset($this->cache[$key]) && ($user = $this->get_user($this->username))) {
|
||||||
$prefs = $user->get_prefs();
|
$prefs = $user->get_prefs();
|
||||||
$pkey = 'kolab_2fa_props_' . $key;
|
$pkey = $this->key2property($key);
|
||||||
$this->cache[$key] = $prefs[$pkey];
|
$this->cache[$key] = $prefs[$pkey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +67,7 @@ class RcubeUser extends Base
|
||||||
{
|
{
|
||||||
if ($user = $this->get_user($this->username)) {
|
if ($user = $this->get_user($this->username)) {
|
||||||
$this->cache[$key] = $value;
|
$this->cache[$key] = $value;
|
||||||
$pkey = 'kolab_2fa_props_' . $key;
|
$pkey = $this->key2property($key);
|
||||||
return $user->save_prefs(array($pkey => $value), true);
|
return $user->save_prefs(array($pkey => $value), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,4 +112,18 @@ class RcubeUser extends Base
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private function key2property($key)
|
||||||
|
{
|
||||||
|
// map key to configured property name
|
||||||
|
if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) {
|
||||||
|
return $this->config['keymap'][$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
// default
|
||||||
|
return 'kolab_2fa_props_' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue