
Implementasi Server
Buatlah migration, seeder, middleware dan route untuk menjalankan autentikasi hmac. Misalnya, Anda bisa membuat seperti ini.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->uuid('client_id')->unique();
$table->string('client_name');
$table->string('client_key')->unique();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('clients');
}
};
<?php
namespace Database\Seeders;
use App\Models\Client;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class ClientSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$clientId = Str::uuid();
$clientName = 'ZULFAME';
$clientKey = bin2hex(random_bytes(32));
Client::create([
'client_id' => $clientId,
'client_name' => $clientName,
'client_key' => $clientKey,
]);
echo "Client ID: $clientId\n";
echo "Client Key: $clientKey\n";
}
}
<?php
namespace App\Http\Middleware;
use App\Models\Client;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class HmacMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
try {
$clientId = $request->header('X-ZULFAME-ID');
$clientKey = $request->header('X-ZULFAME-KEY');
$requestSignature = $request->header('X-ZULFAME-SIGNATURE');
$timestamp = $request->header('X-TIMESTAMP');
if (!$clientId || !$clientKey || !$requestSignature || !$timestamp) {
Log::error('Unauthorized: Missing credentials');
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Missing credentials'
], 401);
}
// Validasi client
$client = Client::where('client_id', $clientId)->where('client_key', $clientKey)->first();
if (!$client) {
Log::error('Unauthorized: Invalid client credentials');
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Invalid client credentials'
], 401);
}
// Validasi format timestamp & konversi ke UNIX timestamp
$timestampUnix = strtotime($timestamp);
if (!$timestampUnix) {
Log::error('Unauthorized: Invalid timestamp format');
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Invalid timestamp format'
], 401);
}
// Cek apakah request sudah kedaluwarsa (5 menit)
if (abs(time() - $timestampUnix) > 300) {
Log::error('Unauthorized: Request expired');
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Request expired'
], 401);
}
// Cek apakah signature sudah pernah digunakan (untuk mencegah replay attack)
$cacheKey = "used_signature:$requestSignature";
if (Cache::has($cacheKey)) {
Log::error('Unauthorized: Duplicate request detected');
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Duplicate request'
], 401);
}
// Generate signature server-side
$serverSignature = hash_hmac('sha256', $clientKey, $timestamp);
if (!hash_equals($serverSignature, $requestSignature)) {
Log::error('Unauthorized: Invalid signature', [
'expected' => $serverSignature,
'received' => $requestSignature,
]);
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Invalid signature'
], 401);
}
// Simpan signature di cache agar tidak bisa digunakan ulang dalam 5 menit
Cache::put($cacheKey, true, now()->addMinutes(5));
Log::info('HMAC Authentication Passed');
return $next($request);
} catch (\Throwable $e) {
Log::error('HMAC Middleware Error:', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'status' => 'error',
'message' => 'Internal Server Error'
], 500);
}
}
}
<?php
use App\Models\Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::prefix('v1')->group(function () {
Route::middleware('auth.hmac')->group(function () {
Route::get('/status', function (Request $request) {
$client = Client::where('client_id', $request->header('X-ZULFAME-ID'))->first();
$data = [
'name' => $client->client_name,
'time' => $request->header('X-TIMESTAMP'),
];
return response()->json([
'status' => 'success',
'message' => 'Authenticated successfully',
'data' => $data,
]);
});
});
});
Implementasi Client
Lakukan konfigurasi klien pada .env kemudian buatlah controller dan route untuk mencoba mengirim permintaan autentikasi hmac. Misalnya, Anda bisa membuat seperti ini.
ZULFAME_CLIENT_ID=dc355ed2-93fd-4a24-be63-39bd2d350fc7
ZULFAME_CLIENT_KEY=f1d47a895dcb16afb80a6255fe89f41465d217db11993bffb42f6117501535ca
<?php
namespace App\Http\Controllers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
class TestController extends Controller
{
public function index()
{
$clientId = env('ZULFAME_CLIENT_ID');
$clientKey = env('ZULFAME_CLIENT_KEY');
$endpoint = "https://api-hmac.test";
$urlPath = "/api/v1/status";
$timestamp = date("Y-m-d H:i:s");
$signature = hash_hmac('sha256', $clientKey, $timestamp);
$client = new Client([
'base_uri' => $endpoint,
'timeout' => 10,
'verify' => false,
]);
try {
$response = $client->request('GET', $urlPath, [
'headers' => [
'X-ZULFAME-ID' => $clientId,
'X-ZULFAME-KEY' => $clientKey,
'X-ZULFAME-SIGNATURE' => $signature,
'X-TIMESTAMP' => $timestamp,
],
]);
$body = json_decode($response->getBody(), true);
Log::info('API Response:', $body);
return response()->json($body);
} catch (RequestException $e) {
Log::error('API Request Failed:', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'status' => 'error',
'message' => $e->getMessage(),
], 500);
}
}
}
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/status', [\App\Http\Controllers\TestController::class, 'index']);
Menerapkan Perizinan
Setelah proses autentikasi hmac diverifikasi, Anda bisa memberikan sebuah perizinan (optional) untuk menambahkan keamanan. Misalnya, Anda bisa membuat seperti ini.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('permissions', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
Schema::create('client_permissions', function (Blueprint $table) {
$table->foreignId('permission_id')->references('id')->on('permissions')->onDelete('cascade');
$table->foreignId('client_id')->references('id')->on('clients')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('permissions');
Schema::dropIfExists('client_permissions');
}
};
<?php
namespace Database\Seeders;
use App\Models\Permission;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class PermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$permissions = [
['name' => 'create-status'],
['name' => 'read-status'],
['name' => 'update-status'],
['name' => 'delete-status']
];
foreach ($permissions as $permission) {
Permission::create([
'name' => $permission['name'],
'created_at' => now(),
'updated_at' => now(),
]);
}
DB::table('client_permissions')->insert([
['client_id' => 1, 'permission_id' => 2, 'created_at' => now(), 'updated_at' => now(),],
]);
}
}
<?php
namespace App\Http\Middleware;
use App\Models\Client;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class PermissionMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
try {
$clientId = $request->header('X-ZULFAME-ID');
$clientKey = $request->header('X-ZULFAME-KEY');
// Validasi client
$client = Client::where('client_id', $clientId)->where('client_key', $clientKey)->first();
if (!$client) {
Log::error('Unauthorized: Invalid client credentials');
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Invalid client credentials'
], 401);
}
// Validasi permission
$permissions = DB::table('client_permissions')
->join('permissions', 'client_permissions.permission_id', '=', 'permissions.id')
->where('client_permissions.client_id', $client->id)
->pluck('permissions.name')
->toArray();
Log::info('Client Permissions:', ['client_id' => $client->id, 'permissions' => $permissions]);
$requiredPermission = collect($request->route()->middleware())
->first(fn($middleware) => str_starts_with($middleware, 'permission:'));
if ($requiredPermission) {
$requiredPermission = str_replace('permission:', '', $requiredPermission);
}
Log::info('Required Permission:', ['required' => $requiredPermission]);
if (!$requiredPermission || !in_array($requiredPermission, $permissions)) {
Log::error('Unauthorized: Insufficient permission', [
'client_id' => $client->id,
'required' => $requiredPermission,
'owned' => $permissions
]);
return response()->json([
'status' => 'error',
'message' => 'Unauthorized: Insufficient permission'
], 403);
}
Log::info('HMAC Permission Passed');
return $next($request);
} catch (\Throwable $e) {
Log::error('HMAC Permission Error:', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'status' => 'error',
'message' => 'Internal Server Error'
], 500);
}
}
}
Route::get('/status', function (Request $request) {
$client = Client::where('client_id', $request->header('X-ZULFAME-ID'))->first();
$data = [
'name' => $client->client_name,
'time' => $request->header('X-TIMESTAMP'),
];
return response()->json([
'status' => 'success',
'message' => 'Authenticated successfully',
'data' => $data,
]);
})->middleware('permission:read-status');