initial plumbing to introduce token expiry and API bans per user

This commit is contained in:
Pliable Pixels 2019-05-11 13:39:40 -04:00
parent e6b7af4583
commit ae14be916c
8 changed files with 91 additions and 14 deletions

View File

@ -245,7 +245,7 @@ DROP TABLE IF EXISTS `Events_Week`;
CREATE TABLE `Events_Week` ( CREATE TABLE `Events_Week` (
`EventId` BIGINT unsigned NOT NULL, `EventId` BIGINT unsigned NOT NULL,
`MonitorId` int(10) unsigned NOT NULL, `MonitorId` int(10) unsigned NOT NULL,
`StartTime` datetime default NULL, `StartTime` datetime default NULL,M
`DiskSpace` bigint default NULL, `DiskSpace` bigint default NULL,
PRIMARY KEY (`EventId`), PRIMARY KEY (`EventId`),
KEY `Events_Week_MonitorId_idx` (`MonitorId`), KEY `Events_Week_MonitorId_idx` (`MonitorId`),
@ -640,6 +640,8 @@ CREATE TABLE `Users` (
`System` enum('None','View','Edit') NOT NULL default 'None', `System` enum('None','View','Edit') NOT NULL default 'None',
`MaxBandwidth` varchar(16), `MaxBandwidth` varchar(16),
`MonitorIds` text, `MonitorIds` text,
`TokenMinExpiry` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`APIEnabled` tinyint(3) UNSIGNED NOT NULL default 1,
PRIMARY KEY (`Id`), PRIMARY KEY (`Id`),
UNIQUE KEY `UC_Username` (`Username`) UNIQUE KEY `UC_Username` (`Username`)
) ENGINE=@ZM_MYSQL_ENGINE@; ) ENGINE=@ZM_MYSQL_ENGINE@;

View File

@ -7,8 +7,9 @@
// returns username if valid, "" if not // returns username if valid, "" if not
std::string verifyToken(std::string jwt_token_str, std::string key) { std::pair <std::string, unsigned int> verifyToken(std::string jwt_token_str, std::string key) {
std::string username = ""; std::string username = "";
int token_issued_at = 0;
try { try {
// is it decodable? // is it decodable?
auto decoded = jwt::decode(jwt_token_str); auto decoded = jwt::decode(jwt_token_str);
@ -24,13 +25,13 @@ std::string verifyToken(std::string jwt_token_str, std::string key) {
std::string type = decoded.get_payload_claim("type").as_string(); std::string type = decoded.get_payload_claim("type").as_string();
if (type != "access") { if (type != "access") {
Error ("Only access tokens are allowed. Please do not use refresh tokens"); Error ("Only access tokens are allowed. Please do not use refresh tokens");
return ""; return std::make_pair("",0);
} }
} }
else { else {
// something is wrong. All ZM tokens have type // something is wrong. All ZM tokens have type
Error ("Missing token type. This should not happen"); Error ("Missing token type. This should not happen");
return ""; return std::make_pair("",0);
} }
if (decoded.has_payload_claim("user")) { if (decoded.has_payload_claim("user")) {
username = decoded.get_payload_claim("user").as_string(); username = decoded.get_payload_claim("user").as_string();
@ -38,19 +39,27 @@ std::string verifyToken(std::string jwt_token_str, std::string key) {
} }
else { else {
Error ("User not found in claim"); Error ("User not found in claim");
return ""; return std::make_pair("",0);
}
if (decoded.has_payload_claim("iat")) {
token_issued_at = (unsigned int) (decoded.get_payload_claim("iat").as_int());
}
else {
Error ("IAT not found in claim. This should not happen");
return std::make_pair("",0);
} }
} // try } // try
catch (const std::exception &e) { catch (const std::exception &e) {
Error("Unable to verify token: %s", e.what()); Error("Unable to verify token: %s", e.what());
return ""; return std::make_pair("",0);
} }
catch (...) { catch (...) {
Error ("unknown exception"); Error ("unknown exception");
return ""; return std::make_pair("",0);
} }
return username; return std::make_pair(username,token_issued_at);
} }
bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash) { bool verifyPassword(const char *username, const char *input_password, const char *db_password_hash) {

View File

@ -27,5 +27,5 @@
bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash); bool verifyPassword( const char *username, const char *input_password, const char *db_password_hash);
std::string verifyToken(std::string token, std::string key); std::pair <std::string, unsigned int> verifyToken(std::string token, std::string key);
#endif // ZM_CRYPT_H #endif // ZM_CRYPT_H

View File

@ -154,7 +154,10 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) {
Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str()); Info ("Inside zmLoadTokenUser, formed key=%s", key.c_str());
std::string username = verifyToken(jwt_token_str, key); std::pair<std::string, unsigned int> ans = verifyToken(jwt_token_str, key);
std::string username = ans.first;
unsigned int iat = ans.second;
if (username != "") { if (username != "") {
char sql[ZM_SQL_MED_BUFSIZ] = ""; char sql[ZM_SQL_MED_BUFSIZ] = "";
snprintf(sql, sizeof(sql), snprintf(sql, sizeof(sql),
@ -175,12 +178,21 @@ User *zmLoadTokenUser (std::string jwt_token_str, bool use_remote_addr ) {
if ( n_users != 1 ) { if ( n_users != 1 ) {
mysql_free_result(result); mysql_free_result(result);
Warning("Unable to authenticate user %s", username.c_str()); Error("Unable to authenticate user %s", username.c_str());
return NULL; return NULL;
} }
MYSQL_ROW dbrow = mysql_fetch_row(result); MYSQL_ROW dbrow = mysql_fetch_row(result);
User *user = new User(dbrow); User *user = new User(dbrow);
unsigned int stored_iat = strtoul(dbrow[14], NULL,0 );
if (stored_iat > iat ) { // admin revoked tokens
mysql_free_result(result);
Error("Token was revoked for %s", username.c_str());
return NULL;
}
Info ("Got stored expiry time of %u",stored_iat);
Info ("Authenticated user '%s' via token", username.c_str()); Info ("Authenticated user '%s' via token", username.c_str());
mysql_free_result(result); mysql_free_result(result);
return user; return user;

View File

@ -1 +1 @@
1.33.8 1.33.9

View File

@ -109,6 +109,19 @@ function userLogin($username='', $password='', $passwordHashed=false, $apiLogin
$password_type = NULL; $password_type = NULL;
if ($saved_user_details) { if ($saved_user_details) {
// if the API layer asked us to login, make sure the user
// has API enabled (admin may have banned API for this user)
if ($apiLogin) {
if ($saved_user_details['APIEnabled'] != 1) {
ZM\Error ("API disabled for: $username");
$_SESSION['loginFailed'] = true;
unset($user);
return false;
}
}
$saved_password = $saved_user_details['Password']; $saved_password = $saved_user_details['Password'];
if ($saved_password[0] == '*') { if ($saved_password[0] == '*') {
// We assume we don't need to support mysql < 4.1 // We assume we don't need to support mysql < 4.1
@ -217,6 +230,17 @@ function validateToken ($token, $allowed_token_type='access') {
$saved_user_details = dbFetchOne ($sql, NULL, $sql_values); $saved_user_details = dbFetchOne ($sql, NULL, $sql_values);
if ($saved_user_details) { if ($saved_user_details) {
$issuedAt = $jwt_payload['iat'];
$minIssuedAt = $saved_user_details['TokenMinExpiry'];
if ($issuedAt < $minIssuedAt) {
ZM\Error ("Token revoked for $username. Please generate a new token");
$_SESSION['loginFailed'] = true;
unset($user);
return array(false, "Token revoked. Please re-generate");
}
$user = $saved_user_details; $user = $saved_user_details;
return array($user, "OK"); return array($user, "OK");
} else { } else {

View File

@ -421,6 +421,7 @@ $SLANG = array(
'Images' => 'Images', 'Images' => 'Images',
'Include' => 'Include', 'Include' => 'Include',
'In' => 'In', 'In' => 'In',
'InvalidateTokens' => 'Invalidate all generated tokens',
'Inverted' => 'Inverted', 'Inverted' => 'Inverted',
'Iris' => 'Iris', 'Iris' => 'Iris',
'KeyString' => 'Key String', 'KeyString' => 'Key String',

View File

@ -29,7 +29,7 @@ $tabs = array();
$tabs['skins'] = translate('Display'); $tabs['skins'] = translate('Display');
$tabs['system'] = translate('System'); $tabs['system'] = translate('System');
$tabs['config'] = translate('Config'); $tabs['config'] = translate('Config');
$tabs['config'] = translate('API'); $tabs['API'] = translate('API');
$tabs['servers'] = translate('Servers'); $tabs['servers'] = translate('Servers');
$tabs['storage'] = translate('Storage'); $tabs['storage'] = translate('Storage');
$tabs['web'] = translate('Web'); $tabs['web'] = translate('Web');
@ -134,7 +134,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
</div> </div>
</form> </form>
<?php
<?php
} else if ( $tab == 'users' ) { } else if ( $tab == 'users' ) {
?> ?>
<form name="userForm" method="post" action="?"> <form name="userForm" method="post" action="?">
@ -424,6 +425,32 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
<?php <?php
} }
?> ?>
<?php
if ($tab == 'API') {
?>
HELLO BABY!
<form method="post">
<input type="submit" name="test" id="test"
value=<?php echo translate("Image") ?> ><br/>
</form>
<?php
function testfun()
{
echo "Your test function on button click is working";
}
if(array_key_exists('test',$_POST)){
testfun();
}
}
?>
<div id="contentButtons"> <div id="contentButtons">
<button type="submit" value="Save"<?php echo $canEdit?'':' disabled="disabled"' ?>><?php echo translate('Save') ?></button> <button type="submit" value="Save"<?php echo $canEdit?'':' disabled="disabled"' ?>><?php echo translate('Save') ?></button>
</div> </div>
@ -432,6 +459,8 @@ foreach ( array_map('basename', glob('skins/'.$current_skin.'/css/*',GLOB_ONLYDI
} }
?> ?>
</div><!-- end #options --> </div><!-- end #options -->
</div> </div>
</div> <!-- end row --> </div> <!-- end row -->