Handshake antara dazo-whitelist (client) dan dazo-whitelist-api (server) memakai RSA 2048-bit signature dengan SHA-256. Signature diverifikasi di ApplicationController::token (server), bukan di middleware. Setelah verifikasi sukses, server issue Passport Personal Access Token (2 jam TTL) dan client menyimpannya di tabel configs.
Prasyarat
storage/rsa_keys/private_key.pemdanpublic_key.pemada didazo-whitelist- Record di tabel
applications(dazo-whitelist-api) berisiid,secret,private_key,public_keyyang cocok .envdazo-whitelistberisiAPI_KEY,API_SECRET,API_PRIVATE,API_PUBLIC(otomatis terisi viaapp:register-client)
Flow handshake lengkap
┌────────────────────────────────────────────────────────────────────┐
│ 1. PERSIAPAN (sekali saat setup) │
└────────────────────────────────────────────────────────────────────┘
dazo-whitelist (client) dazo-whitelist-api (server)
────────────────────── ─────────────────────────
php artisan app:register-client
│
├─ Generate RSA 2048-bit key pair
│ openssl_pkey_new(["private_key_bits" => 2048])
│ openssl_pkey_export($res, $privateKey)
│ $publicKey = openssl_pkey_get_details($res)["key"]
│
├─ Simpan ke storage/rsa_keys/
│ private_key.pem
│ public_key.pem
│
├─ Create record Application (via HTTP ke API engine atau langsung DB)
│ id = Str::uuid()
│ secret = bin2hex(openssl_random_pseudo_bytes(15))
│ private_key = <isi private_key.pem>
│ public_key = <isi public_key.pem>
│
└─ Update .env dazo-whitelist
API_KEY = Application::id
API_SECRET = Application::secret
API_PRIVATE= Application::private_key
API_PUBLIC = <isi public_key.pem>
┌────────────────────────────────────────────────────────────────────┐
│ 2. HANDSHAKE (saat token 401/expired) │
└────────────────────────────────────────────────────────────────────┘
MyApi::get_token() ApplicationController::token()
───────────────────── ──────────────────────────────
1. Baca config:
$client_key = config('app.api_key') // .env API_KEY
$client_secret = config('app.api_secret') // .env API_SECRET
$private_key = config('app.api_private') // .env API_PRIVATE
2. Cek lengkap:
if (!$client_key || !$client_secret || !$private_key)
return ['error' => 'Invalid credential'];
3. Generate timestamp ISO8601:
$timestamp = now()->format('Y-m-d\TH:i:sP')
// e.g. "2026-08-21T10:26:52+07:00"
4. Load private key dari filesystem:
$privateKeyPath = storage_path('rsa_keys/private_key.pem')
if (!File::exists($privateKeyPath))
return ['error' => 'Key tidak ditemukan!'];
5. Build payload (PENTING — urutan & elemen):
$data = implode('|', [
$client_key, // elemen 1: id (UUID)
$private_key, // elemen 2: private_key string (BUKAN public_key!)
$client_secret, // elemen 3: secret
$timestamp, // elemen 4: ISO8601 timestamp
]);
6. Sign dengan RSA + SHA-256:
openssl_sign($data, $signature, File::get($privateKeyPath),
OPENSSL_ALGO_SHA256);
$signature_b64 = base64_encode($signature);
7. POST /api/application/token ─────────────► 8. Receive request
Headers: $data = [
x-client-key: $client_key 'x-client-key' => header('x-client-key'),
x-timestamp: $timestamp 'x-signature' => header('x-signature'),
x-signature: $signature_b64 'x-timestamp' => header('x-timestamp'),
];
─────────────────────────
9. Validate input:
'x-client-key' => 'required|uuid'
'x-signature' => 'required'
'x-timestamp' => 'required|date_format:Y-m-d\TH:i:sP'
10. Parse & cek timestamp:
$timestamp = Carbon::createFromFormat('Y-m-d\TH:i:sP', ...)
if (!$timestamp->between(now-10min, now))
return 400 'Invalid Timestamp'
11. Lookup Application:
$e = Application::where('id', $x-client-key)->first()
if (!$e || !$e->public_key)
return 404 'Invalid app'
12. Rebuild payload (server-side):
$sign = implode('|', [
$e->id, // dari DB
$e->private_key, // dari DB
$e->secret, // dari DB
$data['x-timestamp']
]);
13. Verify signature:
$verified = openssl_verify(
$sign,
base64_decode($x-signature),
$e->public_key,
OPENSSL_ALGO_SHA256
);
if ($verified !== 1)
return 404 'Invalid Signature'
14. Issue Passport token:
Passport::personalAccessTokensExpireIn(now()->addHours(2));
$e->tokens()->delete(); // revoke old
$token = $e->createToken('...');
◄────────────────────────────── 15. Return:
{ "data": { "access_token": "..." } }
16. Simpan token ke tabel configs:
Config::updateOrCreate(
['name' => 'api_token'],
['value' => $token]
);
17. Return:
['type' => 'Bearer', 'token' => $token]Payload signature (KRITIS)
Payload signature harus identik antara client & server:
// Client (MyApi::get_token)
$data = implode('|', [$client_key, $private_key, $client_secret, $timestamp]);
// Server (ApplicationController::token)
$sign = implode('|', [$e->id, $e->private_key, $e->secret, $data['x-timestamp']]);| Posisi | Client side | Server side | Sumber aktual |
|---|---|---|---|
| 1 | $client_key | $e->id | Application.id (UUID) |
| 2 | $private_key | $e->private_key | Application.private_key — sama dengan API_PRIVATE di .env client |
| 3 | $client_secret | $e->secret | Application.secret — sama dengan API_SECRET di .env client |
| 4 | $timestamp | $data['x-timestamp'] | Header request x-timestamp |
Verifikasi di controller, bukan middleware
| Layer | Class | Fungsi |
|---|---|---|
Middleware auth:api | Laravel Passport default | Validasi Bearer token di header Authorization |
Middleware myapi | app/Http/Middleware/MyApi.php | Cek header public-key == auth()->user()->public_key — equality check sederhana |
Controller ApplicationController::token | app/Http/Controllers/ApplicationController.php:62-110 | RSA verify + timestamp check + issue Passport token |
Controller ApplicationController::check | line 18-60 | RSA verify untuk endpoint info aplikasi |
Timestamp toleransi
Server menerima request dengan timestamp dalam rentang:
$timestamp->between($now->copy()->subMinute(10), $now)- Toleransi: 10 menit ke belakang (tidak menerima timestamp di masa depan)
- Tujuan: mencegah replay attack — signature yang di-intercept tidak bisa dipakai ulang setelah 10 menit
- Format: ISO8601 dengan timezone —
Y-m-d\TH:i:sP(e.g.2026-08-21T10:26:52+07:00)
Passport token lifecycle
| Aspek | Keterangan |
|---|---|
| Grant type | Personal Access Token (bukan password grant, bukan client_credentials) |
| TTL | 2 jam (Passport::personalAccessTokensExpireIn(now()->addHours(2))) |
| Revocation | Token lama dihapus sebelum issue baru: $e->tokens()->delete() |
| Storage client | Tabel configs (kolom api_token) di dazo-whitelist |
| Storage server | Tabel oauth_access_tokens (default Passport) di dazo-whitelist-api |
401 auto-refresh retry loop
MyApi::request() menangkap response 401 dan otomatis refresh + retry sekali:
static function request($path, $params, $method = 'GET', $loop = 0) {
// ... kirim request ...
// Catch 401 — hanya refresh sekali ($loop === 0)
if ($response->getStatusCode() == 401 && !$loop && isset($params['headers']['Authorization'])) {
$token = static::get_token(); // handshake ulang
if ($token['error'] ?? null) return $token;
$params['headers']['Authorization'] = 'Bearer ' . $token['token'];
return static::request($path, $params, $method, 1); // retry dengan $loop=1
}
}Juga menangkap RequestException dengan code 401:
} catch (RequestException $e) {
if ($e->hasResponse()) {
$code = $e->getCode();
if (!$loop && $code == 401 && isset($params['headers']['Authorization'])) {
$token = static::get_token();
if ($token['error'] ?? null) return $token;
$params['headers']['Authorization'] = 'Bearer ' . $token['token'];
$responseRetry = static::request($path, $params, $method, 1);
return $responseRetry;
}
}
}Header public-key (di request normal)
Setelah handshake, setiap request normal memakai MyApi::params():
static function params($params) {
$params['headers']['Authorization'] = 'Bearer ' . Config::get_val('api_token');
$params['headers']['public-key'] = config('app.api_public');
$params['headers']['Accept'] = 'application/json';
return $params;
}| Header | Nilai | Divalidasi oleh |
|---|---|---|
Authorization | Bearer {token} dari tabel configs | Middleware auth:api (Passport) |
public-key | API_PUBLIC dari .env | Middleware myapi — cek auth()->user()->public_key == $header |
Accept | application/json | — |
Regenerate RSA keys
Jika private key bocor atau perlu rotasi:
# Di dazo-whitelist
php artisan rsa_key # generate key pair baru
php artisan app:register-client --name=konco-main # re-register ke API engineTroubleshooting
| Gejala | Penyebab | Solusi |
|---|---|---|
401 Unauthorized di semua request ke API engine | Public key di tabel applications tidak cocok private_key.pem | php artisan app:register-client |
Invalid Timestamp | Clock drift antara server client & server API engine > 10 menit | Sinkronkan NTP kedua server |
Invalid Signature | Payload client tidak identik server (e.g. .env tidak ter-update saat register-client) | Cek .env API_KEY/API_SECRET/API_PRIVATE cocok record applications |
Key tidak ditemukan! | storage/rsa_keys/private_key.pem belum ada | php artisan rsa_key atau app:register-client |
| Loop 401 terus-menerus | Application record tidak punya public_key | php artisan app:register-client |
404 Invalid app | API_KEY di .env tidak cocok record applications di API engine | app:register-client atau update .env manual |
Langkah berikutnya
- Detail auth end-user (guard, 2FA)? Baca Auth & Security.
- Detail operasi API engine pasca-handshake? Lihat API Reference.
- Skema tabel
applications? Lihat Schema.