mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-28 01:29:05 +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 */
|
||||
$setup_info['api']['name'] = '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';
|
||||
// maintenance release in sync with changelog in doc/rpm-build/debian.changes
|
||||
$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_notifications';
|
||||
$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
|
||||
$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_modified' => array('type' => 'timestamp','precision' => '8','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_modifier' => array('type' => 'int','meta' => 'user','precision' => '4'),
|
||||
'fs_active' => array('type' => 'bool','nullable' => False,'default' => 't'),
|
||||
@ -525,5 +525,23 @@ $phpgw_baseline = array(
|
||||
'fk' => array(),
|
||||
'ix' => array('contact_id','shared_with'),
|
||||
'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()
|
||||
)
|
||||
);
|
||||
);
|
@ -848,4 +848,33 @@ function api_upgrade21_1_002()
|
||||
function api_upgrade21_1_003()
|
||||
{
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@ -394,7 +401,7 @@ class Session
|
||||
{
|
||||
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]));
|
||||
}
|
||||
}
|
||||
@ -514,7 +521,7 @@ class Session
|
||||
|
||||
if (($blocked = $this->login_blocked($login,$user_ip)) || // too many unsuccessful attempts
|
||||
!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->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
|
||||
*
|
||||
@ -970,7 +993,9 @@ class Session
|
||||
'session_action' => $_SERVER['PHP_SELF'],
|
||||
'session_flags' => $session_flags,
|
||||
// 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];
|
||||
|
||||
// we need to restore the limits
|
||||
$this->limits = $session['session_limits'];
|
||||
|
||||
if ($session['session_dla'] <= time() - $GLOBALS['egw_info']['server']['sessions_timeout'])
|
||||
{
|
||||
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
|
||||
}
|
||||
$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['sessionid'] = $this->sessionid;
|
||||
$user['kp3'] = $this->kp3;
|
||||
|
@ -72,7 +72,7 @@ if (Session::init_handler())
|
||||
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!
|
||||
{
|
||||
@ -109,7 +109,7 @@ $GLOBALS['egw_info']['user']['domain'] = Session::search_instance(
|
||||
|
||||
$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']));
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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')
|
||||
{
|
||||
$_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info'];
|
||||
|
Loading…
Reference in New Issue
Block a user