While working on a set of API webhooks recently, I needed to provide some security scheme that allowed the client to verify that the webhook request was being sent by the correct server, which I was working on. After a little online research, I came across the HMAC scheme for API requests. After a little explanation, I will give an example of implementing this for API endpoints in Laravel.
Hash-based Message Authentication Code (HMAC) is a code obtained using a cryptographic hash function, data and a secret. It’s used to verify the authenticity of the source and content of a request. Both the client and server share a pre-known secret key for generating HMACs.
In simpler terms, there is a secret key that is shared by and known to only the server and client. A hash function uses this key to generate a code whenever a request is made. This code is then added to the request and sent. When the receiving app gets the request, it runs the same process used to create the code, using the details from the request it just received and the secret key it has stored. If the sender is really who they’re meant to be, both the code in the request and the code that has just been generated should match, and the receiver can continue processing the request. Pretty clean, right?
It’s a simple yet effective authentication method, as the integrity checks do not allow for man-in-the-middle attacks unless they possess the secret key. However, a bad guy could resend the same request over and over, and that could cause damage to data or reveal some sensitive information. These are called Replay Attacks, and there are a number of ways to deal with those, but I won’t be talking about any as I plan to keep this as simple as possible.
Now for a quick explanation of our hash function. The values we would be using are:
- URL: The full URL in lowercase, including any query parameters.
- Verb: The HTTP method for the request in uppercase, e.g. POST.
- Content MD5: An MD5 hash of the request body in JSON
The steps we would take to create the hash code are:
- Create a signed string using our values above.
- Generate a hash using the string and our secret key.
- Base64 encode the hash and attach it to the request header
Other values, such as the request content type, can be used, but I’ll keep it simple. Each API’s scheme for creating the signed string used for hashing is usually described in the API’s documentation.
Setting Up
To start, spin up a new Laravel application and add a couple of controllers to add our API logic.
php artisan make:controller UsersController
php artisan make:controller ClientController
Add a few routes for the endpoints in routes/api.php
Route::prefix('/')->middleware('auth.hmac')->group(static function () {
Route::get('/users', [App\Http\Controllers\UsersController::class, 'getAll']);
Route::get('/users/{id}', [App\Http\Controllers\UsersController::class, 'getOne']);
Route::post('/users', [App\Http\Controllers\UsersController::class, 'create']);
Route::put('/users/{id}', [App\Http\Controllers\UsersController::class, 'update']);
});
I’ve assigned a middleware auth.hmac
to the routes, and I'll return to this later. Next, save the example public and secret keys in the .env
file and reference them in a config file config/hmac.php
. The public key here is the intended request header key for the HMAC.
# .env
HMAC_PUBLIC_KEY="X-MEN-SIGNATURE"
HMAC_SECRET_KEY="Xavier's School for Gifted Youngsters"
<!-- hmac.php -->
<?php
return [
'public' => 'X-MEN-SIGNATURE',
'secret' => "Xavier's School for Gifted Youngsters"
];
Next, a few methods in the controllers to create, get and edit users for the simple context of this tutorial. The User model comes with the Laravel install; we don’t need to change anything.
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class UsersController extends Controller
{
public function getAll(Request $request)
{
$users = User::all();
return response()->json($users);
}
public function getOne(Request $request, $id)
{
$user = User::findOrFail($id);
return response()->json($user);
}
public function create(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]);
if ($validator->fails()) {
return response()->json($validator->errors()->first(), 400);
}
extract($request->all());
$password = Hash::make($password);
$user = User::create(compact('name', 'email', 'password'));
return response()->json($user);
}
public function update(Request $request, $id)
{
$user = User::findOrFail($id);
$data = [
'name' => $request->name ?? $user->name,
'email' => $request->email ?? $user->email,
'password' => Hash::make($request->name) ?? $user->password
];
$user->update($data);
return response()->json($user);
}
}
Hash It Up
Now, we have working controller logic for a simple API. However, making a call to it would result in an error due to the missing middleware. Let’s come back to that. This middleware is going to be the location for the HMAC authentication. Before the controller handles the request, we will confirm that it contains the correct HMAC header.
Add a new middleware:
php artisan make:middleware HmacAuth
And register it by adding it to $routeMiddleware
in App\Http\Kernel.php
protected $routeMiddleware = [
...
'auth.hmac' => \App\Http\Middleware\HmacAuth::class,
...
];
Now, for the real work. Remember the scheme used for creating the signed string. Let’s go ahead and implement it. First, we check if the header we need is even on the request and abort it if it isn’t.
$header = config('hmac.public');
$request_hash = $request->headers->get($header);
if (!$request_hash) {
$message = 'Header `' . $header . '` missing.';
abort('403', $message);
}
If it is, we get our string, which is the concatenation using newlines, of the HTTP method, URL and the MD5 hash of the request body in JSON. Using the secret key, we then run the HMAC SHA-256 hashing algorithm on the string. This value is finally Base64 encoded using UTF8 and compared with the value in our HMAC header. If they match, we can keep processing the request, or else, we abort.
Here’s what the final code should look like:
class HmacAuth
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$header = config('hmac.public');
$request_hash = $request->headers->get($header);
if (!$request_hash) {
$message = 'Header `' . $header . '` missing.';
abort('403', $message);
}
$body = $request->all();
$url = config('hmac.webhook');
$verb = $request->method();
$md5 = md5(json_encode($body));
$string = $verb . PHP_EOL . $url . PHP_EOL . $md5;
$hash = hash_hmac('SHA256', $string, config('hmac.secret'));
$base64_hash = base64_encode($hash);
if ($base64_hash !== $request_hash) {
$message = 'Invalid `' . $header . '` Header';
abort('403', $message);
}
return $next($request);
}
}
And that’s all. HMAC signature verification is one of the simplest and most powerful methods for API requests and webhook authentication.
Top comments (0)