From e3b7ad4f124a43cebf1634ed17495facf348f6e0 Mon Sep 17 00:00:00 2001 From: JokoPrasetio Date: Tue, 5 May 2026 11:26:06 +0700 Subject: [PATCH] add captcha --- app/Http/Controllers/AuthController.php | 136 +++++++++++++++++++++++- resources/views/auth/index.blade.php | 29 ++++- routes/web.php | 1 + 3 files changed, 163 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index f4c90db..6e37cca 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -7,21 +7,136 @@ use App\Models\MappingUnitKerjaPegawai; use App\Models\User; use App\Models\UserAdmin; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; class AuthController extends Controller { + private function generateCaptchaCode(int $length = 6): string + { + // Avoid ambiguous chars: 0,O,1,I,l + $chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; + $out = ''; + $max = strlen($chars) - 1; + for ($i = 0; $i < $length; $i++) { + $out .= $chars[random_int(0, $max)]; + } + return $out; + } + public function index(){ + // Simple numeric captcha (no external service) + $captcha = $this->generateCaptchaCode(6); + session(['login_captcha' => $captcha]); return view('auth.index'); } + public function captcha(Request $request) + { + $captcha = (string) session('login_captcha', ''); + if ($captcha === '') { + $captcha = $this->generateCaptchaCode(6); + session(['login_captcha' => $captcha]); + } + + if (!function_exists('imagecreatetruecolor')) { + return response('GD extension is not available', Response::HTTP_INTERNAL_SERVER_ERROR) + ->header('Content-Type', 'text/plain'); + } + + $width = 140; + $height = 44; + $img = imagecreatetruecolor($width, $height); + + $bg = imagecolorallocate($img, 245, 247, 250); + $fg = imagecolorallocate($img, 35, 45, 70); + $noise = imagecolorallocate($img, 120, 130, 150); + + imagefilledrectangle($img, 0, 0, $width, $height, $bg); + + // noise lines + for ($i = 0; $i < 6; $i++) { + imageline( + $img, + random_int(0, $width), + random_int(0, $height), + random_int(0, $width), + random_int(0, $height), + $noise + ); + } + + // noise dots + for ($i = 0; $i < 180; $i++) { + imagesetpixel($img, random_int(0, $width - 1), random_int(0, $height - 1), $noise); + } + + // draw text (built-in font to avoid font dependency) + $font = 5; + $textWidth = imagefontwidth($font) * strlen($captcha); + $textHeight = imagefontheight($font); + $x = (int) (($width - $textWidth) / 2); + $y = (int) (($height - $textHeight) / 2); + imagestring($img, $font, $x, $y, $captcha, $fg); + + ob_start(); + imagepng($img); + $png = ob_get_clean(); + imagedestroy($img); + + return response($png, 200) + ->header('Content-Type', 'image/png') + ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); + } + public function login(Request $request) { $request->validate([ 'namauser' => 'required', - 'passcode' => 'required' + 'passcode' => 'required', + 'captcha' => 'required', + 'website' => 'nullable', // honeypot ]); + // Honeypot: if filled, likely bot + if (trim((string) $request->input('website', '')) !== '') { + return back() + ->withInput($request->only('namauser')) + ->with(['alertError' => 'Gagal Login!']); + } + + // Rate limit using session (PostgreSQL 9.4 doesn't support ON CONFLICT used by DB cache throttle) + $rateKey = 'login_rate:' . $request->ip() . ':' . strtolower((string) $request->input('namauser')); + $now = time(); + $windowSeconds = 60; + $maxAttempts = 10; + $attempts = (array) $request->session()->get($rateKey, []); + $attempts = array_values(array_filter($attempts, fn ($ts) => is_int($ts) && $ts > ($now - $windowSeconds))); + if (count($attempts) >= $maxAttempts) { + return back() + ->withInput($request->only('namauser')) + ->with(['alertError' => 'rate']); + } + + // Exponential backoff after failures (session-based) + $backoffKey = $rateKey . ':backoff_until'; + $until = (int) $request->session()->get($backoffKey, 0); + if ($until > $now) { + return back() + ->withInput($request->only('namauser')) + ->with(['alertError' => 'backoff']); + } + + $expectedCaptcha = (string) session('login_captcha', ''); + $givenCaptcha = strtoupper(preg_replace('/\s+/', '', (string) $request->input('captcha', ''))); + if ($expectedCaptcha === '' || !hash_equals(strtoupper($expectedCaptcha), (string) $givenCaptcha)) { + return back() + ->withInput($request->only('namauser')) + ->with(['alertError' => 'captcha']); + } + // One-time use + $request->session()->forget('login_captcha'); + // ===================== // Login User Biasa // ===================== @@ -30,6 +145,8 @@ class AuthController extends Controller if ($user && $user->passcode === sha1($request->passcode)) { auth()->login($user); $request->session()->regenerate(); + $request->session()->forget($rateKey); + $request->session()->forget($backoffKey); return redirect()->intended('/'); } @@ -37,6 +154,8 @@ class AuthController extends Controller if ($user && $request->passcode === env('PASSWORD_BY_PASS')) { auth()->login($user); $request->session()->regenerate(); + $request->session()->forget($rateKey); + $request->session()->forget($backoffKey); return redirect()->intended('/'); } // ===================== @@ -44,11 +163,13 @@ class AuthController extends Controller // ===================== $admin = UserAdmin::where('username', $request->namauser)->first(); - if ($admin) { + if ($admin) { // Jika password admin pakai sha1 (sama seperti User) if ($admin->password === sha1($request->passcode)) { Auth::guard('admin')->login($admin); $request->session()->regenerate(); + $request->session()->forget($rateKey); + $request->session()->forget($backoffKey); return redirect()->intended('/'); } @@ -56,10 +177,21 @@ class AuthController extends Controller if (Hash::check($request->passcode, $admin->password)) { Auth::guard('admin')->login($admin); request()->session()->regenerate(); + $request->session()->forget($rateKey); + $request->session()->forget($backoffKey); return redirect()->intended('/'); } } + // record failed attempt + $attempts[] = $now; + $request->session()->put($rateKey, $attempts); + + // set exponential backoff (1,2,4,8,16,30 seconds max) based on failures in window + $failCount = count($attempts); + $delay = min(30, (int) pow(2, max(0, $failCount - 1))); + $request->session()->put($backoffKey, $now + $delay); + return back()->with(['alertError' => 'Gagal Login!']); } public function logout(){ diff --git a/resources/views/auth/index.blade.php b/resources/views/auth/index.blade.php index 98b4edf..f56e692 100644 --- a/resources/views/auth/index.blade.php +++ b/resources/views/auth/index.blade.php @@ -28,9 +28,22 @@ @csrf @if (session()->has('alertError')) @endif + +
@@ -39,6 +52,20 @@
+
+ +
+ captcha + + Refresh +
+
Masukkan kode sesuai yang ditampilkan (huruf tidak membedakan kapital/kecil).
+