Memperdalam keamanan API: implementasi JWT dengan masa berlaku dan refresh token, OAuth2 untuk integrasi pihak ketiga dan social login, API token dengan expiry, serta perlindungan endpoint autentikasi dari brute-force dengan rate limiting dan throttling.

Di episode 10 kita mengamankan API dengan token sederhana — sebuah string acak yang disimpan di tabel users. Itu bekerja, tapi punya keterbatasan nyata: token tidak bisa kadaluarsa tanpa mekanisme ekstra, dan memverifikasi setiap token berarti query ke database. Ketika ekosistem kalian tumbuh — mobile app, SPA, partner integrasi — kalian butuh skema autentikasi yang lebih matang: JWT, OAuth2, dan token dengan siklus hidup lengkap.
Di episode ini kita membangun lapisan keamanan API yang production-grade: JWT stateless dengan masa berlaku dan refresh token, OAuth2 untuk social login dan integrasi pihak ketiga, API token dengan expiry yang terkelola, serta perlindungan endpoint login dari serangan brute-force.
JWT (JSON Web Token) adalah token yang self-contained: seluruh klaim (user id, role, masa berlaku) terenkripsi di dalam token, ditandatangani dengan kunci rahasia. Server tidak perlu query database untuk memverifikasi — cukup memeriksa tanda tangan dan masa berlaku.
Pasang package JWT paling umum di ekosistem PHP:
composer require firebase/php-jwt<?php
namespace app\services;
use app\models\User;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Yii;
class TokenService
{
private const ALGORITHM = 'HS256';
public function issue(User $user, int $ttl = 3600): string
{
$now = time();
$payload = [
'iss' => 'app-kalian',
'iat' => $now,
'exp' => $now + $ttl,
'sub' => $user->id,
'role' => $user->getRoleName(),
];
return JWT::encode($payload, $this->secret(), self::ALGORITHM);
}
public function verify(string $token): array
{
return (array) JWT::decode($token, new Key($this->secret(), self::ALGORITHM));
}
private function secret(): string
{
return Yii::$app->params['jwtSecret'];
}
}Tiga klaim wajib: iat (dibuat), exp (kedaluwarsa), sub (subjek/user). Jangan pernah menaruh data sensitif (password) di payload — JWT hanya di-sign, bukan di-encrypt.
Buat class autentikator yang membaca JWT dan memuat user:
<?php
namespace app\auth;
use app\models\User;
use app\services\TokenService;
use Yii;
use yii\filters\auth\AuthMethod;
class JwtAuth extends AuthMethod
{
public function authenticate($user, $request, $response): ?\yii\web\IdentityInterface
{
$header = $request->getHeaders()->get('Authorization', '');
if (!preg_match('/^Bearer\s+(.+)$/', $header, $matches)) {
return null;
}
try {
$payload = Yii::createObject(TokenService::class)->verify($matches[1]);
} catch (\Throwable $e) {
$this->handleFailure($response);
return null;
}
$identity = User::findIdentity($payload['sub']);
if ($identity === null) {
$this->handleFailure($response);
return null;
}
return $identity;
}
}$behaviors['authenticator'] = [
'class' => \app\auth\JwtAuth::class,
];Sekarang setiap request Authorization: Bearer <jwt> diverifikasi tanpa database query — inilah kekuatan stateless authentication untuk API yang melayani ribuan request per detik.
Important
JWT stateless punya konsekuensi keamanan yang harus dipahami: token tidak bisa dicabut sebelum kedaluwarsa. Jika token dicuri, attacker bisa memakainya sampai exp. Solusi praktik: gunakan TTL pendek (15-60 menit) untuk access token, dan pasangkan refresh token (disimpan di server, bisa dicabut) untuk mendapatkan access token baru. Jangan pernah memakai JWT ber-TTL panjang tanpa strategi pencabutan.
Pola standar: access token (JWT, TTL pendek) untuk setiap request + refresh token (random string, disimpan di database, TTL panjang) untuk mendapatkan access token baru.
public function safeUp(): void
{
$this->createTable('refresh_tokens', [
'id' => $this->primaryKey(),
'user_id' => $this->integer()->notNull(),
'token' => $this->string(64)->notNull()->unique(),
'expires_at' => $this->integer()->notNull(),
'revoked_at' => $this->integer()->null(),
'created_at' => $this->integer()->notNull(),
]);
}public function actionRefresh()
{
$params = Yii::$app->request->post();
$row = RefreshToken::find()
->where([
'token' => $params['refresh_token'],
'user_id' => Yii::$app->user->id,
])
->andWhere(['is', 'revoked_at', null])
->andWhere(['>', 'expires_at', time()])
->one();
if ($row === null) {
throw new UnauthorizedHttpException('Refresh token tidak valid.');
}
// rotasi: token lama dicabut, yang baru diterbitkan
$row->revoked_at = time();
$row->save(false);
return [
'access_token' => $this->tokenService->issue(Yii::$app->user->identity),
'refresh_token' => $this->tokenService->issueRefreshToken(Yii::$app->user->id),
];
}Rotasi refresh token — setiap penggunaan mengeluarkan token baru — memastikan token lama tidak bisa dipakai ulang jika bocor.
Ketika aplikasi kalian harus memakai login Google, GitHub, atau menyediakan akses ke partner, OAuth2 adalah protokol standarnya. Untuk social login (client OAuth2), Yii menyediakan yii2-authclient:
composer require yiisoft/yii2-authclient'components' => [
'authClientCollection' => [
'class' => 'yii\authclient\Collection',
'clients' => [
'google' => [
'class' => 'yii\authclient\clients\Google',
'clientId' => getenv('GOOGLE_CLIENT_ID'),
'clientSecret' => getenv('GOOGLE_CLIENT_SECRET'),
],
'github' => [
'class' => 'yii\authclient\clients\GitHub',
'clientId' => getenv('GITHUB_CLIENT_ID'),
'clientSecret' => getenv('GITHUB_CLIENT_SECRET'),
],
],
],
],Login sosial dilakukan lewat widget yii\authclient\widgets\AuthChoice di view login. Jika aplikasi kalian justru menjadi provider OAuth2 (menyediakan API ke pihak ketiga dengan flow OAuth2), gunakan ekosistem league/oauth2-server melalui package integrasi Yii (misal marc-mabe/php-enum pendukung) — konsepnya: client aplikasi mendaftar, user menyetujui scope, dan API diakses dengan access token yang diterbitkan server.
Tip
Pilih sesuai arah integrasi: client (memakai OAuth2 Google/GitHub untuk login) → yii2-authclient. server (membuka API kalian ke aplikasi lain dengan flow OAuth2) → league/oauth2-server via integrasi Yii. Keduanya tidak saling menggantikan — banyak aplikasi memakai keduanya sekaligus.
Untuk integrasi sederhana (partner yang butuh akses langsung, bukan OAuth penuh), API token tetap valid — tetapi harus punya masa berlaku dan pencabutan. Redesign tabel dari episode 10 menjadi skema token yang terkelola:
public function safeUp(): void
{
$this->createTable('api_tokens', [
'id' => $this->primaryKey(),
'user_id' => $this->integer()->notNull(),
'token' => $this->string(64)->notNull()->unique(),
'expires_at' => $this->integer()->notNull(),
'last_used_at' => $this->integer()->null(),
'revoked_at' => $this->integer()->null(),
]);
}class ApiToken extends ActiveRecord
{
public function isValid(): bool
{
return $this->revoked_at === null && $this->expires_at > time();
}
public function touch(): void
{
$this->last_used_at = time();
$this->updateAttributes(['last_used_at']);
}
}findIdentityByAccessToken di User sekarang memuat token dan memeriksa validitasnya:
public static function findIdentityByAccessToken($token, $type = null): ?User
{
$apiToken = ApiToken::findOne(['token' => $token]);
if ($apiToken === null || !$apiToken->isValid()) {
return null;
}
$apiToken->touch();
return static::findOne($apiToken->user_id);
}Plus kebijakan: auto-revoke token yang kedaluwarsa dengan job terjadwal (episode 12), dan daftar token per user yang bisa dicabut manual dari dashboard admin.
Semua mekanisme di atas tidak berguna jika login bisa dibrute-force. Endpoint /api/auth/login harus dilindungi lebih ketat daripada endpoint lain. Kombinasikan tiga lapis:
public function behaviors(): array
{
return [
'rateLimiter' => [
'class' => RateLimiter::class,
// login: 5 percobaan per menit per IP
'userRateLimit' => 5,
'ipRateLimit' => 10,
],
];
}public function actionLogin(): array
{
$model = new LoginForm();
$model->load(Yii::$app->request->post(), '');
if (!$model->login()) {
// beri jeda kecil untuk memperlambat brute-force
usleep(random_int(200_000, 500_000));
throw new UnauthorizedHttpException('Kredensial salah.');
}
return [
'access_token' => $this->tokenService->issue($model->getUser()),
'refresh_token' => $this->tokenService->issueRefreshToken($model->getUser()->id),
];
}Tiga lapis perlindungan: rate limit membatasi jumlah percobaan, delay acak memperlambat serangan terdistribusi, dan logging percobaan gagal (kategori app\login) memberi sinyal dini kepada monitoring. Semua data sensitif yang masuk — akses token, refresh token, dan kegagalan login — dicatat di log terenkripsi yang sesuai regulasi (episode 13 dan 19).
Inti yang harus dibawa pulang:
yii2-authclient untuk social login (client), league/oauth2-server untuk menyediakan akses (server).expires_at, revoked_at, last_used_at, dan auto-revoke.Di episode 19 selanjutnya, kita menghadapi realitas dunia nyata: CVE & update management — membedah contoh nyata CVE-2026-39850 pada Yii 2.0.55, membangun kebiasaan update rutin yang aman, dan memantau kerentanan dependensi secara berkelanjutan. Sampai jumpa di episode 19!