I read a lot about security mechanisms used in different APIs. But I'm still wondering if my own implementation is secure or not since it seems that there will ever be a risk of something you forgot ...
The scenario:
I have 2 PHP-running systems: Client and Server. There are multiple clients and one server but the communication is always between one client and the server. Both have their own user database (userID x may represent a user in both systems). The API is hosted on the server side so communication is always directed from client to server. Each client has a unique and numeric auto increment ID and a unique and secure key (UUID) which is only known by the client itself and the server.
When a logged in user on the client side uses the API the first time, he is asked to send his Name, Email and auto increment ID to the server. The Server stores the Data and generates a UUID for the user which is returned and passed by the user in further requests instead of its numerical auto increment ID.
The client calls the API script via https://www.server.de/api/api.php.
$host = 'https://www.server.de/api/api.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $host);
curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_POST, 1);
In the API request, the clientID, the user UUID (when available, some requests won't need user authentication), the $request
array as JSON string used in the API script, a generated nonce and a timestamp are send via post. The whole $post
array is hashed with the private $key
. The resulting hash is then added to the $post
array.
// example request array
$request = array('action' => 'getList', 'listID' => 3);
$nonce = bin2hex(openssl_random_pseudo_bytes(10));
$param = array(
'clientID' => $client_ID,
'userID' => $user_UUID ?: 'noauth',
'request' => json_encode($request),
'timestamp' => time(),
'nonce' => $nonce
);
$file = false;
$filepath = false;
if($request['file']){
$filepath = $request['file'];
$file = curl_file_create($filepath, 'image/jpg');
unset($request['file']);
$param['request'] = json_encode($request);
}
$param['hash'] = hash_hmac('sha256', json_encode($param), $key);
// NOTE: file and filehash are NOT in signature of the $_POST request
// filehash is the signature for the content of the file
if($file){
$param['file'] = $file;
$filecontent = file_get_contents($filepath);
$filehash = hash_hmac('sha256', $filecontent, $key);
$param['filehash'] = $filehash;
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $param );
$jsonstr = curl_exec($ch);
curl_close($ch);
$returndata = json_decode($jsonstr, true);
When receiving an API request, the server reads the ClientID. If a client is found for that ID, the $post
array is set up and hashed with the private key using hash_hmac
and the SHA-256 algorithm and compared with hash_equals
. If the hashes are equal, I assume that the API request is a valid request from a valid client and has not changed on its way (so data integrity is guaranteed). In addition I also look at the timestamp and the nonce. If the timestamp is older than 5 minutes or if the nonce was used before the server will reject the request.
In case of user authorization required, the server will also look at the user UUID and end with an error if no user was found or if the user has no rights to perform the request.
For files I get and hash the files content and compare it like I did with the $post
array. If hashes are equal I check if the file extension $ext = pathinfo($_FILES["file"]["name"])['extension'];
is allowed and store the file move_uploaded_file($_FILES["file"]["tmp_name"], 'dir/uploads/'.$_FILES["file"]["name"]);
Returning data from the API is printed as JSON string. echo json_encode($data);
What do you think? If anybody knows a better practice or sees any kind of security lack - I'm looking forward to learn from your answer.
The client calls the API script via https://www.server.de/api/api.php.
\$\endgroup\$