From: Jon Langseth Date: Thu, 19 Jan 2012 23:47:29 +0000 (+0100) Subject: Added general framework for API-key authentication. From this point, the API requires... X-Git-Url: https://git.defcon.no/?p=hermes;a=commitdiff_plain;h=6496a650839b71ea7bfaab1b3b461886de4475a8 Added general framework for API-key authentication. From this point, the API requires a valid authentication and session handling. --- diff --git a/api/auth.php b/api/auth.php index 6a8188e..1513aae 100644 --- a/api/auth.php +++ b/api/auth.php @@ -19,6 +19,8 @@ if ( !$config['sql_link'] ) } //************************************************************************************* +if ( ( $_SERVER['PATH_INFO'] == "/login" ) || ( $_SERVER['PATH_INFO'] == "/logout" ) ) +{ switch ( $_SERVER['PATH_INFO'] ) { case "/login": @@ -46,7 +48,7 @@ if ( !$config['sql_link'] ) } else if ( array_key_exists('api_key', $_GET) ) { - if ( apikey_verify( sql_clean( $_GET['api_key'] ) ) == 1 ) + if ( verify_apikey( sql_clean( $_GET['api_key'] ) ) == 1 ) { $type = "key"; $authid = $_GET['api_key']; @@ -66,20 +68,6 @@ if ( !$config['sql_link'] ) $auth_key = update_authkey( $session_name, $authid ); print json_encode( array( 'response' => 'ok', 'session' => $session_name, 'auth_key' => $auth_key )); break; - case "/ping": - // API clients are required to periodically ping the server - // The time between pings (interval) is 5 minutes? - // A ping call refreshes cookie lifetimes, then - // generates and stores a new auth_key - // The ping required a valid session... - // A successful ping returns a 'response' => 'pong' - // along with the new auth_key. - token_auth(); - $session_name = $_GET['session']; - $authid = $_SESSION['authid']; - $auth_key = update_authkey( $session_name, $authid ); - print json_encode( array( 'response' => 'pong', 'auth_key' => $auth_key )); - break; case "/logout": // De-authenticate/deauthorize the ongoing session. // I.e. destroy session data, remove session cookies. @@ -95,12 +83,91 @@ if ( !$config['sql_link'] ) else print json_encode ( array( 'response' => 'ok') ); break; - case "/list_users": - // List valid API user-acounts. - // Fail with notauthorized if current authentication - // does not have write access. - // Should not return users from backend, - // but should only return users with authorization. + default: + print json_encode ( array( 'response' => 'invalid') ); + } +} +else +{ + token_auth(); + + switch ( $_SERVER['PATH_INFO'] ) + { + case "/ping": + // API clients are required to periodically ping the server + // The time between pings (interval) is 5 minutes? + // A ping call refreshes cookie lifetimes, then + // generates and stores a new auth_key + // The ping required a valid session... + // A successful ping returns a 'response' => 'pong' + // along with the new auth_key. + $session_name = $_GET['session']; + $authid = $_SESSION['authid']; + $auth_key = update_authkey( $session_name, $authid ); + print json_encode( array( 'response' => 'pong', 'auth_key' => $auth_key )); + break; + case "/new_apikey": + // If the current authorization has write access, create + // a new API key with requested access (ro/rw). + if ( ! can_write() ) + simple_authfail(); + + if ( array_key_exists('host_ip', $_GET ) + && array_key_exists('access', $_GET )) + { + $host = $_GET['host_ip']; + $access = $_GET['access']; + + if (! preg_match("/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/", $host) || ! authlevel_value( $access ) ) + { + print json_encode ( array( 'response' => 'invalid', 'cause' => 'parameters' ) ); + break; + } + $level = authlevel_value( $access ); + $key = add_apikey( $host, $level ); + if ( ! $key ) + { + print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.')); + break; + } + print json_encode( array( 'response' => 'ok', 'key' => $key, 'host' => $host, 'access' => authlevel_name( $level ) ) ); + break; + } + else print json_encode ( array( 'response' => 'invalid') ); + break; + case "/remove_apikey": + // If the current authorization has write access, + // remove the given API key. + if ( ! can_write() ) + simple_authfail(); + + if ( array_key_exists('api_key', $_GET ) ) + { + $key = sql_clean( $_GET['api_key'] ); + // Perform a key-verification, skipping host/remote-address check. + if ( ! verify_apikey( $key, true ) ) + { + print json_encode( array ( 'response' => 'failed', 'cause' => 'nonexistant')); + break; + } + if ( ! remove_apikey( $key ) ) + { + print json_encode( array( 'response' => 'failed', 'cause' => 'error', 'detail' => 'Database error.')); + break; + } + print json_encode( array( 'response' => 'ok', 'key' => $key ) ); + break; + } + else print json_encode ( array( 'response' => 'invalid') ); + break; + case "/list_apikeys": + // List valid API keys. + // Fail is current authorization does not have write access. + if ( ! can_write() ) + simple_authfail(); + $list = list_apikeys(); + print json_encode( array( 'response' => 'ok', 'list' => $list ) ); + break; case "/authorize_user": // Add or update a valid back-end user in authorization // if the current authentication has write access. @@ -108,30 +175,31 @@ if ( !$config['sql_link'] ) // needed parameters should be username and access level // If the authorization does not exist, add it. // If the user is already authorized, replace access level. + case "/remove_user": + // If the current authentication has write access: + // Remove authorization for the given users. + // Delete user from backend if backend is read-write. + case "/list_users": + // List valid API user-acounts. + // Fail with notauthorized if current authentication + // does not have write access. + // Should not return users from backend, + // but should only return users with authorization. case "/add_user": // Add user to backend if backend is read-write and // the current authentication has write access. + // The created user should be added to authorizations + // with an access level of "limited_read (1)" case "/update_user": // Update the given user in the backend, if the backend // is read-write, and the current authentication has // write access. - case "/remove_user": - // Delete user from backend if backend is read-write - // and the current authentication has write access. - case "/list_apikeys": - // List valid API keys. - // Fail is current authorization does not have write access. - case "/new_apikey": - // If the current authorization has write access, create - // a new API key with requested access (ro/rw). - case "/remove_apikey": - // If the current authorization has write access, - // remove the given API key. print json_encode ( array( 'response' => 'notimplemented') ); break; default: print json_encode ( array( 'response' => 'invalid') ); } +} //************************************************************************************* mysql_close( $config['sql_link'] ); ?> diff --git a/api/config.php.sample b/api/config.php.sample index 477f4fd..c09eb67 100644 --- a/api/config.php.sample +++ b/api/config.php.sample @@ -15,6 +15,8 @@ function get_config() 'provision_users_table' => 'users', 'provision_phones_table' => 'phones', 'provision_servers_table' => 'servers', + 'apikeys_table' => 'apikeys', + 'authorizations_table' => 'authorizations', 'sessionkeys_table' => 'sessionkeys', 'sessionkey_lifetime' => 5, // Minutes 'numbers_table' => 'number_pool', diff --git a/api/lib/auth_base.php b/api/lib/auth_base.php index 5f51771..97e557f 100644 --- a/api/lib/auth_base.php +++ b/api/lib/auth_base.php @@ -3,6 +3,35 @@ require_once('config.php'); $config = get_config(); +function authlevel_value( $level ) +{ + switch ( $level ) + { + case 'limited_read': + return 1; + case 'full_read': + return 2; + case 'read_write': + return 3; + default: + return 0; + } +} +function authlevel_name( $level ) +{ + switch ( $level ) + { + case 1: + return 'limited_read'; + case 2: + return 'full_read'; + case 3: + return 'read_write'; + default: + return 'no_access'; + } +} + /******************************* * Load authentication plugin .. *******************************/ @@ -15,12 +44,6 @@ else { print json_encode( array( 'response' => 'error', 'cause' => 'config-error' ) ); exit; } /*******************************/ -function apikey_verify( $key ) -{ - if ( $key == "6327c08b70f9" ) return 1; - return false; -} - function new_key( $hex = false ) { // Basically this is at the moment a slightly modified @@ -32,14 +55,14 @@ function new_key( $hex = false ) while ( strlen( $string ) < $length ) { if ( $hex ) - $string .= substr(md5(rand().rand()), 0, $length); + $string .= substr(md5(rand().rand()), 0, $length+1); else { - $string .= crypt( substr(sha1(rand()), 0, $length) ); + $string .= crypt( substr(sha1(rand()), 0, $length+1) ); $string = preg_replace( '/\W/', '', $string); } } - return substr( $string, 0, $length ); + return substr( $string, 1, $length ); } function simple_authfail() @@ -214,24 +237,146 @@ function remove_session ($name, $id = null ) $_SESSION=array(); session_destroy(); - if ( $current_session != $name ) + if ( $current_session && $current_session != $name ) { session_id($current_sessid); session_start(); } } +function add_apikey ( $host, $level ) +{ + global $config; + if ( !is_numeric($level) ) return false; + + $key = new_key(); + + // Try to add the new key to authorizations first. If this + // fails, there will be the least amount of data to clean up ... + if ( ! update_authorization( $key, $level ) ) return false; + + $query = sprintf("INSERT INTO %s ( host, apikey ) VALUES ( '%s', '%s' )", + $config['apikeys_table'], + sql_clean($host), + sql_clean($key)); + + if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false; + return $key; +} + +function remove_apikey( $key ) +{ + global $config; + if ( ! verify_apikey( $key, true ) ) return false; + if ( ! remove_authorization( $key ) ) return false; + + $query = sprintf("DELETE FROM %s WHERE apikey = '%s'", + $config['apikeys_table'], + sql_clean($key) ); + if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false; + + return true; +} + +function verify_apikey( $key, $skip_hostcheck = false ) +{ + global $config; + + $query = sprintf("SELECT host FROM %s WHERE apikey = '%s'", + $config['apikeys_table'], + sql_clean($key) ); + $row = sql_dbquery_single( $config['provision_db'], $query ); + if (!$row) return false; + $host = $row['host']; + + if ( $host && ( $skip_hostcheck ) ) + return true; + + if ( $host == $_SERVER['REMOTE_ADDR'] ) return true; + return false; + +} + +function list_apikeys () +{ + global $config; + $query = sprintf("SELECT k.apikey AS apikey, k.host AS host, + a.access_level AS access_level + FROM %s k INNER JOIN %s a ON k.apikey = a.authid", + $config['apikeys_table'], + $config['authorizations_table']); + $list = array(); + $result = sql_dbquery( $config['provision_db'], $query); + if ( ! $result ) return $list; + while ( $row = @mysql_fetch_assoc( $result ) ) + { + array_push( $list, array( + 'api_key' => $row['apikey'], + 'host' => $row['host'], + 'level' => authlevel_name( $row['access_level'] ) + )); + } + return $list; + +} +function update_authorization( $authid, $level ) +{ + global $config; + if ( !is_numeric($level) ) return false; + $query = sprintf("INSERT INTO %s ( authid, access_level ) VALUES ( '%s', %d ) + ON DUPLICATE KEY UPDATE access_level=%d", + $config['authorizations_table'], + sql_clean($authid), + $level, $level); + if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false; + return true; +} -function get_authorization() +function remove_authorization( $authid ) { - return 1; + global $config; + $query = sprintf("DELETE FROM %s WHERE authid = '%s'", + $config['authorizations_table'], + sql_clean($authid) ); + //print $query . "\n\n"; + if ( ! sql_dbexec( $config['provision_db'], $query ) ) return false; + return true; } + + +function get_authorization( $type, $authid ) +{ + global $config; + + // If API-key is used, but key fails verification, write is impossible. + if ( ( $type == "key" ) && ( ! verify_apikey( $authid ) ) ) + return false; + + // If User-login is used, but backend is unable to provide info, fail. + if ( ( $type == "user" ) && ( ! authuser_getinfo( $authid ) ) ) + return false; + + // The only types of access control supported are "user" or "key". + if ( ($type != "user" ) && ($type != "key") ) + return false; + + $query = sprintf("SELECT access_level FROM %s WHERE authid = '%s'", + $config['authorizations_table'], + sql_clean($authid) ); + $row = sql_dbquery_single( $config['provision_db'], $query ); + if (!$row) return false; + $level = $row['access_level']; + return $level; +} + function can_write ( ) { // Stub, to be called on any API nodes that write data in the DB. $authid = $_SESSION['authid']; $type = $_SESSION['type']; + $level = get_authorization( $type, $authid ); - return true; + if ( $level >= authlevel_value('read_write') ) return $level; + else return false; } ?> diff --git a/doc/sql-data/structure.sql b/doc/sql-data/structure.sql index e2ee817..d077eda 100644 --- a/doc/sql-data/structure.sql +++ b/doc/sql-data/structure.sql @@ -1023,6 +1023,30 @@ CREATE DATABASE /*!32312 IF NOT EXISTS*/ `provision` /*!40100 DEFAULT CHARACTER USE `provision`; +-- +-- Table structure for table `apikeys` +-- + +CREATE TABLE IF NOT EXISTS `apikeys` ( + `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `host` varchar(64) NOT NULL, + `apikey` varchar(128) NOT NULL, + PRIMARY KEY (`apikey`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `authorizations` +-- + +CREATE TABLE IF NOT EXISTS `authorizations` ( + `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, + `authid` varchar(255) NOT NULL, + `access_level` int(11) NOT NULL, + PRIMARY KEY (`authid`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + -- -- Table structure for table `number_pool` --