1

I'm developing an API to let my users access to files stored on another server.

Let's call my two servers, server 1 and server 2!

server 1 is the server im hosting my web site, and

server 2 is the server im storing my files!

My site is basically Javascript based one, so I will be using Javascript to post data to API when user needs to access files which are stored on server 2.

when users requests to access files, the data will be posted to API URL via Javascript! API is made of PHP. Using that PHP script(API) on server 1, I will made another request to server 2 asking for files so there will be another PHP script(API) on server 2.

I need to know how should I do this authentication between two servers as server 2 has no access to user details on server 1?

I hope to do that like this, I can use the method which is used by most payment gateways.

When API on server 2 received a request with some unique data of the user , post back those unique data through SSL to server 1 API and match them with user data in the database, then post back result through SSL to server 2 so then server 2 knows file request is a genuine request.

In this case what kind of user data/credentials server 1 API should post to server 2 and server 2 API should post back to server 1? and which user data should be matched with the data in the database? like user ID, session, cookies, ip, time stamp, ect!

Any clear and described answer would be nice! Thanks.

Naveen Gamage
  • 1,844
  • 12
  • 32
  • 51
  • OAuth2 might be a solution –  Jun 24 '13 at 05:12
  • You can configure server 2 so that ONLY server 1 can have access to files on it. Now, server 1 should take care of authenticating users. i.e. log in of user and decide which files he can access. – Satish Gadhave Jun 24 '13 at 08:17
  • 2
    I would vote to close this if it didn't have an open bounty. I can think of at least 12 protocols which would be sensible to use for transferring the files and several methods of handling authentication, authorization and session management. You've made no mention of any constraints on the implementation nor the security model. What have payment gateways got to do with file transfer? – symcbean Jun 25 '13 at 00:04
  • @symcbean I agree that the question is too broad. I do think the gateway comment is fine though. – eglasius Jun 27 '13 at 00:20

6 Answers6

3

I would go with this:

  1. user initiates action, javascript asks Server 1 (ajax) for request for file on Server 2
  2. Server 1 creates URL using hash_hmac with data: file, user ID, user secret
  3. when clicking that URL (server2.com/?file=FILE&user_id=ID&hash=SHA_1_HASH) server 2 asks server 1 for validation (sends file, user_id and hash)
  4. server 1 does the validation, sends response to server 2
  5. server 2 pushes file or sends 403 HTTP response

This way, server 2 only needs to consume API of server 1, server 1 has all the logic.

Pseudocode for hash and url creation:

// getHash($userId, $file) method
$user = getUser($userId);
$hash = hash_hmac('sha1', $userId . $file, $user->getSecret());

// getUrl($userId, $file) method
return sprintf('http://server2.com/get-file?file=%1&user_id=%2&hash=%3', 
    $userId, 
    $file,
    $security->getHash($userId, $file)
);

Pseudocode for validation:

$hash = $security->getHash($_GET['id'], $_GET['file']);
if ($hash === $_GET['hash']) {
    // All is good
}

Edit: getHash() method accepts user ID and file (ID or string, what ever suits your needs). With that data, it produces a hash, using hash_hmac method. For the secret parameter of hash_hmac function, users "secret key" is used. That key would be stored together with users data in the db table. It would be generated with mt_rand or even something stronger as reading /dev/random or using something like https://stackoverflow.com/a/16478556/691850.

A word of advice, use mod_xsendfile on server 2 (if it is Apache) to push files.

Community
  • 1
  • 1
Anorgan
  • 303
  • 2
  • 10
  • Although I agree with you solution, your example leaves out all the important stuff. I would be able to fill in the blanks, but I think that for this answer to become the accept answer with bounty, you should explain what `$security->getHash()` would do. Because that is what is asked. The 5 step plan is just a better way to explain what Naveen tried to do in his paragraph about `When API on server 2 received...` – Hugo Delsing Jun 25 '13 at 07:03
  • 1
    +1 for HMAC. You just need a single key for both servers. HMAC using that key and include the user_id, file_id, and timestamp. That way you can disable the URL after a certain time, and server 1 doesn't need to ask server 2 for validation. – Steve Clay Jun 29 '13 at 16:57
  • 1
    Also you want http_build_query() for the URL building. – Steve Clay Jun 29 '13 at 16:59
1

Introduction

You can use 2 simple method

  • Authentication Token
  • Signed Request

You can also combine both of them by using Token for authentication and using signature to verify integrity of the message sent

Authentication Token

If you are going to consider matching any identification in the database perhaps you can consider creating authentication token rather than user ID, session, cookies, ip, time stamp, etc! as suggested.

Create a random token and save to Database

$token = bin2hex(mcrypt_create_iv(64, MCRYPT_DEV_URANDOM));
  • This can be easily generated
  • You can guaranteed it more difficult to guess unlike password
  • It can easily be deleted if compromised and re generate another key

Signed Request

The concept is simple, For each file uploaded must meat a specific signature crated using a random generated key just like the token for each specific user

This can easily be implemented with HMAC with hash_hmac_file function

Combine Both Authentication & Signed Request

Here is a simple Prof of concept

Server 1

/**
 * This should be stored securly
 * Only known to User
 * Unique to each User
 * Eg : mcrypt_create_iv(32, MCRYPT_DEV_URANDOM);
 */
$key = "d767d183315656d90cce5c8a316c596c971246fbc48d70f06f94177f6b5d7174";
$token = "3380cb5229d4737ebe8e92c1c2a90542e46ce288901da80fe8d8c456bace2a9e";

$url = "http://server 2/run.php";




// Start File Upload Manager
$request = new FileManager($key, $token);

// Send Multiple Files
$responce = $request->send($url, [
        "file1" => __DIR__ . "/a.png",
        "file2" => __DIR__ . "/b.css"
]);


// Decode Responce
$json = json_decode($responce->data, true);

// Output Information
foreach($json as $file) {
    printf("%s - %s \n", $file['name'], $file['msg']);
}

Output

temp\14-a.png - OK 
temp\14-b.css - OK 

Server 2

// Where to store the files
$tmpDir = __DIR__ . "/temp";

try {
    $file = new FileManager($key, $token);
    echo json_encode($file->recive($tmpDir), 128);
} catch (Exception $e) {

    echo json_encode([
            [
                    "name" => "Execption",
                    "msg" => $e->getMessage(),
                    "status" => 0
            ]
    ], 128);
}

Class Used

class FileManager {
    private $key;

    function __construct($key, $token) {
        $this->key = $key;
        $this->token = $token;
    }

    function send($url, $files) {
        $post = [];

        // Convert to array fromat
        $files = is_array($files) ? $files : [
                $files
        ];

        // Build Post Request
        foreach($files as $name => $file) {
            $file = realpath($file);
            if (! (is_file($file) || is_readable($file))) {
                throw new InvalidArgumentException("Invalid File");
            }

            // Add File
            $post[$name] = "@" . $file;

            // Sign File
            $post[$name . "-sign"] = $this->sign($file);
        }

        // Start Curl ;
        $ch = curl_init($url);
        $options = [
                CURLOPT_HTTPHEADER => [
                        "X-TOKEN:" . $this->token
                ],
                CURLOPT_RETURNTRANSFER => 1,
                CURLOPT_POST => count($post),
                CURLOPT_POSTFIELDS => $post
        ];

        curl_setopt_array($ch, $options);

        // Get Responce
        $responce = [
                "data" => curl_exec($ch),
                "error" => curl_error($ch),
                "error" => curl_errno($ch),
                "info" => curl_getinfo($ch)
        ];
        curl_close($ch);
        return (object) $responce;
    }

    function recive($dir) {
        if (! isset($_SERVER['HTTP_X_TOKEN'])) {
            throw new ErrorException("Missing Security Token");
        }

        if ($_SERVER['HTTP_X_TOKEN'] !== $this->token) {
            throw new ErrorException("Invalid Security Token");
        }

        if (! isset($_FILES)) {
            throw new ErrorException("File was not uploaded");
        }

        $responce = [];
        foreach($_FILES as $name => $file) {
            $responce[$name]['status'] = 0;
            // check if file is uploaded
            if ($file['error'] == UPLOAD_ERR_OK) {
                // Check for signatire
                if (isset($_POST[$name . '-sign']) && $_POST[$name . '-sign'] === $this->sign($file['tmp_name'])) {

                    $path = $dir . DIRECTORY_SEPARATOR . $file['name'];
                    $x = 0;
                    while(file_exists($path)) {
                        $x ++;
                        $path = $dir . DIRECTORY_SEPARATOR . $x . "-" . $file['name'];
                    }

                    // Move File to temp folder
                    move_uploaded_file($file['tmp_name'], $path);

                    $responce[$name]['name'] = $path;
                    $responce[$name]['sign'] = $_POST[$name . '-sign'];
                    $responce[$name]['status'] = 1;
                    $responce[$name]['msg'] = "OK";
                } else {
                    $responce[$name]['msg'] = sprintf("Invalid File Signature");
                }
            } else {
                $responce[$name]['msg'] = sprintf("Upload Error : %s" . $file['error']);
            }
        }

        return $responce;
    }

    private function sign($file) {
        return hash_hmac_file("sha256", $file, $this->key);
    }
}

Other things to consider

For better security you can consider the follow

  • IP Lock down
  • File Size Limit
  • File Type Validation
  • Public-Key Cryptography
  • Changing Date Based token generation

Conclusion

The sample class can be extended in so many ways and rather than use URL you can consider a proper json RCP solution

Baba
  • 94,024
  • 28
  • 166
  • 217
0

A long enough, single-use, short-lived, random generated key should suffice in this case.

  • Client requests for a file to Server 1
  • Server 1 confirms login information and generates a long single-use key and sends it to the user. Server 1 keeps track of this key and matches it with an actual file on Server 2.
  • Client sends a request to Server 2 along with the key
  • Server 2 contacts Server 1 and submits the key
  • Server 1 returns a file path if the key is valid. The key is invalidated (destroyed).
  • Server 2 sends the file to the client
  • Server 1 invalidates the key after say 30 seconds, even if it didn't receive a confirmation request from Server 2. Your front-end should account for this case and retry the process a couple of times before returning an error.

I do not think there is a point in sending cookie/session information along, this information can be brute-forced just like the random key.

A 1024-bit long key sounds more than reasonable. This entropy can be obtained with a string of less than 200 alphanumeric characters.

RandomSeed
  • 29,301
  • 6
  • 52
  • 87
0

For the absolute best security you would need some communication from server 2 to server 1, to double check if the request is valid. Although this communication could be minimal, its still communication and thus slows down the proces. If you could live with a marginally less secure solution, I would suggest the following.

Server 1 requestfile.php:

<?php
//check login
if (!$loggedon) {
  die('You need to be logged on');
}

$dataKey = array();
$uniqueKey = 'fgsdjk%^347JH$#^%&5ghjksc'; //choose whatever you want.

//check file
$file = isset($_GET['file']) ? $_GET['file'] : '';
if (empty($file)) {
  die('Invalid request');       
}

//add user data to create a reasonably unique fingerprint.
//It will mostlikely be the same for people in the same office with the same browser, thats mainly where the security drop comes from.
//I double check if all variables are set just to be sure. Most of these will never be missing.
if (isset($_SERVER['HTTP_USER_AGENT'])) {
  $dataKey[] = $_SERVER['HTTP_USER_AGENT'];
}
if (isset($_SERVER['REMOTE_ADDR'])) {
  $dataKey[] = $_SERVER['REMOTE_ADDR'];
}
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  $dataKey[] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}
if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
  $dataKey[] = $_SERVER['HTTP_ACCEPT_ENCODING'];
}
if (isset($_SERVER['HTTP_ACCEPT'])) {
  $dataKey[] = $_SERVER['HTTP_ACCEPT'];
}

//also add the unique key
$dataKey[] = $uniqueKey;

//add the file
$dataKey[] = $file;

//add a timestamp. Since the request will be a different times, dont use the exact second
//make sure its added last
$dataKey[] = date('YmdHi');

//create a hash
$hash = md5(implode('-', $dataKey));

//send to server 2
header('Location: https://server2.com/download.php?file='.urlencode($file).'&key='.$hash);
?>

On server 2 you will do almost the same.

<?php
$valid = false;
$dataKey = array();
$uniqueKey = 'fgsdjk%^347JH$#^%&5ghjksc'; //same as on server one

//check file
$file = isset($_GET['file']) ? $_GET['file'] : '';
if (empty($file)) {
  die('Invalid request');       
}
//check key
$key = isset($_GET['key']) ? $_GET['key'] : '';
if (empty($key)) {
  die('Invalid request');       
}

//add user data to create a reasonably unique fingerprint.
if (isset($_SERVER['HTTP_USER_AGENT'])) {
  $dataKey[] = $_SERVER['HTTP_USER_AGENT'];
}
if (isset($_SERVER['REMOTE_ADDR'])) {
  $dataKey[] = $_SERVER['REMOTE_ADDR'];
}
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  $dataKey[] = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
}
if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
  $dataKey[] = $_SERVER['HTTP_ACCEPT_ENCODING'];
}
if (isset($_SERVER['HTTP_ACCEPT'])) {
  $dataKey[] = $_SERVER['HTTP_ACCEPT'];
}

//also add the unique key
$dataKey[] = $uniqueKey;

//add the file
$dataKey[] = $file;

//add a timestamp. Since the request will be a different times, dont use the exact second
//keep the request time in a variable
$time = time();
$dataKey[] = date('YmdHi', $time);

//create a hash
$hash = md5(implode('-', $dataKey));


if ($hash == $key) {
  $valid = true;
} else {
  //perhaps the request to server one was made at 2013-06-26 14:59 and the request to server 2 come in at 2013-06-26 15:00
  //It would still fail when the request to server 1 and 2 are more then one minute apart, but I think thats an acceptable margin. You could always adjust for more margin though.

  //drop the current time
  $requesttime = array_pop($dataKey);
  //go back one minute
  $time -= 60;
  //add the time again
  $dataKey[] = date('YmdHi', $time);

  //create a hash
  $hash = md5(implode('-', $dataKey));

  if ($hash == $key) {
    $valid = true;
  }
}

if ($valid!==true) {
  die('Invalid request');
}

//all is ok. Put the code to download the file here
?>
Hugo Delsing
  • 13,803
  • 5
  • 45
  • 72
0

You can restrict access to server2. Only server1 will be able to send request to server2. You can do this by whitelisting ip of server1 on server side or using .htaccess file. In php you can do by checking request generated ip and validate it with server1 ip.

Also you can write a algorithm which generates a unique number. Using that algorithm generate a number on server1 and send it to server2 in request. On server2 check if that number is generated by algorithm and if yes then request is valid.

0

I'd go with a simple symetric encryption, where server 1 encodes the date and the authenticated user using a key known only by server 1 and server 2, sending it to the client who cant read it, but can send it to server 2 as a sort of ticket to authenticate himself. The date is important to not let any client use the same "ticket" over the time. But at least one of the servers must know which user have access to which files, so unless you use dedicated folders or access groups you must keep the user and file infos together.