mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-25 01:13:25 +01:00
WIP REST API: using tokens to authenticate as user or impersonate a user without the password and optional limited application rights
This commit is contained in:
parent
f4699543c3
commit
9359e3eee5
@ -11,7 +11,7 @@
|
|||||||
/* Basic information about this app */
|
/* Basic information about this app */
|
||||||
$setup_info['api']['name'] = 'api';
|
$setup_info['api']['name'] = 'api';
|
||||||
$setup_info['api']['title'] = 'EGroupware API';
|
$setup_info['api']['title'] = 'EGroupware API';
|
||||||
$setup_info['api']['version'] = '23.1';
|
$setup_info['api']['version'] = '23.1.001';
|
||||||
$setup_info['api']['versions']['current_header'] = '1.29';
|
$setup_info['api']['versions']['current_header'] = '1.29';
|
||||||
// maintenance release in sync with changelog in doc/rpm-build/debian.changes
|
// maintenance release in sync with changelog in doc/rpm-build/debian.changes
|
||||||
$setup_info['api']['versions']['maintenance_release'] = '23.1.20230620';
|
$setup_info['api']['versions']['maintenance_release'] = '23.1.20230620';
|
||||||
@ -53,6 +53,7 @@ $setup_info['api']['tables'][] = 'egw_ea_identities';
|
|||||||
$setup_info['api']['tables'][] = 'egw_ea_valid';
|
$setup_info['api']['tables'][] = 'egw_ea_valid';
|
||||||
$setup_info['api']['tables'][] = 'egw_ea_notifications';
|
$setup_info['api']['tables'][] = 'egw_ea_notifications';
|
||||||
$setup_info['api']['tables'][] = 'egw_addressbook_shared';
|
$setup_info['api']['tables'][] = 'egw_addressbook_shared';
|
||||||
|
$setup_info['api']['tables'][] = 'egw_tokens';
|
||||||
|
|
||||||
// hooks used by vfs_home_hooks to manage user- and group-directories for the new stream based VFS
|
// hooks used by vfs_home_hooks to manage user- and group-directories for the new stream based VFS
|
||||||
$setup_info['api']['hooks']['addaccount'] = array('EGroupware\\Api\\Vfs\\Hooks::addAccount', 'EGroupware\\Api\\Mail\\Hooks::addaccount');
|
$setup_info['api']['hooks']['addaccount'] = array('EGroupware\\Api\\Vfs\\Hooks::addAccount', 'EGroupware\\Api\\Mail\\Hooks::addaccount');
|
||||||
|
@ -320,7 +320,7 @@ $phpgw_baseline = array(
|
|||||||
'fs_created' => array('type' => 'timestamp','precision' => '8','nullable' => False),
|
'fs_created' => array('type' => 'timestamp','precision' => '8','nullable' => False),
|
||||||
'fs_modified' => array('type' => 'timestamp','precision' => '8','nullable' => False),
|
'fs_modified' => array('type' => 'timestamp','precision' => '8','nullable' => False),
|
||||||
'fs_mime' => array('type' => 'ascii','precision' => '96','nullable' => False),
|
'fs_mime' => array('type' => 'ascii','precision' => '96','nullable' => False),
|
||||||
'fs_size' => array('type' => 'int','precision' => '8', 'default' => '0'),
|
'fs_size' => array('type' => 'int','precision' => '8','default' => '0'),
|
||||||
'fs_creator' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False),
|
'fs_creator' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False),
|
||||||
'fs_modifier' => array('type' => 'int','meta' => 'user','precision' => '4'),
|
'fs_modifier' => array('type' => 'int','meta' => 'user','precision' => '4'),
|
||||||
'fs_active' => array('type' => 'bool','nullable' => False,'default' => 't'),
|
'fs_active' => array('type' => 'bool','nullable' => False,'default' => 't'),
|
||||||
@ -525,5 +525,23 @@ $phpgw_baseline = array(
|
|||||||
'fk' => array(),
|
'fk' => array(),
|
||||||
'ix' => array('contact_id','shared_with'),
|
'ix' => array('contact_id','shared_with'),
|
||||||
'uc' => array(array('shared_by','shared_with','contact_id'))
|
'uc' => array(array('shared_by','shared_with','contact_id'))
|
||||||
|
),
|
||||||
|
'egw_tokens' => array(
|
||||||
|
'fd' => array(
|
||||||
|
'token_id' => array('type' => 'int','precision' => '4','nullable' => False),
|
||||||
|
'account_id' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => '0=all users'),
|
||||||
|
'token_hash' => array('type' => 'ascii','precision' => '128','nullable' => False,'comment' => 'hash of token'),
|
||||||
|
'token_limits' => array('type' => 'ascii','meta' => 'json','precision' => '4096','comment' => 'limit run rights of session'),
|
||||||
|
'token_created' => array('type' => 'timestamp','nullable' => False),
|
||||||
|
'token_created_by' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False),
|
||||||
|
'token_valid_until' => array('type' => 'timestamp'),
|
||||||
|
'token_revoked' => array('type' => 'timestamp'),
|
||||||
|
'token_revoked_by' => array('type' => 'int','meta' => 'user','precision' => '4'),
|
||||||
|
'token_remark' => array('type' => 'varchar','precision' => 255)
|
||||||
|
),
|
||||||
|
'pk' => array('token_id'),
|
||||||
|
'fk' => array(),
|
||||||
|
'ix' => array('account_id'),
|
||||||
|
'uc' => array()
|
||||||
)
|
)
|
||||||
);
|
);
|
@ -849,3 +849,32 @@ function api_upgrade21_1_003()
|
|||||||
{
|
{
|
||||||
return $GLOBALS['setup_info']['api']['currentver'] = '23.1';
|
return $GLOBALS['setup_info']['api']['currentver'] = '23.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add table for user tokens which can be used instead of password
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function api_upgrade23_1()
|
||||||
|
{
|
||||||
|
$GLOBALS['egw_setup']->oProc->CreateTable('egw_tokens',array(
|
||||||
|
'fd' => array(
|
||||||
|
'token_id' => array('type' => 'int','precision' => '4','nullable' => False),
|
||||||
|
'account_id' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => '0=all users'),
|
||||||
|
'token_hash' => array('type' => 'ascii','precision' => '128','nullable' => False,'comment' => 'hash of token'),
|
||||||
|
'token_limits' => array('type' => 'ascii','meta' => 'json','precision' => '4096','comment' => 'limit run rights of session'),
|
||||||
|
'token_created' => array('type' => 'timestamp','nullable' => False),
|
||||||
|
'token_created_by' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False),
|
||||||
|
'token_valid_until' => array('type' => 'timestamp'),
|
||||||
|
'token_revoked' => array('type' => 'timestamp'),
|
||||||
|
'token_revoked_by' => array('type' => 'int','meta' => 'user','precision' => '4'),
|
||||||
|
'token_remark' => array('type' => 'varchar','precision' => 255)
|
||||||
|
),
|
||||||
|
'pk' => array('token_id'),
|
||||||
|
'fk' => array(),
|
||||||
|
'ix' => array('account_id'),
|
||||||
|
'uc' => array()
|
||||||
|
));
|
||||||
|
|
||||||
|
return $GLOBALS['setup_info']['api']['currentver'] = '23.1.001';
|
||||||
|
}
|
105
api/src/Auth/Token.php
Normal file
105
api/src/Auth/Token.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* EGroupware Api: Access token for limited user access instead of passwords
|
||||||
|
*
|
||||||
|
* @link https://www.egroupware.org
|
||||||
|
* @package api
|
||||||
|
* @subpackage auth
|
||||||
|
* @author Ralf Becker <rb@egroupware.org>
|
||||||
|
* @copyright (c) 2023 by Ralf Becker <rb@egroupware.org>
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace EGroupware\Api\Auth;
|
||||||
|
|
||||||
|
use EGroupware\Api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token can be used instead of password to create sessions, which are:
|
||||||
|
* - optionally more limited in the allowed apps than the user
|
||||||
|
* - can be created to be valid to impersonate arbitrary user e.g. for REST API
|
||||||
|
*/
|
||||||
|
class Token extends APi\Storage\Base
|
||||||
|
{
|
||||||
|
const APP = 'api';
|
||||||
|
const TABLE = 'egw_tokens';
|
||||||
|
const PREFIX = 'token';
|
||||||
|
const TOKEN_REGEXP = '/^'.self::PREFIX.'(\d+)_(.*)$/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @throws Api\Exception\WrongParameter
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct(self::APP, self::TABLE, null, '', true, 'object');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate a user with a token
|
||||||
|
*
|
||||||
|
* @param string $user
|
||||||
|
* @param string $token must start with "token<token_id>:", or function will return null
|
||||||
|
* @param ?array& $limits on return limits of token
|
||||||
|
* @return bool|null null: $token is no token, probably a password, false: invalid token, true: valid token for $user
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function authenticate(string $user, string $token, array& $limits=null)
|
||||||
|
{
|
||||||
|
if (!preg_match(self::TOKEN_REGEXP, $token, $matches))
|
||||||
|
{
|
||||||
|
return null; // no a token
|
||||||
|
}
|
||||||
|
if (!($data = self::getInstance()->read([
|
||||||
|
'token_id' => $matches[1],
|
||||||
|
'account_id' => [0, Api\Accounts::getInstance()->name2id($user)],
|
||||||
|
'token_revoked' => null,
|
||||||
|
'(token_valid_until IS NULL OR token_valid_until > NOW())'
|
||||||
|
])) || !password_verify($matches[2], $data['token_hash']))
|
||||||
|
{
|
||||||
|
return false; // wrong/invalid token
|
||||||
|
}
|
||||||
|
$limits = $data['token_limits'] ? json_decode($data['token_limits'], true) : null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a token and return it
|
||||||
|
*
|
||||||
|
* @param int $account_id
|
||||||
|
* @param ?DateTime $until
|
||||||
|
* @param ?string $remark
|
||||||
|
* @param ?array $limits app-name => rights pairs, run rights are everything evaluation to true,
|
||||||
|
* the rights can be an array with more granulate rights, but the app needs to check this itself!
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function create(int $account_id, DateTime $until=null, string $remark=null, array $limits=null): string
|
||||||
|
{
|
||||||
|
$token = Api\Auth::randomstring(16);
|
||||||
|
$inst = self::getInstance();
|
||||||
|
$inst->init([
|
||||||
|
'account_id' => $account_id,
|
||||||
|
'token_hash' => password_hash($token, PASSWORD_DEFAULT),
|
||||||
|
'token_created' => new Api\DateTime(),
|
||||||
|
'token_created_by' => $GLOBALS['egw_info']['user']['account_id'],
|
||||||
|
'token_valid_until' => $until,
|
||||||
|
'token_remark' => $remark,
|
||||||
|
'token_limits' => $limits ? json_encode($limits) : null,
|
||||||
|
]);
|
||||||
|
if (!($token_id = $inst->save()))
|
||||||
|
{
|
||||||
|
throw new Api\Exception('Error storing token');
|
||||||
|
}
|
||||||
|
return self::PREFIX.$token_id.'_'.$token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static self $instance;
|
||||||
|
public static function getInstance()
|
||||||
|
{
|
||||||
|
if (!isset(self::$instance))
|
||||||
|
{
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
}
|
@ -193,6 +193,13 @@ class Session
|
|||||||
*/
|
*/
|
||||||
protected $action;
|
protected $action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit apps available in a session, when not null
|
||||||
|
*
|
||||||
|
* @var array|null app-name => true or array pairs
|
||||||
|
*/
|
||||||
|
public $limits=null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor just loads up some defaults from cookies
|
* Constructor just loads up some defaults from cookies
|
||||||
*
|
*
|
||||||
@ -394,7 +401,7 @@ class Session
|
|||||||
{
|
{
|
||||||
if (isset($_SESSION[$name]))
|
if (isset($_SESSION[$name]))
|
||||||
{
|
{
|
||||||
$_SESSION[$name] = unserialize(trim(mdecrypt_generic(self::$mcrypt,$_SESSION[$name])));
|
$_SESSION[$name] = unserialize(trim(mdecrypt_generic(self::$mcrypt,$_SESSION[$name])), ['allowed_classes' => true]);
|
||||||
//error_log(__METHOD__."() 'decrypting' session var $name: gettype($name) = ".gettype($_SESSION[$name]));
|
//error_log(__METHOD__."() 'decrypting' session var $name: gettype($name) = ".gettype($_SESSION[$name]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,7 +521,7 @@ class Session
|
|||||||
|
|
||||||
if (($blocked = $this->login_blocked($login,$user_ip)) || // too many unsuccessful attempts
|
if (($blocked = $this->login_blocked($login,$user_ip)) || // too many unsuccessful attempts
|
||||||
!empty($GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid]) ||
|
!empty($GLOBALS['egw_info']['server']['global_denied_users'][$this->account_lid]) ||
|
||||||
$auth_check && !$GLOBALS['egw']->auth->authenticate($this->account_lid, $this->passwd, $this->passwd_type) ||
|
$auth_check && !$this->authenticate() ||
|
||||||
$this->account_id && $GLOBALS['egw']->accounts->get_type($this->account_id) == 'g')
|
$this->account_id && $GLOBALS['egw']->accounts->get_type($this->account_id) == 'g')
|
||||||
{
|
{
|
||||||
$this->reason = $blocked ? 'blocked, too many attempts' : 'bad login or password';
|
$this->reason = $blocked ? 'blocked, too many attempts' : 'bad login or password';
|
||||||
@ -704,6 +711,22 @@ class Session
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate user with password or token
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function authenticate()
|
||||||
|
{
|
||||||
|
$is_valid_token = Auth\Token::authenticate($this->account_lid, $this->passwd, $this->limits);
|
||||||
|
if (!isset($is_valid_token))
|
||||||
|
{
|
||||||
|
return $GLOBALS['egw']->auth->authenticate($this->account_lid, $this->passwd, $this->passwd_type);
|
||||||
|
}
|
||||||
|
return $is_valid_token;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if password authentication is required or given token is sufficient
|
* Check if password authentication is required or given token is sufficient
|
||||||
*
|
*
|
||||||
@ -970,7 +993,9 @@ class Session
|
|||||||
'session_action' => $_SERVER['PHP_SELF'],
|
'session_action' => $_SERVER['PHP_SELF'],
|
||||||
'session_flags' => $session_flags,
|
'session_flags' => $session_flags,
|
||||||
// we need the install-id to differ between several installations sharing one tmp-dir
|
// we need the install-id to differ between several installations sharing one tmp-dir
|
||||||
'session_install_id' => $GLOBALS['egw_info']['server']['install_id']
|
'session_install_id' => $GLOBALS['egw_info']['server']['install_id'],
|
||||||
|
// we need to preserve the limits
|
||||||
|
'session_limits' => $this->limits,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1291,6 +1316,9 @@ class Session
|
|||||||
}
|
}
|
||||||
$session =& $_SESSION[self::EGW_SESSION_VAR];
|
$session =& $_SESSION[self::EGW_SESSION_VAR];
|
||||||
|
|
||||||
|
// we need to restore the limits
|
||||||
|
$this->limits = $session['session_limits'];
|
||||||
|
|
||||||
if ($session['session_dla'] <= time() - $GLOBALS['egw_info']['server']['sessions_timeout'])
|
if ($session['session_dla'] <= time() - $GLOBALS['egw_info']['server']['sessions_timeout'])
|
||||||
{
|
{
|
||||||
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session timed out!");
|
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."('$sessionid') session timed out!");
|
||||||
@ -1959,6 +1987,10 @@ class Session
|
|||||||
$GLOBALS['egw']->datetime->__construct(); // to set tz_offset from the now read prefs
|
$GLOBALS['egw']->datetime->__construct(); // to set tz_offset from the now read prefs
|
||||||
}
|
}
|
||||||
$user['apps'] = $GLOBALS['egw']->applications->read_repository();
|
$user['apps'] = $GLOBALS['egw']->applications->read_repository();
|
||||||
|
if (!empty($this->limits))
|
||||||
|
{
|
||||||
|
$user['apps'] = array_intersect_key($user['apps'], array_filter($this->limits));
|
||||||
|
}
|
||||||
$user['domain'] = $this->account_domain;
|
$user['domain'] = $this->account_domain;
|
||||||
$user['sessionid'] = $this->sessionid;
|
$user['sessionid'] = $this->sessionid;
|
||||||
$user['kp3'] = $this->kp3;
|
$user['kp3'] = $this->kp3;
|
||||||
|
@ -72,7 +72,7 @@ if (Session::init_handler())
|
|||||||
require_once($file);
|
require_once($file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$GLOBALS['egw'] = unserialize($_SESSION[Session::EGW_OBJECT_CACHE]);
|
$GLOBALS['egw'] = unserialize($_SESSION[Session::EGW_OBJECT_CACHE], ['allowed_classes' => true]);
|
||||||
|
|
||||||
if (is_object($GLOBALS['egw']) && ($GLOBALS['egw'] instanceof Egw)) // only egw object has wakeup2, setups egw_minimal eg. has not!
|
if (is_object($GLOBALS['egw']) && ($GLOBALS['egw'] instanceof Egw)) // only egw object has wakeup2, setups egw_minimal eg. has not!
|
||||||
{
|
{
|
||||||
@ -109,7 +109,7 @@ $GLOBALS['egw_info']['user']['domain'] = Session::search_instance(
|
|||||||
|
|
||||||
$GLOBALS['egw_info']['server'] += $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']];
|
$GLOBALS['egw_info']['server'] += $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']];
|
||||||
|
|
||||||
// the egw-object instanciates all sub-classes (eg. $GLOBALS['egw']->db) and the egw_info array
|
// the egw-object instantiates all sub-classes (eg. $GLOBALS['egw']->db) and the egw_info array
|
||||||
$GLOBALS['egw'] = new Egw(array_keys($GLOBALS['egw_domain']));
|
$GLOBALS['egw'] = new Egw(array_keys($GLOBALS['egw_domain']));
|
||||||
|
|
||||||
if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && !$GLOBALS['egw_info']['server']['show_domain_selectbox'])
|
if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && !$GLOBALS['egw_info']['server']['show_domain_selectbox'])
|
||||||
@ -117,7 +117,7 @@ if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && !$GLOBALS['egw_inf
|
|||||||
unset($GLOBALS['egw_domain']); // we kill this for security reasons
|
unset($GLOBALS['egw_domain']); // we kill this for security reasons
|
||||||
}
|
}
|
||||||
|
|
||||||
// saving the the egw_info array and the egw-object in the session
|
// saving the egw_info array and the egw-object in the session
|
||||||
if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login')
|
if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login')
|
||||||
{
|
{
|
||||||
$_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info'];
|
$_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info'];
|
||||||
|
Loading…
Reference in New Issue
Block a user