Mengamankan AI Agent PHP dengan sistem authentication yang kuat: login user, API Key per-user, JWT authentication, dan rate limiting untuk mencegah penyalahgunaan.
Daftar Isi
- Definisi: Mengapa AI Agent PHP Wajib Memiliki Sistem Autentikasi?
- TL;DR (Ringkasan Cepat)
- Desain Database untuk Menambahkan Authentication AI Agent PHP
- Menambahkan Authentication API Key pada AI Agent PHP
- Menambahkan Authentication JWT Token untuk AI Agent PHP
- Sliding Window Rate Limiter: Mencegah Token Abuse
- Resilient Redis Rate Limiter dengan Fallback Database Otomatis
- Middleware Otorisasi Berbasis Peran (RBAC) pada AI Agent PHP
- Middleware Keamanan pada Endpoint AI Agent PHP
- Pengujian Otomatis Keamanan AI Agent Menggunakan PHPUnit
- Matriks Komparasi HTTP Status Code Keamanan AI Agent (401 vs 403 vs 429)
- FAQ: Autentikasi dan Keamanan AI Agent PHP
- 1. Kapan sebaiknya menggunakan API Key vs JWT Token pada AI Agent?
- 2. Bagaimana cara mengamankan Secret Key JWT pada server PHP?
- 3. Mengapa Rate Limiting sangat kritis pada aplikasi AI Agent?
Terakhir diperbarui:
Definisi: Mengapa AI Agent PHP Wajib Memiliki Sistem Autentikasi?
Direct Answer:Autentikasi AI Agent PHP adalah lapisan keamanan gerbang (security gateway middleware) yang memvalidasi hak akses pengguna atau aplikasi eksternal melalui API Key statis atau JSON Web Token (JWT) terenkripsi serta membatasi kuota request (Rate Limiting) guna mencegah eksploitasi tak terotorisasi, pencurian data context, dan pembengkakan tagihan token model LLM.
Dalam kurikulum belajar ai-agent-php tingkat lanjut, penerapan Authentication AI Agent PHP adalah syarat mutlak sebelum meluncurkan aplikasi ke lingkungan produksi. Membuka endpoint agen tanpa proteksi sama dengan memberikan akses kartu kredit terbuka ke publik karena setiap prompt memicu biaya komputasi API OpenAI/Anthropic.
TL;DR (Ringkasan Cepat)
- API Key Authentication: Ideal untuk integrasi server-to-server, cron jobs, dan webhook backend.
- JWT (JSON Web Token): Standar emas untuk otentikasi client frontend (React, Vue, Mobile Apps) dengan masa berlaku (*expiration*) otomatis.
- Sliding Window Rate Limiter: Wajib diterapkan (misal: 20 request/menit) untuk menangkal serangan DDoS dan brute-force token exhaustion.
- Hashing Password Kuat: Simpan kredensial pengguna selalu menggunakan
password_hash(PASSWORD_BCRYPT).

Desain Database untuk Menambahkan Authentication AI Agent PHP
Struktur tabel MySQL di bawah ini mengelola entitas pengguna, penyimpanan kunci API berbasis hash, serta pencatatan kuota pembatasan lalu lintas (Rate Limiting):
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
api_key VARCHAR(64) UNIQUE NULL,
role ENUM('user','admin') DEFAULT 'user',
is_active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_api_key (api_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE rate_limits (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(100) NOT NULL,
requests INT DEFAULT 0,
window_end DATETIME NOT NULL,
INDEX idx_identifier_window (identifier, window_end)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;Menambahkan Authentication API Key pada AI Agent PHP
Class ApiKeyAuth memvalidasi header request X-API-Key menggunakan parameterized query PDO untuk mencegah SQL Injection:
<?php
namespace App\Auth;
use PDO;
class ApiKeyAuth
{
private PDO $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* Memvalidasi API Key pengguna aktif.
*/
public function authenticate(?string $apiKey): ?array
{
if (empty($apiKey) || strlen($apiKey) < 32) {
return null;
}
$stmt = $this->pdo->prepare(
'SELECT id, name, email, role FROM users
WHERE api_key = ? AND is_active = 1 LIMIT 1'
);
$stmt->execute([$apiKey]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user ?: null;
}
/**
* Membuat API Key baru 64 karakter hex cryptographically secure.
*/
public function generateApiKey(int $userId): string
{
$apiKey = bin2hex(random_bytes(32)); // 64 karakter aman
$stmt = $this->pdo->prepare('UPDATE users SET api_key = ? WHERE id = ?');
$stmt->execute([$apiKey, $userId]);
return $apiKey;
}
}Menambahkan Authentication JWT Token untuk AI Agent PHP
Untuk aplikasi antarmuka web dan mobile, token JSON Web Token (JWT) berbasis algoritma HS256 memberikan keamanan nir-keadaan (stateless) dengan klaim waktu kedaluwarsa (exp):
<?php
namespace App\Auth;
class JwtAuth
{
private string $secretKey;
private int $ttlSeconds;
public function __construct(string $secretKey, int $ttlSeconds = 3600)
{
$this->secretKey = $secretKey;
$this->ttlSeconds = $ttlSeconds;
}
/**
* Menghasilkan JWT Token yang ditandatangani secara kriptografis (HS256).
*/
public function generateToken(array $user): string
{
$header = $this->base64UrlEncode(json_encode(['typ' => 'JWT', 'alg' => 'HS256']));
$payload = $this->base64UrlEncode(json_encode([
'sub' => $user['id'],
'name' => $user['name'],
'role' => $user['role'] ?? 'user',
'iat' => time(),
'exp' => time() + $this->ttlSeconds,
]));
$signature = $this->base64UrlEncode(
hash_hmac('sha256', "{$header}.{$payload}", $this->secretKey, true)
);
return "{$header}.{$payload}.{$signature}";
}
/**
* Memverifikasi dan mendekode JWT Bearer Token.
*/
public function validateToken(?string $jwt): ?array
{
if (empty($jwt)) {
return null;
}
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
return null;
}
[$header64, $payload64, $signature64] = $parts;
// Verifikasi Integritas Tanda Tangan HMAC-SHA256
$expectedSig = $this->base64UrlEncode(
hash_hmac('sha256', "{$header64}.{$payload64}", $this->secretKey, true)
);
if (!hash_equals($expectedSig, $signature64)) {
return null; // Tanda tangan tidak valid / telah dimanipulasi
}
$payload = json_decode($this->base64UrlDecode($payload64), true);
// Periksa Kedaluwarsa Waktu Token (Expiration Check)
if (!isset($payload['exp']) || $payload['exp'] < time()) {
return null; // Token telah kedaluwarsa
}
return $payload;
}
private function base64UrlEncode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private function base64UrlDecode(string $data): string
{
return base64_decode(strtr($data, '-_', '+/'));
}
}Sliding Window Rate Limiter: Mencegah Token Abuse
Mekanisme pembatasan kuota per-user/per-IP untuk melindungi anggaran tagihan model LLM:
<?php
namespace App\Auth;
use PDO;
class RateLimiter
{
private PDO $pdo;
private int $maxRequests;
private int $windowSeconds;
public function __construct(PDO $pdo, int $maxRequests = 20, int $windowSeconds = 60)
{
$this->pdo = $pdo;
$this->maxRequests = $maxRequests;
$this->windowSeconds = $windowSeconds;
}
public function isAllowed(string $identifier): bool
{
$now = time();
$windowEnd = date('Y-m-d H:i:s', $now + $this->windowSeconds);
// 1. Bersihkan entri kedaluwarsa secara berkala
$this->pdo->prepare('DELETE FROM rate_limits WHERE window_end < NOW()')->execute();
// 2. Cek kuota aktif dalam window berjalan
$stmt = $this->pdo->prepare(
'SELECT id, requests FROM rate_limits WHERE identifier = ? AND window_end > NOW() LIMIT 1'
);
$stmt->execute([$identifier]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
$this->pdo->prepare(
'INSERT INTO rate_limits (identifier, requests, window_end) VALUES (?, 1, ?)'
)->execute([$identifier, $windowEnd]);
return true;
}
if ($row['requests'] >= $this->maxRequests) {
return false; // Kuota limit terlampaui
}
// 3. Tambah hit counter
$this->pdo->prepare('UPDATE rate_limits SET requests = requests + 1 WHERE id = ?')
->execute([$row['id']]);
return true;
}
}Resilient Redis Rate Limiter dengan Fallback Database Otomatis
Di lingkungan cloud dinamis, Redis dapat mengalami *network partition* atau *failover restart*. Menerapkan blok try-catch dengan *fallback* otomatis ke database MySQL atau status *fail-open* mencegah endpoint AI Agent mengembalikan HTTP 500 Internal Server Error kepada pengguna:
<?php
namespace App\Auth;
use Redis;
use PDO;
use Throwable;
class ResilientRedisRateLimiter
{
private ?Redis $redis;
private ?PDO $pdoFallback;
private int $maxRequests;
private int $windowSeconds;
public function __construct(?Redis $redis, ?PDO $pdoFallback = null, int $maxRequests = 20, int $windowSeconds = 60)
{
$this->redis = $redis;
$this->pdoFallback = $pdoFallback;
$this->maxRequests = $maxRequests;
$this->windowSeconds = $windowSeconds;
}
/**
* Sliding Window Log dengan Fallback Graceful jika Redis Downtime.
*/
public function isAllowed(string $identifier): bool
{
// 1. Coba Eksekusi via Redis In-Memory
if ($this->redis !== null) {
try {
$key = "rate_limit:{$identifier}";
$now = microtime(true);
$windowStart = $now - $this->windowSeconds;
$pipe = $this->redis->multi();
$pipe->zRemRangeByScore($key, '-inf', (string)$windowStart);
$pipe->zAdd($key, $now, "{$now}:" . bin2hex(random_bytes(4)));
$pipe->zCard($key);
$pipe->expire($key, $this->windowSeconds + 5);
$results = $pipe->exec();
$requestCount = $results[2] ?? 0;
return $requestCount <= $this->maxRequests;
} catch (Throwable $e) {
// Catat error Redis tanpa memicu HTTP 500
error_log("Redis Rate Limiter Error: " . $e->getMessage() . " -> Switching to Database Fallback.");
}
}
// 2. Fallback Otomatis ke Database MySQL / PDO
if ($this->pdoFallback !== null) {
try {
$mysqlLimiter = new RateLimiter($this->pdoFallback, $this->maxRequests, $this->windowSeconds);
return $mysqlLimiter->isAllowed($identifier);
} catch (Throwable $dbErr) {
error_log("Database Fallback Rate Limiter Error: " . $dbErr->getMessage());
}
}
// 3. Fail-Open Safe: Izinkan request jika seluruh layer cache/DB gagal sementara
return true;
}
}High Availability (HA) Rule: Sistem rate limiting tidak boleh menjadi single point of failure (SPOF) yang melumpuhkan layanan agen.
Middleware Otorisasi Berbasis Peran (RBAC) pada AI Agent PHP
Selain otentikasi identitas, sistem enterprise memerlukan Role-Based Access Control (RBAC) untuk membatasi eksekusi tools berkategori bahaya tinggi (seperti DatabaseTool untuk modifikasi data atau eksekusi SQL) hanya untuk role admin, sedangkan role user hanya diizinkan memanggil tools non-destruktif (seperti CalculatorTool dan WeatherTool):
<?php
namespace App\Auth;
class ToolPermissionManager
{
/**
* Matriks perizinan eksekusi tools berdasarkan role pengguna.
*/
private static array $rolePermissions = [
'user' => [
'calculate',
'get_weather',
'convert_currency',
'read_public_file',
],
'admin' => [
'calculate',
'get_weather',
'convert_currency',
'read_public_file',
'search_database_users', // Akses Database
'execute_custom_query', // Akses Query Khusus
'github_search',
],
];
/**
* Memverifikasi apakah role pengguna diizinkan mengeksekusi nama tool tertentu.
*/
public static function canExecute(string $role, string $toolName): bool
{
$allowedTools = self::$rolePermissions[$role] ?? [];
return in_array($toolName, $allowedTools, true);
}
/**
* Memfilter daftar tools yang dikirimkan ke model AI berdasarkan role pengguna.
*/
public static function filterAvailableTools(string $role, array $allToolDefinitions): array
{
return array_values(array_filter($allToolDefinitions, function ($tool) use ($role) {
$name = $tool['function']['name'] ?? $tool['name'] ?? '';
return self::canExecute($role, $name);
}));
}
}Dalam loop eksekusi MultiToolsAgent, jika pengguna dengan role user mencoba memicu pemanggilan search_database_users, interceptor otorisasi akan menolak eksekusi dan mengembalikan status HTTP 403 Forbidden dengan pesan edukatif.
Middleware Keamanan pada Endpoint AI Agent PHP
Menggabungkan autentikasi ganda (API Key / JWT), evaluasi perizinan RBAC, dan Rate Limiter pada controller endpoint API:
<?php
// api/agent.php
header('Content-Type: application/json');
require_once __DIR__ . '/../vendor/autoload.php';
$pdo = require __DIR__ . '/../config/database.php';
$authKey = new App\Auth\ApiKeyAuth($pdo);
$authJwt = new App\Auth\JwtAuth(getenv('JWT_SECRET') ?: 'default-secret-key-32-chars-min!');
// 1. Ekstrak Kredensial dari Header
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? null;
$authHdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$jwtToken = preg_match('/Bearer\s+(\S+)/i', $authHdr, $m) ? $m[1] : null;
$user = null;
if ($apiKey) {
$user = $authKey->authenticate($apiKey);
} elseif ($jwtToken) {
$user = $authJwt->validateToken($jwtToken);
}
// 2. Tolak Jika Tidak Terautentikasi (HTTP 401)
if (!$user) {
http_response_code(401);
echo json_encode([
'status' => 'unauthorized',
'message' => 'Akses ditolak: Kredensial API Key atau JWT Token tidak valid.',
]);
exit;
}
// 3. Evaluasi Batas Rate Limit (HTTP 429)
$userId = $user['id'] ?? $user['sub'];
$limiter = new App\Auth\RateLimiter($pdo, maxRequests: 20, windowSeconds: 60);
if (!$limiter->isAllowed("user:{$userId}")) {
http_response_code(429);
echo json_encode([
'status' => 'rate_limited',
'message' => 'Batas kuota request (20 req/menit) terlampaui. Silakan coba kembali nanti.',
]);
exit;
}
// 4. Jalankan AI Agent Loop dengan Aman
$payload = json_decode(file_get_contents('php://input'), true);
$task = trim($payload['task'] ?? '');
if (empty($task)) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Parameter task wajib diisi.']);
exit;
}
$agent = new App\Agent\MultiToolsAgent($openAiClient, $mcpClient);
$result = $agent->run($task);
echo json_encode([
'status' => 'success',
'user' => $user['name'] ?? 'Authenticated User',
'result' => $result,
]);Pengujian Otomatis Keamanan AI Agent Menggunakan PHPUnit
Untuk memastikan lapisan autentikasi dan pembatasan laju request berfungsi dengan andal, buat unit test terotomatisasi menggunakan PHPUnit guna memverifikasi skenario penolakan HTTP 401 Unauthorized dan HTTP 429 Too Many Requests:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use App\Auth\ApiKeyAuth;
use App\Auth\JwtAuth;
use App\Auth\RateLimiter;
use PDO;
class AgentSecurityTest extends TestCase
{
private PDO $pdo;
private ApiKeyAuth $apiKeyAuth;
private JwtAuth $jwtAuth;
private RateLimiter $rateLimiter;
protected function setUp(): void
{
// Gunakan SQLite In-Memory Database untuk pengujian cepat
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Buat skema tabel pengujian
$this->pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, email TEXT, api_key TEXT, is_active INTEGER
)');
$this->pdo->exec('CREATE TABLE rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
identifier TEXT, requests INTEGER, window_end TEXT
)');
// Isi data mock user
$this->pdo->exec("INSERT INTO users (name, email, api_key, is_active)
VALUES ('Rusmawan', 'rusmawan@infokoding.com', 'valid-mock-api-key-32-chars-long-123', 1)");
$this->apiKeyAuth = new ApiKeyAuth($this->pdo);
$this->jwtAuth = new JwtAuth('test-secret-key-32-chars-minimum!', ttlSeconds: 60);
$this->rateLimiter = new RateLimiter($this->pdo, maxRequests: 3, windowSeconds: 60);
}
/**
* Menguji Penolakan HTTP 401 jika API Key Kosong atau Tidak Valid.
*/
public function testRejectsUnauthorizedRequestWithInvalidApiKey(): void
{
$invalidResult = $this->apiKeyAuth->authenticate('wrong-or-malicious-key');
$emptyResult = $this->apiKeyAuth->authenticate(null);
$this->assertNull($invalidResult, 'API Key salah harus menghasilkan null (HTTP 401)');
$this->assertNull($emptyResult, 'API Key kosong harus menghasilkan null (HTTP 401)');
}
/**
* Menguji Validasi Sukses dan Penolakan Token JWT yang Telah Kedaluwarsa.
*/
public function testJwtTokenValidationAndExpiration(): void
{
$validToken = $this->jwtAuth->generateToken(['id' => 101, 'name' => 'Rusmawan', 'role' => 'admin']);
$payload = $this->jwtAuth->validateToken($validToken);
$this->assertNotNull($payload);
$this->assertSame(101, $payload['sub']);
// Uji token kedaluwarsa dengan TTL 0 detik
$expiredJwtAuth = new JwtAuth('test-secret-key-32-chars-minimum!', ttlSeconds: -10);
$expiredToken = $expiredJwtAuth->generateToken(['id' => 101, 'name' => 'Rusmawan']);
$this->assertNull($this->jwtAuth->validateToken($expiredToken), 'Token kedaluwarsa harus ditolak (HTTP 401)');
}
/**
* Menguji Penolakan HTTP 429 ketika Kuota Rate Limit Terlampaui.
*/
public function testEnforcesRateLimitingWithHttp429Scenario(): void
{
$clientId = 'client_ip_192_168_1_100';
// 3 request pertama diizinkan (maxRequests = 3)
$this->assertTrue($this->rateLimiter->isAllowed($clientId), 'Request 1 harus lolos');
$this->assertTrue($this->rateLimiter->isAllowed($clientId), 'Request 2 harus lolos');
$this->assertTrue($this->rateLimiter->isAllowed($clientId), 'Request 3 harus lolos');
// Request ke-4 harus diblokir (memicu status HTTP 429)
$this->assertFalse(
$this->rateLimiter->isAllowed($clientId),
'Request ke-4 harus diblokir karena melebihi kuota 3 req/window (HTTP 429)'
);
}
}TDD Tip: Jalankan vendor/bin/phpunit tests/Unit/AgentSecurityTest.php dalam pipeline CI/CD untuk menjamin endpoint tidak mengalami regresi keamanan.
Matriks Komparasi HTTP Status Code Keamanan AI Agent (401 vs 403 vs 429)
Diagram ASCII di bawah ini menggambarkan alur pengambilan keputusan gerbang keamanan, sedangkan tabel semantik HTML merinci status kode HTTP yang wajib dikembalikan ke klien:
[ INBOUND CLIENT REQUEST ]
│
▼
┌───────────────────┐ TIDAK
│ Punya Kredensial? ├───────────────────► [ HTTP 401 Unauthorized ]
└─────────┬─────────┘ Kredensial API Key/JWT kosong atau invalid
│ YA
▼
┌───────────────────┐ TIDAK
│ Punya Hak Akses? ├───────────────────► [ HTTP 403 Forbidden ]
└─────────┬─────────┘ Akun terblokir / Role tidak mencukupi
│ YA
▼
┌───────────────────┐ TIDAK
│ Kuota Tersedia? ├───────────────────► [ HTTP 429 Too Many Requests ]
└─────────┬─────────┘ Batas rate limit per-menit terlampaui
│ YA
▼
[ EXECUTE AI AGENT & LLM LOOP ]| HTTP Status | Nama Standar RFC | Pemicu pada AI Agent | Rekomendasi Tindakan Klien |
|---|---|---|---|
| 401 | Unauthorized | Header X-API-Key kosong, format JWT malformed, atau token telah kedaluwarsa (exp expired). | Kirim ulang request dengan menyertakan token API Key atau login ulang untuk mendapatkan JWT baru. |
| 403 | Forbidden | Pengguna teridentifikasi valid, namun akun berstatus non-aktif (is_active=0) atau role dibatasi. | Hubungi administrator sistem untuk meningkatkan tier paket akses atau memulihkan status akun. |
| 429 | Too Many Requests | Frekuensi pemanggilan melebihi kuota 20 req/menit pada sliding window identifier. | Tunda request dan terapkan Exponential Backoff sesuai durasi header Retry-After. |
Crawler Parsing Note: Tag <table> di atas disusun secara semantik dengan atribut scope="col" dan scope="row" untuk mempermudah Google Search Engine dan AI Retrieval mengekstrak data komparasi tabular secara akurat.
FAQ: Autentikasi dan Keamanan AI Agent PHP
1. Kapan sebaiknya menggunakan API Key vs JWT Token pada AI Agent?
Ringkasan Jawaban:Gunakan API Key untuk integrasi server-to-server (backend backend atau cron job otomatis) dan gunakan JWT Token untuk aplikasi web/mobile interaktif di mana pengguna melakukan login langsung.
JWT memiliki masa kedaluwarsa otomatis (TTL) yang mencegah penyalahgunaan token jika perangkat pengguna hilang.
2. Bagaimana cara mengamankan Secret Key JWT pada server PHP?
Ringkasan Jawaban:SimpanJWT_SECRETdi environment variable (.env) dengan panjang minimal 256-bit (32 karakter acak) dan jangan pernah memasukkannya ke dalam repositori Git.
Rotasikan kunci JWT secara berkala untuk menjaga integritas tanda tangan digital.
3. Mengapa Rate Limiting sangat kritis pada aplikasi AI Agent?
Ringkasan Jawaban:Karena setiap interaksi AI Agent mengeksekusi komputasi LLM dan memanggil tools berbayar, Rate Limiting mencegah serangan DoS yang dapat menguras ribuan dolar saldo API Anda dalam beberapa menit.
Kombinasikan rate limit per-user dengan rate limit global IP untuk perlindungan maksimal.
- Dual Authentication Layer: Menggabungkan
ApiKeyAuthuntuk mesin latar belakang (cron/webhook) danJwtAuthHS256 untuk interaksi pengguna frontend. - Resilient Sliding Window: Penerapan pembatasan laju request (20 req/menit) dengan strategi cache in-memory Redis dan fallback database MySQL anti-downtime.
- Granular RBAC Authorization: Pembatasan pemanggilan tool berbahaya (seperti eksekusi query SQL) hanya untuk role
adminmelaluiToolPermissionManager. - Automated Security Testing: Validasi regresi otomatis menggunakan PHPUnit untuk memastikan kode status
HTTP 401,HTTP 403, danHTTP 429dikembalikan secara presisi.
Rusmawan Abdullah Sani
Lead Software Engineer & System Architect
Praktisi pengembangan backend PHP modern, arsitektur AI Agent, microservices, dan otomasi server Linux. Terhubung melalui profil LinkedIn.