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
13 changes: 12 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_BOT_NAME=@your_bot_username_here

# Global Rate Limiter Configuration
RATE_LIMITER_ENABLED=false
# IMPORTANT: Keep RATE_LIMITER_ENABLED=true for production to prevent brute-force and DDoS attacks
RATE_LIMITER_ENABLED=true
RATE_LIMITER_MAX_ATTEMPTS=60
RATE_LIMITER_DECAY_MINUTES=1

# Account Lockout Configuration
# Temporary lock after repeated failed login attempts
ACCOUNT_LOCKOUT_MAX_ATTEMPTS=5
ACCOUNT_LOCKOUT_DECAY_MINUTES=15

# Progressive Delay Configuration
# Additional delay (in seconds) added after each failed attempt
PROGRESSIVE_DELAY_BASE_SECONDS=2
PROGRESSIVE_DELAY_MULTIPLIER=2
84 changes: 73 additions & 11 deletions app/Http/Controllers/Api/Auth/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,40 +34,95 @@ class AuthController extends Controller
*
* @return void
*
* @throws ValidationException
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request)
{
$credential = $request->input('credential');

// Find user to check lockout status
$user = User::where('email', $credential)
->orWhere('username', $credential)
->first();

// Check if account is locked
if ($user && $user->isLocked()) {
$remainingSeconds = $user->getLockoutRemainingSeconds();
$minutes = ceil($remainingSeconds / 60);

return response()->json([
'message' => "AKUN TERKUNCI. Terlalu banyak gagal login. Coba lagi dalam {$minutes} menit.",
'locked' => true,
'retry_after' => $remainingSeconds,
], Response::HTTP_FORBIDDEN);
}

// Check rate limiter with enhanced key (IP + User-Agent)
if (RateLimiter::tooManyAttempts($this->throttleKey(), static::MAX_ATTEMPT)) {
event(new Lockout($request));

$seconds = RateLimiter::availableIn($this->throttleKey());

return response()->json([
'message' => 'USER TELAH DIBLOKIR KARENA GAGAL LOGIN '.static::MAX_ATTEMPT.' KALI SILAKAN COBA KEMBALI DALAM 10 MENIT',
], Response::HTTP_FORBIDDEN);
'message' => 'TERLALU BANYAK PERCobaAN. Silakan tunggu ' . ceil($seconds / 60) . ' menit sebelum mencoba lagi.',
'retry_after' => $seconds,
], Response::HTTP_TOO_MANY_REQUESTS);
}

if (! Auth::attempt($request->only('email', 'password'))) {
// Record failed attempt with progressive delay and account lockout
$result = ['delay' => 0, 'locked' => false, 'attempts' => 0];

if ($user) {
$result = $user->recordFailedLogin();
}

RateLimiter::hit($this->throttleKey(), static::DECAY_SECOND);

return response()->json([
'message' => 'Invalid login details',
], Response::HTTP_UNAUTHORIZED);
$response = [
'message' => 'Kredensial tidak valid',
'attempts_remaining' => $result['remaining'] ?? null,
];

// Add progressive delay information
if ($result['delay'] > 0) {
$response['progressive_delay'] = $result['delay'];
$response['message'] = "Kredensial tidak valid. Percobaan gagal ke-{$result['attempts']}. Delay: {$result['delay']} detik.";
}

// Add lockout warning
if ($result['locked']) {
$response['message'] = "AKUN TERKUNCI. Terlalu banyak gagal login ({$result['attempts']} kali).";
$response['locked'] = true;
$response['lockout_expires_in'] = $result['lockout_expires_in'] ?? 900;
} elseif ($result['remaining'] === 0) {
$response['message'] = "PERINGATAN: Akun akan terkunci setelah {$result['attempts']} kali gagal login.";
}

return response()->json($response, Response::HTTP_UNAUTHORIZED);
}

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

// hapus token yang masih tersimpan
Auth::user()->tokens->each(function ($token, $key) {
// Reset failed login attempts on successful login
$user->resetFailedLogins();

// Clear rate limiter on successful login
RateLimiter::clear($this->throttleKey());

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

$token = $user->createToken('auth_token')->plainTextToken;
RateLimiter::clear($this->throttleKey());

return response()
->json(['message' => 'Login Success ', 'access_token' => $token, 'token_type' => 'Bearer']);
->json([
'message' => 'Login Success',
'access_token' => $token,
'token_type' => 'Bearer',
]);
}

/**
Expand All @@ -84,12 +139,19 @@ protected function logOut(Request $request)

/**
* Get the rate limiting throttle key for the request.
*
* Combines credential (email/username), IP address, and User-Agent
* to prevent bypass via VPN/IP rotation alone.
*
* @return string
*/
protected function throttleKey()
{
return Str::lower(request('credential')).'|'.request()->ip();
$credential = Str::lower(request('credential', ''));
$ip = request()->ip();
$userAgent = hash('xxh64', request()->userAgent() ?? 'unknown');

return "{$credential}|{$ip}|{$userAgent}";
}

public function token()
Expand Down
Loading