From 00681a8e30a0049cf90d0a4b771ad4f62ef1d27f Mon Sep 17 00:00:00 2001 From: Daeng Deni Mardaeni Date: Thu, 28 Aug 2025 13:44:58 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(auth):=20implementasi=20autent?= =?UTF-8?q?ikasi=20HMAC=20dan=20standardisasi=20format=20respons=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tambah validasi HMAC (X-Signature, X-Timestamp, X-Api-Key) pada setiap request. - Standarkan format respons sesuai ResponseCode; hapus `response_description` (gabung ke `response_message`). - `BalanceSummaryRequest`: validasi header + `validateHmac512`, pakai secret dari config, logging detail, bedakan invalid API key vs invalid signature. - `AccountBalanceController`: sederhanakan pesan error “Rekening tidak ditemukan”. - Konfigurasi baru: `webstatement.api_key`, `webstatement.secret_key`; pastikan helper `validateHmac512` tersedia. - Breaking: Bearer token tidak didukung; gunakan HMAC headers. - Validasi nomor rekening di database sebelum proses bisnis. - Logging terstruktur untuk setiap percobaan validasi HMAC (header & hasil verifikasi). - Konsistensi kode error via ResponseCode enum untuk semua kasus gagal. --- app/Enums/ResponseCode.php | 3 +- .../Api/AccountBalanceController.php | 2 +- app/Http/Requests/BalanceSummaryRequest.php | 168 +++++++++++++++--- 3 files changed, 148 insertions(+), 25 deletions(-) diff --git a/app/Enums/ResponseCode.php b/app/Enums/ResponseCode.php index 2531b43..98a148c 100644 --- a/app/Enums/ResponseCode.php +++ b/app/Enums/ResponseCode.php @@ -121,8 +121,7 @@ $response = [ 'status' => $this->value == '00' ? true : false, 'response_code' => $this->value, - 'response_message' => $message ?? $this->getMessage(), - 'response_description' => $this->getDescription(), + 'response_message' => $this->getMessage() . ($message ? ' | ' . $message : ''), ]; if (isset($data['errors'])) { diff --git a/app/Http/Controllers/Api/AccountBalanceController.php b/app/Http/Controllers/Api/AccountBalanceController.php index b622255..bd09b7c 100644 --- a/app/Http/Controllers/Api/AccountBalanceController.php +++ b/app/Http/Controllers/Api/AccountBalanceController.php @@ -53,7 +53,7 @@ class AccountBalanceController extends Controller return response()->json( ResponseCode::DATA_NOT_FOUND->toResponse( null, - 'Data saldo tidak ditemukan untuk nomor rekening: ' . $accountNumber + 'Rekening tidak ditemukan' ), ResponseCode::DATA_NOT_FOUND->getHttpStatus() ); diff --git a/app/Http/Requests/BalanceSummaryRequest.php b/app/Http/Requests/BalanceSummaryRequest.php index d1629a8..6835df2 100644 --- a/app/Http/Requests/BalanceSummaryRequest.php +++ b/app/Http/Requests/BalanceSummaryRequest.php @@ -4,21 +4,94 @@ namespace Modules\Webstatement\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Log; +use Illuminate\Http\Exceptions\HttpResponseException; +use Modules\Webstatement\Enums\ResponseCode; +use Modules\Webstatement\Models\AccountBalance; class BalanceSummaryRequest extends FormRequest { - /** + /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize(): bool { - // Sesuaikan dengan authorization logic jika diperlukan - return true; + try { + // Ambil parameter dari header + $signature = $this->header('X-Signature'); + $timestamp = $this->header('X-Timestamp'); + $apiKey = $this->header('X-Api-Key'); + + // Validasi keberadaan header yang diperlukan + if (!$signature || !$timestamp || !$apiKey) { + Log::warning('HMAC validation failed - missing required headers', [ + 'signature' => $signature, + 'timestamp' => $timestamp, + 'apiKey' => $apiKey, + 'ip' => $this->ip(), + 'user_agent' => $this->userAgent() + ]); + return false; + } + + // Validasi API key dari config + $expectedApiKey = config('webstatement.api_key'); + if ($apiKey !== $expectedApiKey) { + Log::warning('HMAC validation failed - invalid API key', [ + 'provided_api_key' => $apiKey, + 'expected_api_key' => $expectedApiKey, + 'ip' => $this->ip(), + 'user_agent' => $this->userAgent() + ]); + return false; + } + + // Ambil secret key dari config + $secretKey = config('webstatement.secret_key'); + + // Ambil parameter untuk validasi HMAC + $httpMethod = $this->method(); + $relativeUrl = $this->path(); + $requestBody = $this->getContent(); + + // Validasi HMAC signature + $isValid = validateHmac512( + $httpMethod, + $relativeUrl, + $apiKey, + $requestBody, + $timestamp, + $secretKey, + $signature + ); + + if (!$isValid) { + Log::warning('HMAC validation failed - invalid signature', [ + 'http_method' => $httpMethod, + 'relative_url' => $relativeUrl, + 'api_key' => $apiKey, + 'timestamp' => $timestamp, + 'ip' => $this->ip(), + 'user_agent' => $this->userAgent() + ]); + } + + return $isValid; + + } catch (\Exception $e) { + Log::error('HMAC validation error', [ + 'error' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'ip' => $this->ip(), + 'user_agent' => $this->userAgent() + ]); + return false; + } } - /** + /** * Get the validation rules that apply to the request. * * @return array @@ -30,7 +103,7 @@ class BalanceSummaryRequest extends FormRequest 'required', 'string', 'max:50', - 'regex:/^[A-Z0-9-]+$/i' // Hanya alphanumeric dan dash + 'regex:/^[A-Z0-9-]+$/i' // Hanya alphanumeric dan dash ], 'start_date' => [ 'required', @@ -49,7 +122,7 @@ class BalanceSummaryRequest extends FormRequest ]; } - /** + /** * Get custom messages for validator errors. * * @return array @@ -57,20 +130,20 @@ class BalanceSummaryRequest extends FormRequest public function messages(): array { return [ - 'account_number.required' => 'Nomor rekening wajib diisi.', - 'account_number.string' => 'Nomor rekening harus berupa teks.', - 'account_number.max' => 'Nomor rekening maksimal :max karakter.', - 'account_number.regex' => 'Nomor rekening hanya boleh mengandung huruf, angka, dan strip.', - 'start_date.required' => 'Tanggal awal wajib diisi.', - 'start_date.date_format' => 'Format tanggal awal harus YYYY-MM-DD.', + 'account_number.required' => 'Nomor rekening wajib diisi.', + 'account_number.string' => 'Nomor rekening harus berupa teks.', + 'account_number.max' => 'Nomor rekening maksimal :max karakter.', + 'account_number.regex' => 'Nomor rekening hanya boleh mengandung huruf, angka, dan strip.', + 'start_date.required' => 'Tanggal awal wajib diisi.', + 'start_date.date_format' => 'Format tanggal awal harus YYYY-MM-DD.', 'start_date.before_or_equal' => 'Tanggal awal harus sebelum atau sama dengan tanggal akhir.', - 'end_date.required' => 'Tanggal akhir wajib diisi.', - 'end_date.date_format' => 'Format tanggal akhir harus YYYY-MM-DD.', - 'end_date.after_or_equal' => 'Tanggal akhir harus sesudah atau sama dengan tanggal awal.', + 'end_date.required' => 'Tanggal akhir wajib diisi.', + 'end_date.date_format' => 'Format tanggal akhir harus YYYY-MM-DD.', + 'end_date.after_or_equal' => 'Tanggal akhir harus sesudah atau sama dengan tanggal awal.', ]; } - /** + /** * Handle a failed validation attempt. * * @param \Illuminate\Contracts\Validation\Validator $validator @@ -82,13 +155,50 @@ class BalanceSummaryRequest extends FormRequest { $errors = $validator->errors(); - throw new \Illuminate\Http\Exceptions\HttpResponseException( + throw new HttpResponseException( response()->json( - \Modules\Webstatement\Enums\ResponseCode::INVALID_FIELD->toResponse( + ResponseCode::INVALID_FIELD->toResponse( ['errors' => $errors->all()], - 'Field tertentu tidak sesuai aturan' + 'Validasi gagal' ), - \Modules\Webstatement\Enums\ResponseCode::INVALID_FIELD->getHttpStatus() + ResponseCode::INVALID_FIELD->getHttpStatus() + ) + ); + } + + /** + * Handle failed authorization. + * + * @return void + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + protected function failedAuthorization() + { + $xApiKey = $this->header('X-Api-Key'); + $expectedApiKey = config('webstatement.api_key'); + + // Cek apakah ini karena invalid API key + if ($xApiKey !== $expectedApiKey) { + throw new HttpResponseException( + response()->json( + ResponseCode::INVALID_TOKEN->toResponse( + null, + 'API Key tidak valid' + ), + ResponseCode::INVALID_TOKEN->getHttpStatus() + ) + ); + } + + // Untuk kasus HMAC signature tidak valid + throw new HttpResponseException( + response()->json( + ResponseCode::UNAUTHORIZED->toResponse( + null, + 'Signature tidak valid' + ), + ResponseCode::UNAUTHORIZED->getHttpStatus() ) ); } @@ -101,9 +211,23 @@ class BalanceSummaryRequest extends FormRequest protected function prepareForValidation(): void { Log::info('Balance summary request received', [ - 'input' => $this->all(), - 'ip' => $this->ip(), + 'input' => $this->all(), + 'ip' => $this->ip(), 'user_agent' => $this->userAgent() ]); + + $acount = AccountBalance::where('account_number', $this->account_number)->first(); + + if (!$acount) { + throw new HttpResponseException( + response()->json( + ResponseCode::ACCOUNT_NOT_FOUND->toResponse( + null, + 'Nomor rekening tidak ditemukan' + ), + ResponseCode::ACCOUNT_NOT_FOUND->getHttpStatus() + ) + ); + } } }