Konco punya dua lapisan auth: end-user auth (member/admin via session + 2FA) dan inter-service auth (dazo-whitelist ↔ dazo-whitelist-api via RSA signature + Passport Bearer). Keduanya berjalan independen — end-user tidak pernah melihat Passport token API engine.
End-user auth (web app)
Multi-guard
| Guard | Driver | Provider | Middleware | Target |
|---|---|---|---|---|
member (default) | session | users (Eloquent User) | auth/cekAuth:member | Member Portal (/member/*) |
admin | session | users (Eloquent User) | auth:admin | Admin Portal (/admin/*) |
Kedua guard memakai tabel users yang sama; pembedanya role (Spatie) + guard config. Config di config/auth.php:
'defaults' => ['guard' => 'member', 'passwords' => 'users'],
'guards' => [
'member' => ['driver' => 'session', 'provider' => 'users'],
'admin' => ['driver' => 'session', 'provider' => 'users'],
],
'providers' => [
'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
],Google 2FA
| Aspek | Keterangan |
|---|---|
| Package | pragmarx/google2fa-laravel ^2.2 |
| Middleware alias | 2fa (app/Http/Middleware/TwoFactorAuthentication.php) |
| Wajib untuk | Guard admin & member — semua route di dalam grup auth:admin, 2fa atau auth, 2fa |
| TOTP | Time-based One-Time Password via Authenticator app (Google Authenticator, Authy, dll) |
| Secret storage | Tabel UserAuthenticator (relasi User ↔ UserAuthenticator) |
| Verify route | GET /2fa/verify — tanpa middleware 2fa, agar user bisa submit kode |
RBAC (Spatie)
| Aspek | Keterangan |
|---|---|
| Package | spatie/laravel-permission ^5.9 |
| Model | Role, Permission (app/Models/) |
| Middleware | permission:<name> (e.g. permission:user-R, permission:user-CUD) |
| Permission pattern | <resource>-<action> — e.g. user-R (read), user-CUD (create/update/delete) |
| Seeder | database/seeders/ — seed roles & permissions awal |
| Helper | app/helper/role_helper.php — utility cek role/permission |
Permission diterapkan granular per route, contoh di routes/_admin.php:
$p = 'permission:user-';
$class = UserController::class;
Route::get('/', [$class, 'index'])->name('index')->middleware($p . 'R');
Route::post('/', [$class, 'store'])->name('store')->middleware($p . 'CUD');Inter-service auth (RSA + Passport)
Komunikasi dazo-whitelist → dazo-whitelist-api memakai dua lapisan:
- RSA 2048-bit signature — membuktikan identitas klien
- Passport OAuth2 Bearer Token — access token tervalidasi (2 jam TTL)
Handshake flow
dazo-whitelist (client) dazo-whitelist-api (server)
─────────────────────── ─────────────────────────
1. Baca API_KEY, API_SECRET, API_PRIVATE ┐
dari .env & storage/rsa_keys/ │
│
2. Buat timestamp ISO8601 (now) │
$data = implode('|', [ │
$client_key, // API_KEY (UUID app) │
$private_key, // API_PRIVATE │
$client_secret, // API_SECRET │
$timestamp // ISO8601 │
]); │
│
3. openssl_sign($data, $sig, private_key.pem, │
OPENSSL_ALGO_SHA256) │
$signature = base64_encode($sig) │
│
4. POST /api/application/token │
Headers: │
x-client-key: API_KEY (UUID) │
x-timestamp: 2026-08-21T10:26:52+07:00 │
x-signature: base64(...) ├──► 5. Validate signature
│
│ $sign = implode('|', [
│ $e->id, // UUID app
│ $e->private_key, // dari DB
│ $e->secret, // dari DB
│ $data['x-timestamp']
│ ]);
│
│ openssl_verify($sign,
│ base64_decode($x-signature),
│ $e->public_key,
│ OPENSSL_ALGO_SHA256)
│
│ 6. Cek timestamp ±10 menit
│ (toleransi replay)
│
│ 7. Issue Passport token (2 jam)
│ $e->tokens()->delete() // revoke old
│ $token = $e->createToken(...)
│
◄─────────────────────────────────────┤
8. Simpan token ke tabel `configs` │ return { access_token: ... }
(kolom `api_token`) │Payload signature (penting!)
Payload aktual (ApplicationController::token line 92):
$sign = implode('|', [$e->id, $e->private_key, $e->secret, $data['x-timestamp']]);| Elemen | Sumber (client) | Sumber (server) |
|---|---|---|
$e->id | API_KEY di .env (client) | Kolom id tabel applications |
$e->private_key | API_PRIVATE di .env (client) | Kolom private_key tabel applications |
$e->secret | API_SECRET di .env (client) | Kolom secret tabel applications |
x-timestamp | now()->format('Y-m-d\TH:i:sP') | Header x-timestamp |
Verifikasi di controller, bukan middleware
| Layer | Class | Fungsi |
|---|---|---|
Route middleware myapi | app/Http/Middleware/MyApi.php | Cek header public-key vs auth()->user()->public_key — simple equality check |
Route middleware auth:api | Laravel Passport default | Validasi Bearer token |
Controller ApplicationController::token | app/Http/Controllers/ApplicationController.php:62-110 | RSA verify signature + timestamp ±10 min + issue Passport token |
Controller ApplicationController::check | line 18-60 | RSA verify signature untuk endpoint info aplikasi |
Timestamp toleransi
$timestamp = Carbon::createFromFormat('Y-m-d\TH:i:sP', $data['x-timestamp']);
$now = Carbon::now();
if (!$timestamp->between($now->copy()->subMinute(10), $now)) {
return $this->error_response(400, 'Invalid Timestamp');
}Toleransi 10 menit — mencegah replay attack. Request dengan timestamp di luar rentang now-10min ... now ditolak.
Passport token lifecycle
| Aspek | Keterangan |
|---|---|
| TTL | 2 jam (Passport::personalAccessTokensExpireIn(now()->addHours(2))) |
| Storage (client) | Tabel configs (kolom api_token) di dazo-whitelist |
| Revocation | Token lama dihapus saat issue baru: $e->tokens()->delete() |
| Grant type | Personal Access Token (bukan password grant) |
401 auto-refresh retry loop
App\Src\MyApi::request menangkap respon 401 dan otomatis refresh token, lalu retry request:
// MyApi.php (disederhanakan)
static function request($path, $params, $method = 'GET', $loop = 0) {
// ... kirim request dengan Bearer token dari tabel configs ...
// Tangkap 401 — hanya retry 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
}
}RSA key management
Penyimpanan key pair
dazo-whitelist/
└── storage/rsa_keys/
├── private_key.pem # PEM format, RSA 2048-bit — dipakai openssl_sign
├── public_key.pem # PEM format — dikirim ke API engine, dipakai openssl_verify
└── .gitignore # wajib gitignoredGenerate & register
# Generate key pair saja
php artisan rsa_key
# Generate + daftar ke API engine + update .env sekaligus
php artisan app:register-client --name=konco-mainRegisterClientApp (app/Console/Commands/RegisterClientApp.php):
- Generate RSA 2048-bit key pair via OpenSSL
- Simpan ke
storage/rsa_keys/private_key.pem&public_key.pem - Create record
Applicationdi databasedazo-whitelist-api:id= UUIDsecret=bin2hex(openssl_random_pseudo_bytes(15))(30 char hex)public_key= isipublic_key.pem
- Update
.envdazo-whitelist:API_KEY=Application::idAPI_SECRET=Application::secretAPI_PRIVATE=Application::private_key(juga disimpan ke DB)API_PUBLIC= isipublic_key.pem
Yang TIDAK boleh diubah tanpa koordinasi
| Area | Lokasi | Dampak |
|---|---|---|
| Payload signature urutan | MyApi::get_token (client) ↔ ApplicationController::token (server) | Signature mismatch → 401 di semua request |
| Algoritma sign | OPENSSL_ALGO_SHA256 (kedua sisi) | Signature invalid |
| Headers handshake | x-client-key, x-timestamp, x-signature, public-key, Authorization | Request ditolak |
| Passport TTL | Passport::personalAccessTokensExpireIn(now()->addHours(2)) | Bila diperpendek, refresh lebih sering; bila diperpanjang, token expired tetap dipakai |
| Timestamp format | Y-m-d\TH:i:sP (ISO8601 with timezone) | Validation gagal |
| Timestamp toleransi | subMinute(10) | Bila diubah, replay window berubah |
Langkah berikutnya
- Detail step-by-step handshake & retry? Lihat RSA Handshake deep-dive.
- Setup local dengan RSA keys? Baca Local Development.
- Konvensi kode & trait? Lihat Conventions.