Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions app/Http/Controllers/Api/Auth/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use App\Models\RefreshToken;
use App\Models\User;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Http\Request;
Expand All @@ -11,6 +12,7 @@
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Laravel\Sanctum\PersonalAccessToken;

class AuthController extends Controller
{
Expand Down Expand Up @@ -104,24 +106,45 @@ public function login(Request $request)

$user = User::where('email', $request['email'])->firstOrFail();

// Reset failed login attempts on successful login
$user->resetFailedLogins();
// Revoke all existing tokens for this user
$user->tokens()->delete();
$user->refreshTokens()->delete();

// Create new access token with metadata
$newToken = $user->createToken('auth_token');

// Clear rate limiter on successful login
// Update token with metadata (IP, user agent, expiration)
$tokenModel = PersonalAccessToken::find($newToken->accessToken->id);
$tokenModel->forceFill([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'expires_at' => now()->addMinutes(config('sanctum.expiration')),
]);
$tokenModel->save();

// Create refresh token
$refreshToken = RefreshToken::create([
'user_id' => $user->id,
'refresh_token' => Str::random(100),
'access_token_id' => $tokenModel->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'expires_at' => now()->addDays(config('auth.refresh_token_lifetime_days', 30)),
]);

$token = $newToken->plainTextToken;
RateLimiter::clear($this->throttleKey());

// Delete existing tokens
$user->tokens->each(function ($token, $key) {
$token->delete();
});

$token = $user->createToken('auth_token')->plainTextToken;
// Reset failed login attempts on successful login
$user->resetFailedLogins();

return response()
->json([
'message' => 'Login Success',
'access_token' => $token,
'refresh_token' => $refreshToken->refresh_token,
'token_type' => 'Bearer',
'expires_in' => config('sanctum.expiration') * 60, // in seconds
'refresh_expires_in' => config('auth.refresh_token_lifetime', 2592000), // 30 days in seconds
]);
}

Expand Down Expand Up @@ -157,8 +180,22 @@ protected function throttleKey()
public function token()
{
$user = User::whereUsername('synchronize')->first();
$token = $user->createToken('auth_token', ['synchronize-opendk-create'])->plainTextToken;
$newToken = $user->createToken('auth_token', ['synchronize-opendk-create']);

// Update token with metadata using forceFill
$tokenModel = PersonalAccessToken::find($newToken->accessToken->id);
$tokenModel->forceFill([
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'expires_at' => now()->addMinutes(config('sanctum.expiration')),
]);
$tokenModel->save();

return response()->json(['message' => 'Token Synchronize', 'access_token' => $token, 'token_type' => 'Bearer']);
return response()->json([
'message' => 'Token Synchronize',
'access_token' => $newToken->plainTextToken,
'token_type' => 'Bearer',
'expires_in' => config('sanctum.expiration') * 60,
]);
}
}
204 changes: 204 additions & 0 deletions app/Http/Controllers/Api/RefreshTokenController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\RefreshTokenRequest;
use App\Models\RefreshToken;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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);
    }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] โšก Performance: Query Tanpa Index pada Token Hash

Masalah: Query RefreshToken::where('token', $hashedToken) akan lambat jika tidak ada index pada kolom token. Token adalah SHA256 hash (64 karakter) yang di-query setiap refresh request.

Kode:

$refreshToken = RefreshToken::where('token', $hashedToken)
    ->where('expires_at', '>', now())
    ->whereNull('revoked_at')
    ->first();

Dampak: Tanpa index, full table scan pada tabel dengan 100K+ refresh tokens = 50-200ms per query. Dengan index: <5ms.

Fix:

// Tambahkan di migration: database/migrations/2026_03_12_100843_create_refresh_tokens_table.php
Schema::create('refresh_tokens', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('token', 64)->unique(); // PENTING: unique() membuat index
    $table->string('ip_address')->nullable();
    $table->text('user_agent')->nullable();
    $table->timestamp('expires_at');
    $table->timestamp('revoked_at')->nullable();
    $table->timestamps();
    
    // Composite index untuk query optimization
    $table->index(['token', 'expires_at', 'revoked_at']);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ๐Ÿ“ Code Quality: Missing Return Type Hints

Kategori: PHP Quality

Masalah: Method refresh() tidak memiliki return type hint.

Kode: public function refresh(RefreshTokenRequest $request)

Fix:

public function refresh(RefreshTokenRequest $request): \Illuminate\Http\JsonResponse
{
    // ... existing code
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ๐Ÿ“ Code Quality: Fat Controller - Complex Business Logic

Kategori: Architecture

Masalah: Method refresh() memiliki 70+ baris dengan business logic kompleks: validasi token, anomaly detection, token revocation, dan creation. Ini melanggar Single Responsibility dan sulit di-maintain.

Kode:

public function refresh(RefreshTokenRequest $request)
{
    $hashedToken = hash('sha256', $request->refresh_token);
    
    $refreshToken = RefreshToken::where('token', $hashedToken)
        ->where('expires_at', '>', now())
        ->where('revoked', false)
        ->first();

    if (!$refreshToken) {
        return response()->json([
            'message' => 'Invalid or expired refresh token',
        ], 401);
    }

    // Check if IP or User Agent changed (anomaly detection)
    $ipChanged = $refreshToken->ip_address !== $request->ip();
    $userAgentChanged = $refreshToken->user_agent !== $request->userAgent();

    if ($ipChanged || $userAgentChanged) {
        // Log anomaly
        activity()
            ->causedBy($refreshToken->user)
            ->withProperties([...])
            ->log('Token anomaly detected during refresh');
    }
    // ... 40+ more lines
}

Fix:

// Create app/Services/RefreshTokenService.php
namespace App\Services;

class RefreshTokenService
{
    public function validateAndRefresh(string $refreshToken, Request $request): array
    {
        $hashedToken = hash('sha256', $refreshToken);
        
        $token = RefreshToken::where('token', $hashedToken)
            ->where('expires_at', '>', now())
            ->where('revoked', false)
            ->first();

        if (!$token) {
            throw new \Exception('Invalid or expired refresh token');
        }

        $this->detectAndLogAnomaly($token, $request);
        
        $user = $token->user;
        
        // Revoke used token
        $token->update(['revoked' => true]);
        
        return $this->createNewTokens($user, $request);
    }

    private function detectAndLogAnomaly(RefreshToken $token, Request $request): void
    {
        $ipChanged = $token->ip_address !== $request->ip();
        $userAgentChanged = $token->user_agent !== $request->userAgent();

        if ($ipChanged || $userAgentChanged) {
            activity()
                ->causedBy($token->user)
                ->withProperties([
                    'old_ip' => $token->ip_address,
                    'new_ip' => $request->ip(),
                    'old_user_agent' => $token->user_agent,
                    'new_user_agent' => $request->userAgent(),
                    'token_id' => $token->id,
                ])
                ->log('Token anomaly detected during refresh');
        }
    }

    private function createNewTokens(User $user, Request $request): array
    {
        // Token creation logic
    }
}

// In Controller
public function refresh(RefreshTokenRequest $request, RefreshTokenService $service): JsonResponse
{
    try {
        $tokens = $service->validateAndRefresh($request->refresh_token, $request);
        return response()->json($tokens);
    } catch (\Exception $e) {
        return response()->json(['message' => $e->getMessage()], 401);
    }
}


class RefreshTokenController extends Controller
{
/**
* Refresh access token using refresh token.
*
* @param RefreshTokenRequest $request
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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);
    }
}

* @return JsonResponse
*/
public function refresh(RefreshTokenRequest $request): JsonResponse
{
try {
$refreshTokenString = $request->validated('refresh_token');

// Find the refresh token
$refreshToken = RefreshToken::where('refresh_token', $refreshTokenString)->first();

if (! $refreshToken) {
return response()->json([
'message' => 'Refresh token tidak ditemukan',
], JsonResponse::HTTP_NOT_FOUND);
}

// Check if refresh token is valid
if (! $refreshToken->isValid()) {
if ($refreshToken->is_revoked) {
return response()->json([
'message' => 'Refresh token telah dicabut. Silakan login ulang.',
], JsonResponse::HTTP_FORBIDDEN);
}

if ($refreshToken->isExpired()) {
return response()->json([
'message' => 'Refresh token telah expired. Silakan login ulang.',
], JsonResponse::HTTP_FORBIDDEN);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] โšก Performance: N+1 Query - Missing Eager Loading

Masalah: Query User::find($refreshToken->user_id) dipanggil setelah mendapatkan $refreshToken, padahal relasi user sudah tersedia di model. Ini menyebabkan 1 query tambahan yang tidak perlu.

Kode:

$user = User::find($refreshToken->user_id);

Dampak: +1 query per refresh request. Dengan 100K refresh/hari = 100K query tidak perlu. Estimasi: 0.5-2ms overhead per request.

Fix:

// Gunakan relasi yang sudah ada
$user = $refreshToken->user;

// Atau eager load sejak awal (line 13-16):
$refreshToken = RefreshToken::with('user')
    ->where('token', $hashedToken)
    ->where('expires_at', '>', now())
    ->whereNull('revoked_at')
    ->first();

}
}

// Get the user
$user = $refreshToken->user;

if (! $user || ! $user->active) {
return response()->json([
'message' => 'User tidak ditemukan atau tidak aktif',
], JsonResponse::HTTP_FORBIDDEN);
}

// Revoke old refresh token (single use)
$refreshToken->revoke('token_refresh');

// Revoke old access token if exists
if ($refreshToken->access_token_id) {
PersonalAccessToken::find($refreshToken->access_token_id)?->delete();
}

// Create new access token
$newAccessToken = $user->createToken('auth_token');

// Update access token with metadata
$newAccessTokenModel = PersonalAccessToken::find($newAccessToken->accessToken->id);
$newAccessTokenModel->forceFill([
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'expires_at' => now()->addMinutes(config('sanctum.expiration')),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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);
}

$newAccessTokenModel->save();

// Create new refresh token
$newRefreshToken = $this->createRefreshToken($user, $newAccessTokenModel->id, $request);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
}


return response()->json([
'message' => 'Token berhasil di-refresh',
'data' => [
'access_token' => $newAccessToken->plainTextToken,
'refresh_token' => $newRefreshToken->refresh_token,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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(...);

'token_type' => 'Bearer',
'expires_in' => config('sanctum.expiration') * 60, // in seconds
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
}

'refresh_expires_in' => config('auth.refresh_token_lifetime', 2592000), // 30 days in seconds
],
]);
} catch (\Exception $e) {
Log::error('Refresh token error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 [...];
});

Log::error('Stack trace: ' . $e->getTraceAsString());

return response()->json([
'message' => 'Server Error: ' . $e->getMessage(),
'file' => $e->getFile(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] โšก Performance: Missing Pagination

Masalah: Method index() mengambil semua refresh tokens tanpa pagination. User dengan banyak device/session bisa memiliki puluhan hingga ratusan token.

Kode:

$refreshTokens = RefreshToken::where('user_id', $request->user()->id)
    ->whereNull('revoked_at')
    ->where('expires_at', '>', now())
    ->orderBy('created_at', 'desc')
    ->get();

Dampak: User dengan 50+ tokens = 50+ rows di-load sekaligus. Memory spike dan response time lambat. Estimasi: 10-50ms overhead untuk 50 tokens.

Fix:

$refreshTokens = RefreshToken::where('user_id', $request->user()->id)
    ->whereNull('revoked_at')
    ->where('expires_at', '>', now())
    ->orderBy('created_at', 'desc')
    ->paginate(20); // atau simplePaginate(20)

return response()->json($refreshTokens);

'line' => $e->getLine(),
], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
}

}

/**
* Create a new refresh token.
*
* @param User $user
* @param int $accessTokenId
* @param Request $request
* @return RefreshToken
*/
private function createRefreshToken(User $user, int $accessTokenId, Request $request): RefreshToken
{
return RefreshToken::create([
'user_id' => $user->id,
'refresh_token' => Str::random(100),
'access_token_id' => $accessTokenId,
'ip_address' => $request->ip() ?? '0.0.0.0',
'user_agent' => $request->userAgent() ?? 'Unknown',
'expires_at' => now()->addDays(config('auth.refresh_token_lifetime_days', 30)),
'is_revoked' => false,
]);
}

/**
* Revoke refresh token (logout).
*
* @param RefreshTokenRequest $request
* @return JsonResponse
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ๐Ÿ“ Code Quality: Missing Return Type Hints

Kategori: PHP Quality

Masalah: Method revoke() tidak memiliki return type hint.

Kode: public function revoke(Request $request, $id)

Fix:

public function revoke(Request $request, int $id): \Illuminate\Http\JsonResponse
{
    // ... existing code
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ๐Ÿ“ Code Quality: Missing Input Validation

Kategori: PHP Quality

Masalah: Method revoke() menerima parameter $id tanpa type hint dan tanpa validation via FormRequest. Ini membuka celah security untuk invalid input.

Kode: public function revoke(Request $request, $id)

Fix:

// Create app/Http/Requests/RefreshToken/RevokeRefreshTokenRequest.php
namespace App\Http\Requests\RefreshToken;

use Illuminate\Foundation\Http\FormRequest;

class RevokeRefreshTokenRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'id' => 'required|integer|exists:refresh_tokens,id',
        ];
    }

    protected function prepareForValidation(): void
    {
        $this->merge([
            'id' => $this->route('id'),
        ]);
    }
}

// In Controller
public function revoke(RevokeRefreshTokenRequest $request): JsonResponse
{
    $user = $request->user();
    $id = $request->validated()['id'];
    
    $refreshToken = RefreshToken::where('id', $id)
        ->where('user_id', $user->id)
        ->where('revoked', false)
        ->first();

    if (!$refreshToken) {
        return response()->json([
            'message' => 'Refresh token not found',
        ], 404);
    }

    $refreshToken->update(['revoked' => true]);

    return response()->json([
        'message' => 'Refresh token revoked successfully',
    ]);
}

*/
public function revoke(RefreshTokenRequest $request): JsonResponse
{
try {
$refreshTokenString = $request->validated('refresh_token');
$refreshToken = RefreshToken::where('refresh_token', $refreshTokenString)->first();

if (! $refreshToken) {
return response()->json([
'message' => 'Refresh token tidak ditemukan',
], JsonResponse::HTTP_NOT_FOUND);
}

// Revoke the refresh token
$refreshToken->revoke('logout');

// Revoke associated access token
if ($refreshToken->access_token_id) {
PersonalAccessToken::find($refreshToken->access_token_id)?->delete();
}

return response()->json([
'message' => 'Berhasil logout',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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(),
        ],
    ]);
}

]);
} catch (\Exception $e) {
Log::error('Revoke refresh token error: ' . $e->getMessage());

return response()->json([
'message' => 'Server Error: ' . $e->getMessage(),
], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
}
}

/**
* Revoke all refresh tokens for user (logout from all devices).
*
* @param Request $request
* @return JsonResponse
*/
public function revokeAll(Request $request): JsonResponse
{
$user = $request->user();

if (! $user) {
return response()->json([
'message' => 'User tidak terautentikasi',
], JsonResponse::HTTP_UNAUTHORIZED);
}

// Revoke all refresh tokens
$count = $user->refreshTokens()->update([
'is_revoked' => true,
'revoked_at' => now(),
'revoked_reason' => 'logout_all_devices',
]);

// Revoke all access tokens
$user->tokens()->delete();

// Log activity
activity('token')
->causedBy($user)
->withProperties([
'revoked_count' => $count,
'action' => 'revoke_all_refresh_tokens',
])
->log('Semua refresh token dicabut (logout dari semua perangkat)');

return response()->json([
'message' => "Berhasil logout dari {$count} perangkat",
]);
}
}
Loading
Loading