Isaac Lloyd
apisecurityphp

Homemade API Authentication Using PHP's Hash Functions

5 minutes

I’ve spent the past few weeks creating this blog from scratch down to the very last line of code. In doing this, I found the need to lazy load the comments for this blog after the user scrolls down to read them- similar to how youtube does it. Essentially, some javascript on the webpage will make a request back to my php script on the server to fetch and encode the json data for the comments.

Even though my site is small, I wanted to try implementing a little bit of security so I can get some practice in. I wanted to generate a single-use key that would have to be sent with the request to load comment data so people wouldn’t be able to abuse my API endpoint (not that there’s any real use for this data).

This is actually pretty straight forward, and PHP already provides almost all of the infrastructure to make this happen. Today we will be covering the php hash functions and then building a simple JSON endpoint using them.

So, what is a hash?

In simple terms, a hash is a function that generates a fixed length string from a set of input data. The string it returns is almost impossible to turn back into the input data. The input data will always generate the same hash so it cannot be changed. This is actually how passwords are stored, since it isn’t considered good practice to save passwords as raw text. The password can just be hashed every time you log in and then checked against the original hash created when you made your account. Tom Scott has an awesome video explaining this process:

Our implementation of hashing is actually going to be even simpler than this. All we need is two strings, a secret key and a public key. Our secret key is going to be a random string, and our public key is going to be the current timestamp. We will then combine them with the php hash() function to get our hashed string. It would be possible to do this without the timestamp, but using it will allow us to make our api key expire in the future. Here’s the code for what I’ve just explained:

function generateAPIKey() {
    $secretKey = "dQw4w9WgXcQ"; // this never changes
    $publicKey = time(); // seconds since epoch
    $hash = hash_hmac('sha256', $secretKey, $publicKey); // create hash
    return array("hash" => $hash, "publicKey" => $publicKey);
}

sha256 is our hashing algorithm. There are many options.

When we make a request to our api, we will pass the $hash and $publicKey variables as GET parameters so we can validate our hash against the public and private keys. Here’s what that looks like in our api.php file:

//hash-api.php?hash=54eef70ba4e86d68fbb8e052c9ecefab2d32f32d12fa1a7ae10fc41f981bab6d&publicKey=1681933372

$secretKey = "dQw4w9WgXcQ"; // same as before
$publicKey = $_GET["publicKey"];
$oldHash = $_GET["hash"];

$newHash = hash_hmac('sha256', $secretKey, $publicKey); // same as before
if (hash_equals($oldHash, $newHash)) { // test the hash we generated before against the new credentials
    echo "valid request!";
} else {
    echo "invalid request :(";
}

That pretty much covers the "security" side of our API endpoint. The other side is returning data to the browser in the correct format. We tell the browser what format we are using with a MIME Type header. The MIME Type for json is "application/json". We also will encode our data as json with the php json_encode() function.

header('Content-Type: application/json'); // set mime type
echo json_encode($dataToReturn, JSON_PRETTY_PRINT); // encode properly

Awesome. Now to tie everything together:

hash-generateAndRequest.php:

<?php
function generateAPIKey() {
    $secretKey = "dQw4w9WgXcQ"; // this never changes
    $publicKey = time(); // seconds since epoch
    $hash = hash_hmac('sha256', $secretKey, $publicKey); // create hash
    return array("hash" => $hash, "publicKey" => $publicKey);
}

$apiData = generateAPIKey();
?>
<!DOCTYPE html>
<html>
<head>
<script>
// make request (now in browser/javascript)
fetch('hash-api.php?hash=<?=$apiData["hash"]?>&publicKey=<?=$apiData["publicKey"]?>', {cache: 'no-store'})
  .then(response => response.json())
  .then(data => {
    const json = JSON.stringify(data);
    alert(json);
  })
  .catch(error => alert('Error:', error));
</script>
</head>
</html>

hash-api.php:

<?php
$secretKey = "dQw4w9WgXcQ";
$publicKey = $_GET["publicKey"];
$oldHash = $_GET["hash"];

$timeoutSeconds = 300; // set time before request expires

if ((time() - $timeoutSeconds) <= $publicKey) {
    $newHash = hash_hmac('sha256', $secretKey, $publicKey);
    if (!hash_equals($oldHash, $newHash)) { // if hash doesn’t equal
        http_response_code(498); // invalid/expired request response code
        echo "Bad Request";
        die(); // end execution
    }
} else {
    http_response_code(498);
    echo "Expired";
    die();
}

$dataToReturn = new stdClass();
$dataToReturn->title = "It worked!";
$dataToReturn->properties = array(
    "Color" => "blue",
    "State" => "UT",
    "Brand" => "Toyota"
);

header('Content-Type: application/json');
echo json_encode($dataToReturn, JSON_PRETTY_PRINT);
?>

That just about concludes it. Hopefully you've learned something, feel free to leave a comment on this post (using this same infrastructure!)

Further reading: