[SECURITY] Token API Harus Expire & Support Rotasi/Revocation#983
[SECURITY] Token API Harus Expire & Support Rotasi/Revocation#983pandigresik wants to merge 4 commits intorilis-devfrom
Conversation
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Log; | ||
| use Illuminate\Support\Str; | ||
| use Laravel\Sanctum\PersonalAccessToken; |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Missing Error Handling
Kategori: PHP Quality
Masalah: Method refresh() tidak memiliki try-catch untuk operasi database yang bisa gagal (create token, update, delete). Database error akan expose stack trace.
Kode: Semua operasi database di method refresh(), revoke(), revokeAll()
Fix:
public function refresh(RefreshTokenRequest $request): JsonResponse
{
try {
$refreshTokenString = $request->validated()['refresh_token'];
// ... existing validation logic
return response()->json([
'access_token' => $newToken->plainTextToken,
'refresh_token' => $newRefreshToken->refresh_token,
'token_type' => 'Bearer',
'expires_in' => config('sanctum.expiration') * 60,
]);
} catch (\Exception $e) {
\Log::error('Token refresh failed: ' . $e->getMessage());
return response()->json([
'message' => 'An error occurred while refreshing token. Please try again.'
], 500);
}
}| 'access_token' => $newAccessToken->plainTextToken, | ||
| 'refresh_token' => $newRefreshToken->refresh_token, | ||
| 'token_type' => 'Bearer', | ||
| 'expires_in' => config('sanctum.expiration') * 60, // in seconds |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing FormRequest Validation
Kategori: PHP Quality
Masalah: Method revoke() menggunakan inline validation $request->validate() instead of FormRequest. Tidak konsisten dengan method refresh() yang sudah pakai FormRequest.
Kode:
public function revoke(Request $request)
{
$request->validate([
'refresh_token' => 'required|string',
]);Fix:
// Create app/Http/Requests/RevokeRefreshTokenRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RevokeRefreshTokenRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'refresh_token' => 'required|string|size:100',
];
}
}
// Update controller
public function revoke(RevokeRefreshTokenRequest $request): JsonResponse
{
$refreshToken = RefreshToken::where('refresh_token', $request->validated()['refresh_token'])
->where('user_id', $request->user()->id)
->first();
// ... rest of code
}| use App\Http\Requests\Token\RotateTokenRequest; | ||
| use Illuminate\Http\JsonResponse; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Auth; |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Semua method tidak memiliki return type hints (index, show, revoke, revokeAll, rotate, stats).
Kode: public function index(Request $request), public function show(Request $request, $tokenId), dll.
Fix:
use Illuminate\Http\JsonResponse;
public function index(Request $request): JsonResponse
{
// ... existing code
}
public function show(Request $request, string $tokenId): JsonResponse
{
// ... existing code
}
public function revoke(RevokeTokenRequest $request): JsonResponse
{
// ... existing code
}
public function revokeAll(Request $request): JsonResponse
{
// ... existing code
}
public function rotate(RotateTokenRequest $request): JsonResponse
{
// ... existing code
}
public function stats(Request $request): JsonResponse
{
// ... existing code
}| use App\Http\Requests\Token\RotateTokenRequest; | ||
| use Illuminate\Http\JsonResponse; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Auth; |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Missing Error Handling
Kategori: PHP Quality
Masalah: Semua method tidak memiliki try-catch untuk operasi database. Method rotate() dan revokeAll() melakukan operasi delete yang bisa gagal.
Kode: Method revoke(), revokeAll(), rotate()
Fix:
public function revoke(RevokeTokenRequest $request): JsonResponse
{
try {
$tokenId = $request->validated()['token_id'];
$token = $request->user()->tokens()->find($tokenId);
if (!$token) {
return response()->json(['message' => 'Token not found'], 404);
}
if ($token->id === $request->user()->currentAccessToken()->id) {
return response()->json([
'message' => 'Cannot revoke current token. Use logout endpoint instead.'
], 400);
}
$token->delete();
activity()
->causedBy($request->user())
->withProperties([
'token_id' => $tokenId,
'token_name' => $token->name,
'ip_address' => $request->ip(),
])
->log('Token revoked');
return response()->json(['message' => 'Token revoked successfully']);
} catch (\Exception $e) {
\Log::error('Token revoke failed: ' . $e->getMessage());
return response()->json([
'message' => 'An error occurred while revoking token.'
], 500);
}
}| 'is_expired' => $token->expires_at && $token->expires_at->isPast(), | ||
| ]; | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Type Hint for Parameter
Kategori: PHP Quality
Masalah: Parameter $tokenId di method show() tidak memiliki type hint.
Kode: public function show(Request $request, $tokenId)
Fix:
public function show(Request $request, string $tokenId): JsonResponse
{
// ... existing code
}| /** | ||
| * Handle an incoming request. | ||
| * | ||
| * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method handle() tidak memiliki return type hint.
Kode: public function handle(Request $request, Closure $next)
Fix:
use Symfony\Component\HttpFoundation\Response;
public function handle(Request $request, Closure $next): Response
{
// ... existing code
}| /** | ||
| * Handle an incoming request. | ||
| * | ||
| * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Missing Error Handling
Kategori: PHP Quality
Masalah: Method handle() melakukan database update tanpa try-catch. Jika update gagal, middleware akan crash dan block request.
Kode:
$currentToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);Fix:
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
$currentToken = $user->currentAccessToken();
if (!$currentToken) {
return $next($request);
}
$ipChanged = $currentToken->ip_address && $currentToken->ip_address !== $request->ip();
$userAgentChanged = $currentToken->user_agent && $currentToken->user_agent !== $request->userAgent();
if ($ipChanged || $userAgentChanged) {
try {
$changes = [];
if ($ipChanged) {
$changes['ip_address'] = [
'old' => $currentToken->ip_address,
'new' => $request->ip(),
];
}
if ($userAgentChanged) {
$changes['user_agent'] = [
'old' => $currentToken->user_agent,
'new' => $request->userAgent(),
];
}
activity()
->causedBy($user)
->withProperties([
'token_id' => $currentToken->id,
'changes' => $changes,
])
->log('Token anomaly detected');
// Update token metadata with new values
$currentToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
} catch (\Exception $e) {
\Log::error('Token anomaly detection failed: ' . $e->getMessage());
// Continue request even if logging/update fails
}
}
return $next($request);
}| 'is_revoked', | ||
| 'revoked_at', | ||
| 'revoked_reason', | ||
| ]; |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Semua method tidak memiliki return type hints (user, isExpired, isValid, revoke).
Kode: public function user(), public function isExpired(), public function isValid(), public function revoke($reason = null)
Fix:
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function isExpired(): bool
{
return $this->expires_at->isPast();
}
public function isValid(): bool
{
return !$this->is_revoked && !$this->isExpired();
}
public function revoke(?string $reason = null): void
{
$this->update([
'is_revoked' => true,
'revoked_at' => now(),
'revoked_reason' => $reason,
]);
}|
📍 [HIGH] 📝 Code Quality: Missing Return Type Hint Kategori: PHP Quality Masalah: Method Kode: Fix: use Illuminate\Database\Eloquent\Relations\HasMany;
public function refreshTokens(): HasMany
{
return $this->hasMany(RefreshToken::class);
} |
| class RefreshTokenRequest extends FormRequest | ||
| { | ||
| /** | ||
| * Determine if the user is authorized to make this request. |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method authorize(), rules(), dan messages() tidak memiliki return type hints.
Kode: public function authorize(), public function rules(), public function messages()
Fix:
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'refresh_token' => 'required|string|size:100',
];
}
public function messages(): array
{
return [
'refresh_token.required' => 'Refresh token is required',
'refresh_token.string' => 'Refresh token must be a string',
'refresh_token.size' => 'Invalid refresh token format',
];
}| } | ||
|
|
||
| /** | ||
| * Display the specified token details. |
There was a problem hiding this comment.
[HIGH] 🔒 Security: IDOR Vulnerability - Missing Authorization Check
Masalah: Endpoint show() hanya memeriksa apakah token belongs to user, tetapi tidak ada validasi tambahan. Attacker bisa enumerate token IDs milik user lain jika mereka bisa bypass exists:personal_access_tokens,id validation.
Kode:
public function show(Request $request, $tokenId)
{
$token = $request->user()->tokens()->find($tokenId);
if (! $token) {
return response()->json([
'message' => 'Token not found',
], 404);
}
return response()->json([...]);
}Risiko: Jika validation di RevokeTokenRequest bypass atau ada bug di Laravel, attacker bisa mengakses informasi token user lain (IP address, user agent, last used time). Ini bisa digunakan untuk profiling dan social engineering attacks.
Cara Reproduksi:
# Login sebagai user A (ID: 1)
curl -X POST https://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"email":"userA@example.com","password":"password123"}'
# Response: {"access_token":"1|abc123..."}
# Coba akses token milik user B (ID: 2) dengan token user A
# Enumerate token IDs
for i in {1..100}; do
curl -X GET "https://target.com/api/tokens/$i" \
-H "Authorization: Bearer 1|abc123..." \
-H "Content-Type: application/json"
done
# Jika ada bug di validation atau query, bisa dapat info token user lainFix:
public function show(Request $request, $tokenId)
{
// Validasi tokenId adalah integer dan belongs to current user
if (!is_numeric($tokenId)) {
return response()->json(['message' => 'Invalid token ID'], 400);
}
$token = $request->user()->tokens()
->where('id', $tokenId)
->where('tokenable_id', $request->user()->id)
->where('tokenable_type', get_class($request->user()))
->first();
if (! $token) {
return response()->json([
'message' => 'Token not found',
], 404);
}
// Log access untuk audit
activity()
->causedBy($request->user())
->withProperties(['token_id' => $tokenId])
->log('Token details accessed');
return response()->json([...]);
}|
📍 [HIGH] 🔒 Security: Refresh Token Not Invalidated on Password Change Masalah: Ketika user login, semua access token dihapus tetapi refresh token tidak dihapus. Jika user melakukan password reset atau perubahan password, refresh token lama masih bisa digunakan untuk generate access token baru. Kode: // Hapus semua token lama
$user->tokens()->delete();
// Buat access token baru dengan metadata
$token = $user->createToken('api-token', ['*'], now()->addMinutes(config('sanctum.expiration')));Risiko: Attacker yang sudah mendapatkan refresh token (misalnya dari device yang dicuri atau session hijacking) masih bisa menggunakan refresh token tersebut untuk mendapatkan access token baru, bahkan setelah user mengganti password. Ini melanggar prinsip "password change should invalidate all sessions". Cara Reproduksi: # Step 1: Login dan simpan refresh token
curl -X POST https://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"email":"victim@example.com","password":"oldpassword"}'
# Response:
# {
# "access_token": "1|abc123...",
# "refresh_token": "xyz789..."
# }
# Step 2: Attacker mencuri refresh_token (misalnya dari localStorage)
# Step 3: Victim mengganti password melalui web interface
# Step 4: Attacker masih bisa gunakan refresh token yang dicuri
curl -X POST https://target.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"xyz789..."}'
# Response: SUCCESS - dapat access token baru!
# {
# "access_token": "2|newtoken123...",
# "refresh_token": "newrefresh456..."
# }
# Attacker berhasil bypass password change!Fix: public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Hapus semua token lama DAN refresh tokens
$user->tokens()->delete();
// CRITICAL: Hapus semua refresh tokens juga
RefreshToken::where('user_id', $user->id)
->whereNull('revoked_at')
->update(['revoked_at' => now()]);
// Buat access token baru dengan metadata
$token = $user->createToken('api-token', ['*'], now()->addMinutes(config('sanctum.expiration')));
// Update metadata token
$token->accessToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Buat refresh token
$refreshToken = RefreshToken::create([
'user_id' => $user->id,
'token' => hash('sha256', $plainTextRefreshToken = \Str::random(64)),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'expires_at' => now()->addDays(config('auth.refresh_token_lifetime', 30)),
]);
return response()->json([
'access_token' => $token->plainTextToken,
'refresh_token' => $plainTextRefreshToken,
'token_type' => 'Bearer',
'expires_in' => config('sanctum.expiration') * 60,
]);
}
// Tambahkan method untuk password change
public function changePassword(Request $request)
{
$request->validate([
'current_password' => 'required',
'new_password' => 'required|min:8|confirmed',
]);
$user = $request->user();
if (!Hash::check($request->current_password, $user->password)) {
throw ValidationException::withMessages([
'current_password' => ['Current password is incorrect.'],
]);
}
$user->update(['password' => Hash::make($request->new_password)]);
// CRITICAL: Revoke semua tokens dan refresh tokens
$user->tokens()->delete();
RefreshToken::where('user_id', $user->id)
->whereNull('revoked_at')
->update(['revoked_at' => now()]);
return response()->json(['message' => 'Password changed successfully']);
} |
| /** | ||
| * Refresh access token using refresh token. | ||
| * | ||
| * @param RefreshTokenRequest $request |
There was a problem hiding this comment.
[HIGH] 🔒 Security: Race Condition in Single-Use Refresh Token
Masalah: Refresh token validation dan revocation tidak atomic. Attacker bisa exploit race condition dengan mengirim multiple concurrent requests menggunakan refresh token yang sama sebelum di-revoke.
Kode:
$refreshToken = RefreshToken::where('token', $hashedToken)
->where('expires_at', '>', now())
->whereNull('revoked_at')
->first();
if (! $refreshToken) {
return response()->json([
'message' => 'Invalid or expired refresh token',
], 401);
}
// ... validasi anomali ...
// Revoke refresh token lama (single-use)
$refreshToken->update(['revoked_at' => now()]);Risiko: Dalam window waktu antara check whereNull('revoked_at') dan update(['revoked_at' => now()]), attacker bisa mengirim multiple requests concurrent. Semua request akan pass validation dan mendapatkan access token baru, melanggar prinsip single-use token.
Cara Reproduksi:
import requests
import threading
url = "https://target.com/api/auth/refresh"
refresh_token = "stolen_refresh_token_xyz123"
results = []
def exploit_race_condition():
response = requests.post(url, json={"refresh_token": refresh_token})
results.append(response.json())
# Launch 10 concurrent requests
threads = []
for i in range(10):
t = threading.Thread(target=exploit_race_condition)
threads.append(t)
t.start()
# Wait for all threads
for t in threads:
t.join()
# Check results
valid_tokens = [r for r in results if 'access_token' in r]
print(f"Got {len(valid_tokens)} valid access tokens from single refresh token!")
# Expected: 1 token (single-use)
# Actual: Multiple tokens (race condition exploited)Fix:
public function refresh(RefreshTokenRequest $request)
{
$hashedToken = hash('sha256', $request->refresh_token);
// Gunakan database transaction dengan pessimistic locking
DB::beginTransaction();
try {
// Lock row untuk prevent race condition
$refreshToken = RefreshToken::where('token', $hashedToken)
->where('expires_at', '>', now())
->whereNull('revoked_at')
->lockForUpdate() // CRITICAL: Pessimistic lock
->first();
if (! $refreshToken) {
DB::rollBack();
return response()->json([
'message' => 'Invalid or expired refresh token',
], 401);
}
// Validasi IP dan User Agent untuk deteksi anomali
if ($refreshToken->ip_address !== $request->ip() ||
$refreshToken->user_agent !== $request->userAgent()) {
activity()
->causedBy($refreshToken->user)
->withProperties([
'original_ip' => $refreshToken->ip_address,
'current_ip' => $request->ip(),
'original_user_agent' => $refreshToken->user_agent,
'current_user_agent' => $request->userAgent(),
])
->log('Token anomaly detected during refresh');
}
$user = $refreshToken->user;
// Revoke refresh token lama IMMEDIATELY (single-use)
$refreshToken->update(['revoked_at' => now()]);
// Buat access token baru
$token = $user->createToken('api-token', ['*'], now()->addMinutes(config('sanctum.expiration')));
$token->accessToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Buat refresh token baru
$newRefreshToken = RefreshToken::create([
'user_id' => $user->id,
'token' => hash('sha256', $plainTextRefreshToken = \Str::random(64)),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'expires_at' => now()->addDays(config('auth.refresh_token_lifetime', 30)),
]);
DB::commit();
return response()->json([
'access_token' => $token->plainTextToken,
'refresh_token' => $plainTextRefreshToken,
'token_type' => 'Bearer',
'expires_in' => config('sanctum.expiration') * 60,
]);
} catch (\Exception $e) {
DB::rollBack();
// Log error untuk investigation
\Log::error('Refresh token error', [
'error' => $e->getMessage(),
'ip' => $request->ip(),
]);
return response()->json([
'message' => 'An error occurred during token refresh',
], 500);
}
}| */ | ||
| public function handle(Request $request, Closure $next): Response | ||
| { | ||
| $user = $request->user(); |
There was a problem hiding this comment.
[MEDIUM] 🔒 Security: Sensitive Data Exposure in Activity Logs
Masalah: User agent dan IP address di-log tanpa sanitization. User agent bisa mengandung malicious payload atau PII yang tidak seharusnya disimpan di logs.
Kode:
activity()
->causedBy($request->user())
->withProperties([
'token_id' => $token->id,
'original_ip' => $token->ip_address,
'current_ip' => $request->ip(),
'original_user_agent' => $token->user_agent,
'current_user_agent' => $request->userAgent(),
])
->log('Token anomaly detected');Risiko:
- User agent bisa mengandung XSS payload jika log viewer tidak properly escape
- IP address adalah PII yang harus di-handle sesuai GDPR/privacy regulations
- Log bisa menjadi sangat besar jika user agent panjang (DoS pada log storage)
- Attacker bisa inject malicious data via user agent untuk exploit log analysis tools
Cara Reproduksi:
# Inject XSS payload via User-Agent
curl -X GET https://target.com/api/user \
-H "Authorization: Bearer 1|validtoken123" \
-H "User-Agent: <script>alert('XSS')</script> Mozilla/5.0" \
-H "X-Forwarded-For: 1.2.3.4"
# Inject SQL payload (jika log disimpan di database tanpa escaping)
curl -X GET https://target.com/api/user \
-H "Authorization: Bearer 1|validtoken123" \
-H "User-Agent: '; DROP TABLE activity_log; --" \
-H "X-Forwarded-For: 1.2.3.4"
# DoS via large User-Agent
curl -X GET https://target.com/api/user \
-H "Authorization: Bearer 1|validtoken123" \
-H "User-Agent: $(python -c 'print("A"*10000)')" \
-H "X-Forwarded-For: 1.2.3.4"
# Check logs - akan berisi malicious payloadFix:
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && $request->user()->currentAccessToken()) {
$token = $request->user()->currentAccessToken();
// Deteksi perubahan IP atau User Agent
if ($token->ip_address !== $request->ip() ||
$token->user_agent !== $request->userAgent()) {
// Sanitize user agent - limit length dan remove dangerous characters
$sanitizedUserAgent = $this->sanitizeUserAgent($request->userAgent());
$sanitizedOriginalUserAgent = $this->sanitizeUserAgent($token->user_agent);
// Hash IP untuk privacy (GDPR compliance)
$hashedCurrentIp = hash('sha256', $request->ip() . config('app.key'));
$hashedOriginalIp = hash('sha256', $token->ip_address . config('app.key'));
// Log anomali dengan sanitized data
activity()
->causedBy($request->user())
->withProperties([
'token_id' => $token->id,
'original_ip_hash' => $hashedOriginalIp,
'current_ip_hash' => $hashedCurrentIp,
'original_user_agent' => $sanitizedOriginalUserAgent,
'current_user_agent' => $sanitizedUserAgent,
'anomaly_type' => $this->getAnomalyType($token, $request),
])
->log('Token anomaly detected');
// Update metadata token dengan nilai terbaru
$token->update([
'ip_address' => $request->ip(),
'user_agent' => substr($request->userAgent(), 0, 500), // Limit length
]);
}
}
return $next($request);
}
/**
* Sanitize user agent untuk prevent injection attacks
*/
private function sanitizeUserAgent(?string $userAgent): string
{
if (empty($userAgent)) {
return 'Unknown';
}
// Limit length
$userAgent = substr($userAgent, 0, 500);
// Remove dangerous characters
$userAgent = preg_replace('/[<>"\']/', '', $userAgent);
// Remove control characters
$userAgent = preg_replace('/[\x00-\x1F\x7F]/', '', $userAgent);
return $userAgent;
}
/**
* Determine anomaly type untuk better categorization
*/
private function getAnomalyType($token, Request $request): string
{
$ipChanged = $token->ip_address !== $request->ip();
$uaChanged = $token->user_agent !== $request->userAgent();
if ($ipChanged && $uaChanged) {
return 'both_changed';
} elseif ($ipChanged) {
return 'ip_changed';
} else {
return 'user_agent_changed';
}
}|
📍 [MEDIUM] 🔒 Security: Missing Rate Limiting on Critical Endpoints Masalah: Endpoint Kode: // Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
// Refresh token management - NO RATE LIMITING!
Route::post('/refresh', [RefreshTokenController::class, 'refresh']);
Route::post('/refresh/revoke', [RefreshTokenController::class, 'revoke']);
Route::post('/refresh/revoke-all', [RefreshTokenController::class, 'revokeAll']);
Route::get('/refresh/tokens', [RefreshTokenController::class, 'index']);
// ...
});Risiko:
Cara Reproduksi: # Brute force refresh token
for i in {1..10000}; do
TOKEN=$(openssl rand -hex 32)
curl -X POST https://target.com/api/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$TOKEN\"}" &
done
# DoS attack - flood dengan valid requests
VALID_TOKEN="abc123xyz789..."
for i in {1..1000}; do
curl -X POST https://target.com/api/refresh \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$VALID_TOKEN\"}" &
done
# Server akan overwhelmed dengan database queriesFix: // routes/api.php
// Public routes dengan rate limiting
Route::middleware('throttle:5,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});
// Protected routes dengan rate limiting berbeda
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
// Refresh token management - STRICT rate limiting
Route::middleware('throttle:10,1')->group(function () {
Route::post('/refresh', [RefreshTokenController::class, 'refresh']);
});
Route::middleware('throttle:20,1')->group(function () {
Route::post('/refresh/revoke', [RefreshTokenController::class, 'revoke']);
Route::post('/refresh/revoke-all', [RefreshTokenController::class, 'revokeAll']);
Route::get('/refresh/tokens', [RefreshTokenController::class, 'index']);
});
// Token management - moderate rate limiting
Route::middleware('throttle:30,1')->group(function () {
Route::get('/tokens', [TokenController::class, 'index']);
Route::get('/tokens/{tokenId}', [TokenController::class, 'show']);
Route::post('/tokens/revoke', [TokenController::class, 'revoke']);
Route::post('/tokens/rotate', [TokenController::class, 'rotate']);
Route::post('/tokens/revoke-others', [TokenController::class, 'revokeOthers']);
Route::post('/tokens/revoke-all', [TokenController::class, 'revokeAll']);
});
});
// Atau gunakan custom rate limiter di RouteServiceProvider
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting()
{
RateLimiter::for('refresh', function (Request $request) {
return Limit::perMinute(10)
->by($request->ip())
->response(function () {
return response()->json([
'message' => 'Too many refresh attempts. Please try again later.',
], 429);
});
});
RateLimiter::for('token-management', function (Request $request) {
return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip());
});
} |
| } | ||
|
|
||
| return response()->json([ | ||
| 'message' => 'Berhasil logout', |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Missing Input Validation
Kategori: PHP Quality
Masalah: Method show() menerima parameter $id tanpa type hint dan tanpa validation. Sama seperti revoke(), ini membuka celah security.
Kode: public function show(Request $request, $id)
Fix:
// Use FormRequest dengan route parameter validation
public function show(ShowRefreshTokenRequest $request): JsonResponse
{
$user = $request->user();
$id = $request->validated()['id'];
$refreshToken = RefreshToken::where('id', $id)
->where('user_id', $user->id)
->first();
if (!$refreshToken) {
return response()->json([
'message' => 'Refresh token not found',
], 404);
}
return response()->json([
'token' => [
'id' => $refreshToken->id,
'ip_address' => $refreshToken->ip_address,
'user_agent' => $refreshToken->user_agent,
'created_at' => $refreshToken->created_at,
'expires_at' => $refreshToken->expires_at,
'revoked' => $refreshToken->revoked,
'is_current' => $refreshToken->ip_address === $request->ip() &&
$refreshToken->user_agent === $request->userAgent(),
],
]);
}| use Illuminate\Support\Facades\Auth; | ||
| use Laravel\Sanctum\PersonalAccessToken; | ||
|
|
||
| class TokenController extends Controller |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method index() tidak memiliki return type hint.
Kode: public function index(Request $request)
Fix:
public function index(Request $request): \Illuminate\Http\JsonResponse
{
// ... existing code
}| 'is_expired' => $token->expires_at && $token->expires_at->isPast(), | ||
| ]; | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method show() tidak memiliki return type hint.
Kode: public function show(Request $request, $id)
Fix:
public function show(Request $request, int $id): \Illuminate\Http\JsonResponse
{
// ... existing code
}| 'is_expired' => $token->expires_at && $token->expires_at->isPast(), | ||
| ]; | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Missing Input Validation
Kategori: PHP Quality
Masalah: Method show() menerima parameter $id tanpa type hint dan tanpa validation via FormRequest.
Kode: public function show(Request $request, $id)
Fix:
// Create FormRequest untuk route parameter validation
public function show(ShowTokenRequest $request): JsonResponse
{
$user = $request->user();
$id = $request->validated()['id'];
$token = $user->tokens()->find($id);
if (!$token) {
return response()->json([
'message' => 'Token not found',
], 404);
}
return response()->json([
'token' => [
'id' => $token->id,
'name' => $token->name,
'ip_address' => $token->ip_address,
'user_agent' => $token->user_agent,
'created_at' => $token->created_at,
'last_used_at' => $token->last_used_at,
'expires_at' => $token->expires_at,
'abilities' => $token->abilities,
'is_current' => $token->id === $request->user()->currentAccessToken()->id,
],
]);
}| 'user_agent' => $token->user_agent, | ||
| 'is_expired' => $token->expires_at && $token->expires_at->isPast(), | ||
| ], | ||
| ]); |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method revoke() tidak memiliki return type hint.
Kode: public function revoke(RevokeTokenRequest $request)
Fix:
public function revoke(RevokeTokenRequest $request): \Illuminate\Http\JsonResponse
{
// ... existing code
}| } | ||
|
|
||
| $token->delete(); | ||
|
|
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method revokeAll() tidak memiliki return type hint.
Kode: public function revokeAll(Request $request)
Fix:
public function revokeAll(Request $request): \Illuminate\Http\JsonResponse
{
// ... existing code
}| } | ||
|
|
||
| /** | ||
| * Rotate the specified token (revoke and create new one). |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method rotate() tidak memiliki return type hint.
Kode: public function rotate(RotateTokenRequest $request)
Fix:
public function rotate(RotateTokenRequest $request): \Illuminate\Http\JsonResponse
{
// ... existing code
}| // Update metadata for new token using forceFill since ip_address and user_agent are not fillable | ||
| $newTokenModel = PersonalAccessToken::find($newToken->accessToken->id); | ||
| $newTokenModel->forceFill([ | ||
| 'ip_address' => $request->ip(), |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method stats() tidak memiliki return type hint.
Kode: public function stats(Request $request)
Fix:
public function stats(Request $request): \Illuminate\Http\JsonResponse
{
// ... existing code
}| /** | ||
| * Handle an incoming request. | ||
| * | ||
| * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method handle() tidak memiliki return type hint yang spesifik. Seharusnya return Response atau mixed dengan proper documentation.
Kode:
public function handle(Request $request, Closure $next)
{
// ... code
}Fix:
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): \Symfony\Component\HttpFoundation\Response
{
$user = $request->user();
if (!$user) {
return $next($request);
}
$token = $request->user()->currentAccessToken();
if (!$token) {
return $next($request);
}
// Check for IP address change
$ipChanged = $token->ip_address && $token->ip_address !== $request->ip();
// Check for User Agent change
$userAgentChanged = $token->user_agent && $token->user_agent !== $request->userAgent();
if ($ipChanged || $userAgentChanged) {
// Log the anomaly
activity()
->causedBy($user)
->withProperties([
'token_id' => $token->id,
'old_ip' => $token->ip_address,
'new_ip' => $request->ip(),
'old_user_agent' => $token->user_agent,
'new_user_agent' => $request->userAgent(),
'ip_changed' => $ipChanged,
'user_agent_changed' => $userAgentChanged,
])
->log('Token anomaly detected');
// Update token metadata with new values
$token->ip_address = $request->ip();
$token->user_agent = $request->userAgent();
$token->save();
}
return $next($request);
}| /** | ||
| * Handle an incoming request. | ||
| * | ||
| * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next |
There was a problem hiding this comment.
[CRITICAL] 📝 Code Quality: Security Risk - Auto-Update Token Metadata
Kategori: Architecture
Masalah: Middleware secara otomatis meng-update IP dan User Agent token ketika terdeteksi anomali. Ini berbahaya karena jika token dicuri, attacker bisa menggunakan token dari IP berbeda dan middleware akan otomatis meng-update metadata, menghilangkan jejak pencurian. Seharusnya token di-revoke atau memerlukan re-authentication.
Kode:
if ($ipChanged || $userAgentChanged) {
// Log the anomaly
activity()->causedBy($user)->log('Token anomaly detected');
// Update token metadata with new values
$token->ip_address = $request->ip();
$token->user_agent = $request->userAgent();
$token->save();
}Fix:
if ($ipChanged || $userAgentChanged) {
// Log the anomaly
activity()
->causedBy($user)
->withProperties([
'token_id' => $token->id,
'old_ip' => $token->ip_address,
'new_ip' => $request->ip(),
'old_user_agent' => $token->user_agent,
'new_user_agent' => $request->userAgent(),
'ip_changed' => $ipChanged,
'user_agent_changed' => $userAgentChanged,
])
->log('Token anomaly detected');
// JANGAN auto-update! Revoke token dan force re-authentication
$token->delete();
return response()->json([
'message' => 'Token anomaly detected. Please login again.',
'error' => 'token_anomaly',
], 401);
}| 'is_revoked', | ||
| 'revoked_at', | ||
| 'revoked_reason', | ||
| ]; |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method user() tidak memiliki return type hint untuk relationship.
Kode: public function user()
Fix:
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}| /** | ||
| * The attributes that should be cast. | ||
| * | ||
| * @var array<string, string> |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method isExpired() tidak memiliki return type hint.
Kode: public function isExpired()
Fix:
public function isExpired(): bool
{
return $this->expires_at->isPast();
}| protected $casts = [ | ||
| 'expires_at' => 'datetime', | ||
| 'revoked_at' => 'datetime', | ||
| 'is_revoked' => 'boolean', |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Return Type Hints
Kategori: PHP Quality
Masalah: Method isValid() tidak memiliki return type hint.
Kode: public function isValid()
Fix:
public function isValid(): bool
{
return !$this->revoked && !$this->isExpired();
}|
📍 [HIGH] 📝 Code Quality: Missing Return Type Hints Kategori: PHP Quality Masalah: Method Kode: Fix: public function getActivitylogOptions(): \Spatie\Activitylog\LogOptions
{
return LogOptions::defaults()
->logOnly(['name', 'email']);
} |
|
📍 [HIGH] 📝 Code Quality: Missing Return Type Hints Kategori: PHP Quality Masalah: Method Kode: Fix: public function refreshTokens(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(RefreshToken::class);
} |
| $table->id(); | ||
| $table->foreignId('user_id')->constrained()->onDelete('cascade'); | ||
| $table->string('refresh_token', 100)->unique(); | ||
| $table->string('access_token_id')->nullable(); // Reference to personal_access_tokens |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Index for Performance
Kategori: Architecture
Masalah: Tabel refresh_tokens memiliki index composite ['user_id', 'revoked', 'expires_at'] tetapi query di RefreshTokenController::refresh() menggunakan where('token', $hashedToken) sebagai kondisi pertama. Token column harus di-index untuk performa optimal.
Kode:
$table->string('token', 64)->unique();
// ... later
$table->index(['user_id', 'revoked', 'expires_at']);Fix:
Schema::create('refresh_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('token', 64)->unique(); // unique sudah otomatis create index
$table->string('ip_address')->nullable();
$table->text('user_agent')->nullable();
$table->timestamp('expires_at');
$table->boolean('revoked')->default(false);
$table->timestamps();
// Index untuk query by user_id + status
$table->index(['user_id', 'revoked', 'expires_at']);
// Index untuk query by token + status (untuk refresh operation)
$table->index(['token', 'revoked', 'expires_at']);
});|
📍 [CRITICAL] 🐛 Bug: Null Config Value Causes Carbon TypeError Kode: Skenario: Jika Dampak: Login endpoint crash dengan 500 error, semua user tidak bisa login ke sistem Fix: $token = $user->createToken(
'auth_token',
['*'],
now()->addMinutes(config('sanctum.expiration') ?? 1440)
); |
|
📍 [HIGH] 🐛 Bug: Race Condition - Token Deletion Tanpa Transaction Kode: $user->tokens()->delete();
// ... beberapa baris kemudian ...
RefreshToken::create([...]);Skenario: Jika Dampak: User tidak bisa login dan tidak bisa refresh token, harus reset password atau admin intervention Fix: DB::transaction(function () use ($user, $request) {
$user->tokens()->delete();
$token = $user->createToken(
'auth_token',
['*'],
now()->addMinutes(config('sanctum.expiration') ?? 1440)
);
$token->accessToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
$refreshToken = RefreshToken::create([
'user_id' => $user->id,
'token' => hash('sha256', $plainTextToken = Str::random(64)),
'expires_at' => now()->addDays(30),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
return [...];
}); |
| 'ip_address' => $request->ip(), | ||
| 'user_agent' => $request->userAgent(), | ||
| 'expires_at' => now()->addMinutes(config('sanctum.expiration')), | ||
| ]); |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Null Dereference - Fatal Error Saat Token Tidak Ditemukan
Kode:
$refreshToken = RefreshToken::where('token', $hashedToken)->first();
if ($refreshToken->isExpired()) {
return response()->json(['error' => 'Refresh token expired'], 401);
}Skenario: Jika token tidak ditemukan di database (token invalid, sudah dihapus, atau typo), first() return null. Kemudian memanggil $refreshToken->isExpired() pada null menyebabkan fatal error: Call to a member function isExpired() on null
Dampak: Endpoint crash dengan 500 error setiap kali user kirim invalid refresh token. Attacker bisa DoS endpoint ini dengan spam invalid tokens.
Fix:
$refreshToken = RefreshToken::where('token', $hashedToken)->first();
if (!$refreshToken) {
return response()->json(['error' => 'Invalid refresh token'], 401);
}
if ($refreshToken->isExpired()) {
return response()->json(['error' => 'Refresh token expired'], 401);
}| 'message' => 'Server Error: ' . $e->getMessage(), | ||
| 'file' => $e->getFile(), | ||
| 'line' => $e->getLine(), | ||
| ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Null Config Value di Token Creation
Kode: now()->addMinutes(config('sanctum.expiration'))
Skenario: Sama seperti AuthController - jika config null, Carbon throw TypeError
Dampak: Refresh token endpoint crash, user tidak bisa perpanjang session
Fix:
now()->addMinutes(config('sanctum.expiration') ?? 1440)| 'message' => 'Token berhasil di-refresh', | ||
| 'data' => [ | ||
| 'access_token' => $newAccessToken->plainTextToken, | ||
| 'refresh_token' => $newRefreshToken->refresh_token, |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Null Dereference - Orphaned Refresh Token
Kode:
$user = $refreshToken->user;
// ... langsung digunakan tanpa null check ...
$newToken = $user->createToken(...);Skenario: Jika user dihapus dari database tapi refresh token masih ada (orphaned record karena tidak ada cascade delete), $refreshToken->user return null. Memanggil $user->createToken() pada null menyebabkan fatal error.
Dampak: Endpoint crash untuk orphaned tokens. User yang sudah dihapus tapi refresh token masih beredar bisa crash sistem.
Fix:
$user = $refreshToken->user;
if (!$user) {
$refreshToken->delete(); // cleanup orphaned token
return response()->json(['error' => 'User not found'], 401);
}
$newToken = $user->createToken(...);| ], | ||
| ]); | ||
| } catch (\Exception $e) { | ||
| Log::error('Refresh token error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Race Condition - Token Rotation Tanpa Transaction
Kode:
$refreshToken->delete();
$oldToken->delete();
$newToken = $user->createToken(...);
RefreshToken::create(...);Skenario: Jika salah satu operasi gagal di tengah-tengah (misal RefreshToken::create() gagal), old tokens sudah terhapus tapi new tokens tidak terbuat. User kehilangan akses total.
Dampak: User terkunci dari sistem, harus login ulang. Dalam production dengan high traffic, ini bisa terjadi sering karena DB connection timeout atau deadlock.
Fix:
DB::transaction(function () use ($refreshToken, $oldToken, $user, $request) {
$refreshToken->delete();
$oldToken->delete();
$newToken = $user->createToken(
'auth_token',
['*'],
now()->addMinutes(config('sanctum.expiration') ?? 1440)
);
$newToken->accessToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
$newRefreshToken = RefreshToken::create([...]);
return [...];
});| if (! $oldToken) { | ||
| return response()->json([ | ||
| 'message' => 'Token tidak ditemukan', | ||
| ], 404); |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Null Dereference di Token Revoke
Kode:
$token = $user->tokens()->find($request->token_id);
$token->delete();Skenario: Jika token_id tidak ditemukan atau belongs to another user (karena validation tidak cek ownership), find() return null. Memanggil delete() pada null menyebabkan fatal error.
Dampak: Endpoint crash setiap kali user coba revoke token yang tidak ada. Attacker bisa spam dengan random token_id untuk DoS.
Fix:
$token = $user->tokens()->find($request->token_id);
if (!$token) {
return response()->json(['error' => 'Token not found'], 404);
}
$token->delete();| ], | ||
| ]); | ||
| } | ||
|
|
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Null Config di Token Rotation
Kode: now()->addMinutes(config('sanctum.expiration'))
Skenario: Config null menyebabkan TypeError di Carbon
Dampak: Token rotation endpoint crash
Fix:
now()->addMinutes(config('sanctum.expiration') ?? 1440)|
|
||
| return response()->json([ | ||
| 'message' => 'Token berhasil dirotasi', | ||
| 'data' => [ |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Race Condition di Token Rotation
Kode:
$oldToken->delete();
$newToken = $user->createToken(...);
$newToken->update([...]);Skenario: Jika update() gagal setelah delete() dan createToken() berhasil, old token sudah hilang, new token ada tapi metadata (IP, user agent) salah. User punya token tapi anomaly detection akan terus trigger false alarm.
Dampak: User experience buruk - terus dapat warning anomaly padahal legitimate. Atau jika createToken() gagal, user kehilangan token.
Fix:
DB::transaction(function () use ($oldToken, $user, $request) {
$oldToken->delete();
$newToken = $user->createToken(
'auth_token',
['*'],
now()->addMinutes(config('sanctum.expiration') ?? 1440)
);
$newToken->accessToken->update([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
return [...];
});|
|
||
| // Revoke old token | ||
| $oldToken->delete(); | ||
|
|
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Null Response Instead of 404
Kode:
$token = $user->tokens()->find($request->token_id);
return response()->json(['token' => $token]);Skenario: Jika token tidak ditemukan, API return {"token": null} dengan status 200 OK. Client tidak bisa distinguish antara success dengan empty result vs token not found error.
Dampak: Client-side logic bingung - apakah token tidak ada atau API error? Menyebabkan bug di frontend/mobile app.
Fix:
$token = $user->tokens()->find($request->token_id);
if (!$token) {
return response()->json(['error' => 'Token not found'], 404);
}
return response()->json(['token' => $token]);| 'request_path' => $request->path(), | ||
| ]); | ||
| } | ||
|
|
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Null Dereference di Middleware
Kode:
$token = $request->user()->currentAccessToken();
$storedIp = $token->ip_address;Skenario: Jika currentAccessToken() return null (token invalid, expired, atau Sanctum issue), memanggil $token->ip_address pada null menyebabkan fatal error. Middleware crash = semua protected routes crash.
Dampak: CRITICAL - seluruh API yang pakai middleware ini down. Semua authenticated requests gagal dengan 500 error.
Fix:
$token = $request->user()->currentAccessToken();
if (!$token) {
return $next($request); // skip anomaly detection if no token
}
$storedIp = $token->ip_address;|
|
||
| // Log activity for security audit | ||
| try { | ||
| activity('token_anomaly') |
There was a problem hiding this comment.
[CRITICAL] 🐛 Bug: Activity Log Failure Crashes Middleware
Kode:
activity()
->causedBy($request->user())
->log('Token anomaly detected: IP changed from ' . $storedIp . ' to ' . $currentIp);Skenario: Jika Spatie Activity Log tidak terinstall, tidak configured, atau DB error saat insert log, activity() throw exception. Karena tidak ada try-catch, middleware crash dan request gagal.
Dampak: CRITICAL - middleware crash = semua protected API routes down. Satu dependency (activity log) failure menyebabkan total system failure.
Fix:
try {
activity()
->causedBy($request->user())
->log('Token anomaly detected: IP changed from ' . $storedIp . ' to ' . $currentIp);
} catch (\Exception $e) {
// Log error tapi jangan crash request
\Log::error('Failed to log token anomaly: ' . $e->getMessage());
}| } | ||
|
|
||
| // If anomalies detected, update token metadata and log activity | ||
| if (! empty($anomalies)) { |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Token Update Failure Tidak Ditangani
Kode:
$token->update([
'ip_address' => $currentIp,
'user_agent' => $currentUserAgent,
]);Skenario: Jika DB update gagal (connection timeout, deadlock, constraint violation), exception tidak ditangani. Anomaly sudah di-log tapi metadata tidak update, menyebabkan false alarm berulang di setiap request.
Dampak: User terus dapat warning anomaly di setiap request. Log spam dengan ribuan "anomaly detected" untuk user yang sama. Performance degradation karena excessive logging.
Fix:
try {
$token->update([
'ip_address' => $currentIp,
'user_agent' => $currentUserAgent,
]);
} catch (\Exception $e) {
\Log::error('Failed to update token metadata: ' . $e->getMessage());
// Continue request - jangan block user karena metadata update gagal
}
🤖 AI Code Review — PR #983📊 Review Results
Total: 54 inline comments posted. Setiap temuan sudah di-post sebagai inline comment pada file terkait. |
Perbaikan issue #965
Summary Implementasi API Token Expiration & Refresh Token
OpenKab - Laravel Sanctum Security Enhancement
Tanggal: 12 Maret 2026
Status: ✅ Completed
📋 Daftar Isi
🎯 Latar Belakang & Masalah
Masalah Awal
Token Sanctum bersifat permanent (
expiration=null)Dampak Security
Tidak Ada Refresh Token
Kebutuhan
✅ Solusi yang Diimplementasikan
1. Token Expiration
2. Refresh Token Mechanism
3. Token Metadata Tracking
4. Anomaly Detection
5. Token Management API
⚙️ Perubahan Konfigurasi
config/sanctum.phpSebelum:
'expiration' => null(tidak pernah expired)Sesudah:
'expiration' => 1440(24 jam)config/auth.php.env(Optional - untuk override)🗄️ Database Changes
Migration 1: Add Metadata to Personal Access Tokens
File:
database/migrations/2026_03_12_085615_add_metadata_to_personal_access_tokens_table.phpKolom Baru:
ip_address(string, 45) - IP address saat token dibuatuser_agent(text) - User agent browser/deviceMigration 2: Create Refresh Tokens Table
File:
database/migrations/2026_03_12_100843_create_refresh_tokens_table.phpKolom:
user_id- Foreign key ke users tablerefresh_token- Random string 100 karakteraccess_token_id- Reference ke personal_access_tokensip_address- IP saat refresh token dibuatuser_agent- User agent saat refresh token dibuatexpires_at- Waktu expired (30 hari dari creation)is_revoked- Flag apakah token sudah dicabutrevoked_at- Waktu pencabutanrevoked_reason- Alasan pencabutan ('logout', 'token_refresh', 'security')📦 New Models
app/Models/RefreshToken.phpapp/Models/User.php(Updated)Tambah relasi refreshTokens:
🎮 Controllers Updates
app/Http/Controllers/Api/Auth/AuthController.phpPerubahan pada method
login():Response Login Baru:
{ "message": "Login Success", "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", "refresh_token": "random_100_char_string_xyz", "token_type": "Bearer", "expires_in": 86400, "refresh_expires_in": 2592000 }app/Http/Controllers/Api/TokenController.php(New)Endpoints:
index()- List all user tokensshow($tokenId)- Get token detailsrevoke()- Revoke specific tokenrotate()- Rotate token (revoke + create new)revokeAll()- Revoke all except currentrevokeAllIncludingCurrent()- Revoke all tokensContoh method
rotate():app/Http/Controllers/Api/RefreshTokenController.php(New)Endpoints:
refresh()- Refresh access token using refresh tokenrevoke()- Logout single devicerevokeAll()- Logout all devicesMethod
refresh():🛡️ Middleware
app/Http/Middleware/DetectTokenAnomaly.php(New)Fungsi: Deteksi anomali penggunaan token (IP/device berubah)
Logic:
app/Http/Middleware/AuthApiOptional.php(New)Fungsi: Middleware hybrid untuk API yang bisa diakses via session ATAU token
Use Case: Dashboard admin yang mengakses API endpoint
app/Http/Kernel.php(Updated)Tambah middleware aliases:
🛣️ Routes
routes/apiv1.php(Updated)🔌 API Endpoints
Authentication
/api/v1/signin/api/v1/logoutToken Management
/api/v1/tokens/api/v1/tokens/{id}/api/v1/tokens/revoke/api/v1/tokens/rotate/api/v1/tokens/revoke-all/api/v1/tokens/revoke-all-including-currentRefresh Token
/api/v1/refresh-token/refresh/api/v1/refresh-token/revoke/api/v1/refresh-token/revoke-allHybrid Auth (Session OR Token)
/api/v1/identitas/api/v1/identitas/perbarui/{id}Legend:
auth:sanctum🔒 Security Features
1. Token Expiration
sanctum.expirationauth.refresh_token_lifetime2. Refresh Token Security
3. Anomaly Detection
activity_logtable4. Token Management
5. Response Security
{ "message": "Login Success", "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", "refresh_token": "random_100_char_string", "token_type": "Bearer", "expires_in": 86400, // Client tahu kapan expired "refresh_expires_in": 2592000 // Client tahu kapan harus login ulang }🧪 Testing
Test Files Created
tests/Feature/TokenManagementTest.php(15 tests) ✅ PASSINGtest_token_expiration_is_configured()test_login_returns_token_with_expiration()test_token_metadata_captured_on_creation()test_list_user_tokens()test_show_token_details()test_revoke_token()test_rotate_token()test_revoke_all_tokens()test_revoke_all_including_current()test_token_expiration_date()test_is_expired_flag()tests/Feature/TokenAnomalyDetectionTest.php(6 tests) ✅ PASSINGtest_middleware_detects_ip_change()test_middleware_detects_user_agent_change()test_middleware_no_anomaly_when_metadata_matches()test_activity_log_created_for_anomaly()test_middleware_works_without_stored_metadata()test_middleware_updates_last_used_at()tests/Feature/RefreshTokenTest.php(4 tests) ⏸️ SKIPPEDTotal: 21 tests passing, 4 tests skipped (need manual testing)
Run Tests
📦 Migration Guide
Step 1: Backup Database
Step 2: Run Migrations
cd /path/to/OpenKab php artisan migrateOutput yang diharapkan:
Step 3: Clear Config Cache
Step 4: Verify Configuration
Step 5: Test Login
Expected Response:
{ "message": "Login Success", "access_token": "...", "refresh_token": "...", "token_type": "Bearer", "expires_in": 86400, "refresh_expires_in": 2592000 }Step 6: Test Refresh Token
📊 Impact Analysis
Admin Dashboard
authmiddleware)SESSION_LIFETIME(120 menit default)auth.apimiddleware (hybrid)API Clients (Mobile/External)
Existing Tokens
🔧 Troubleshooting
Issue: Refresh token tidak bekerja
Check:
Solution: Run migration
Issue: Token tidak expired setelah 24 jam
Check:
php artisan tinker >>> config('sanctum.expiration')Solution: Clear config cache
Issue: Anomaly detection tidak log
Check:
storage/logs/laravel.logSolution: Verify middleware registered
php artisan route:list --path=api/v1/user # Should show middleware: auth:sanctum,token.anomaly📝 Checklist Deployment
php artisan migrate)php artisan config:clear)📚 References