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
133 changes: 86 additions & 47 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use App\Services\OtpService;
use App\Services\TwoFactorService;
Expand All @@ -14,35 +15,14 @@

class LoginController extends Controller
{
protected $decayMinutes = 3;
use AuthenticatesUsers;

protected $decayMinutes = 3;
protected $maxAttempts = 5;

protected $otpService;
protected $twoFactorService;

/**
* Create a new controller instance.
*/
public function __construct(OtpService $otpService, TwoFactorService $twoFactorService)
{
$this->middleware('guest')->except('logout');
$this->otpService = $otpService;
$this->twoFactorService = $twoFactorService;
$this->username = $this->findUsername();
}
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/

use AuthenticatesUsers;
protected $username;

/**
* Where to redirect users after login.
Expand All @@ -52,12 +32,17 @@ public function __construct(OtpService $otpService, TwoFactorService $twoFactorS
protected $redirectTo = RouteServiceProvider::HOME;

/**
* Login username to be used by the controller.
*
* @var string
* Create a new controller instance.
*/
protected $username;

public function __construct(
OtpService $otpService,
TwoFactorService $twoFactorService
) {
$this->middleware('guest')->except('logout');
$this->otpService = $otpService;
$this->twoFactorService = $twoFactorService;
$this->username = $this->findUsername();
}

/**
* Get the login username to be used by the controller.
Expand All @@ -67,11 +52,8 @@ public function __construct(OtpService $otpService, TwoFactorService $twoFactorS
public function findUsername()
{
$login = request()->input('login');

$fieldType = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';

request()->merge([$fieldType => $login]);

return $fieldType;
}

Expand All @@ -86,12 +68,69 @@ public function username()
}

/**
* Attempt to log the user into the application.
* Check if user account is locked before attempting login.
*
* @return bool
* @param \Illuminate\Http\Request $request
* @throws \Illuminate\Validation\ValidationException
*/
protected function checkAccountLockout(Request $request)
{
$login = $request->input('login');
$fieldType = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';

$user = User::where($fieldType, $login)->first();

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

throw ValidationException::withMessages([
$this->username() => "AKUN TERKUNCI. Terlalu banyak gagal login. Coba lagi dalam {$minutes} menit.",
]);
}

return $user;
}

/**
* Record failed login attempt with account lockout.
*
* @param \Illuminate\Http\Request $request
*/
protected function recordFailedLoginAttempt(Request $request)
{
$login = $request->input('login');
$fieldType = filter_var($login, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';

$user = User::where($fieldType, $login)->first();

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

if ($result['locked']) {
$minutes = ceil($result['lockout_expires_in'] / 60);
$message = "AKUN TERKUNCI. Terlalu banyak gagal login ({$result['attempts']} kali). Coba lagi dalam {$minutes} menit.";
} elseif ($result['remaining'] === 0) {
$message = "PERINGATAN: Akun akan terkunci setelah {$result['attempts']} kali gagal login.";
} else {
$message = "Kredensial tidak valid. Percobaan gagal ke-{$result['attempts']}. Delay: {$result['delay']} detik.";
}

throw ValidationException::withMessages([
$this->username() => $message,
]);
}

$this->incrementLoginAttempts($request);
}

/**
* Override to add account lockout check and password validation.
*/
protected function attemptLogin(Request $request)
{
$this->checkAccountLockout($request);

$successLogin = $this->guard()->attempt(
$this->credentials($request), $request->boolean('remember')
);
Expand All @@ -104,19 +143,19 @@ protected function attemptLogin(Request $request)
->numbers()
->symbols()
->uncompromised(),
],
]);
]]);
session(['weak_password' => false]);
} catch (ValidationException $th) {
} catch (ValidationException $th) {
session(['weak_password' => true]);

return redirect(route('password.change'))->with('success-login', 'Ganti password dengan yang lebih kuat');
}
}
} else {
$this->recordFailedLoginAttempt($request);
}

return $successLogin;
}

/**
* Send the response after the user was authenticated.
*
Expand All @@ -126,18 +165,18 @@ protected function attemptLogin(Request $request)
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();

$this->clearLoginAttempts($request);

// Check if user has 2FA enabled

$user = $this->guard()->user();
if ($user) {
$user->resetFailedLogins();
}

if ($this->twoFactorService->hasTwoFactorEnabled($user)) {
session()->forget('2fa_verified');
// If 2FA is enabled, redirect to 2FA challenge
return redirect()->route('2fa.challenge');
}

// If weak password, redirect to password change

if (session('weak_password')) {
return redirect(route('password.change'))->with('success-login', 'Ganti password dengan yang lebih kuat');
}
Expand Down
Loading