From 538a11cccdde01625c8b6fca3764be4873bdab6f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 13 Sep 2026 05:18:51 -0600 Subject: [PATCH] Update LoginController --- app/Http/Controllers/Auth/LoginController.php | 331 ++++++++++++++---- 1 file changed, 257 insertions(+), 74 deletions(-) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 54a931635..4961ff084 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -111,6 +111,12 @@ class LoginController extends Controller } $user = $this->guard()->getLastAttempted(); + + if (! $user || $this->loginBlocked($user)) { + $this->incrementLoginAttempts($request); + $this->sendFailedLoginResponse($request); + } + $provider = $this->guard()->getProvider(); if (method_exists($provider, 'rehashPasswordIfRequired')) { @@ -120,9 +126,16 @@ class LoginController extends Controller $remember = $request->boolean('remember'); if ((bool) $user->{'2fa_enabled'}) { - PendingLoginService::start($request, $user, $remember, PendingLoginService::STEP_2FA); - - return redirect()->route('login', ['step' => PendingLoginService::STEP_2FA]); + PendingLoginService::start( + $request, + $user, + $remember, + PendingLoginService::STEP_2FA + ); + + return redirect()->route('login', [ + 'step' => PendingLoginService::STEP_2FA, + ]); } return $this->completeLogin($request, $user, $remember); @@ -143,10 +156,19 @@ class LoginController extends Controller } if (PendingLoginService::verifyCode($user, (string) $request->input('code'))) { - return $this->completeLogin($request, $user, PendingLoginService::remember($request)); + return $this->completeLogin( + $request, + $user, + PendingLoginService::remember($request) + ); } - $this->log($request, $user, 'auth.2fa.failed', '2FA verification failed'); + $this->log( + $request, + $user, + 'auth.2fa.failed', + '2FA verification failed' + ); if (PendingLoginService::recordFailure($request)) { return redirect()->route('login')->withErrors([ @@ -156,14 +178,20 @@ class LoginController extends Controller $remaining = PendingLoginService::attemptsRemaining($request); - $params = ['step' => PendingLoginService::STEP_2FA]; + $params = [ + 'step' => PendingLoginService::STEP_2FA, + ]; if ($request->input('mode') === 'backup') { $params['mode'] = 'backup'; } return redirect()->route('login', $params)->withErrors([ - 'code' => __('Invalid code.').' '.trans_choice('{1} :count attempt left|[2,*] :count attempts left', $remaining, ['count' => $remaining]), + 'code' => __('Invalid code.') . ' ' . trans_choice( + '{1} :count attempt left|[2,*] :count attempts left', + $remaining, + ['count' => $remaining] + ), ]); } @@ -183,12 +211,20 @@ class LoginController extends Controller } if ($this->requiresEmailVerification($user)) { - return redirect()->route('login', ['step' => PendingLoginService::STEP_VERIFY])->withErrors([ - 'verify' => __('Your email is not verified yet. Open the link we sent, or resend it below.'), - ]); - } - - return $this->completeLogin($request, $user, PendingLoginService::remember($request)); + return redirect() + ->route('login', [ + 'step' => PendingLoginService::STEP_VERIFY, + ]) + ->withErrors([ + 'verify' => __('Your email is not verified yet. Open the link we sent, or resend it below.'), + ]); + } + + return $this->completeLogin( + $request, + $user, + PendingLoginService::remember($request) + ); } public function resendVerification(Request $request): Response @@ -200,17 +236,31 @@ class LoginController extends Controller } if (! $this->requiresEmailVerification($user)) { - return $this->completeLogin($request, $user, PendingLoginService::remember($request)); + return $this->completeLogin( + $request, + $user, + PendingLoginService::remember($request) + ); } if (! EmailVerificationService::send($user)) { - return redirect()->route('login', ['step' => PendingLoginService::STEP_VERIFY])->withErrors([ - 'verify' => __('A verification email was sent a moment ago. Check your inbox, then try again in a minute.'), - ]); - } - - return redirect()->route('login', ['step' => PendingLoginService::STEP_VERIFY]) - ->with('status', __('Verification email sent to').' '.$user->email); + return redirect() + ->route('login', [ + 'step' => PendingLoginService::STEP_VERIFY, + ]) + ->withErrors([ + 'verify' => __('A verification email was sent a moment ago. Check your inbox, then try again in a minute.'), + ]); + } + + return redirect() + ->route('login', [ + 'step' => PendingLoginService::STEP_VERIFY, + ]) + ->with( + 'status', + __('Verification email sent to') . ' ' . $user->email + ); } public function updatePendingEmail(Request $request): Response @@ -222,7 +272,11 @@ class LoginController extends Controller } if (! $this->requiresEmailVerification($user)) { - return $this->completeLogin($request, $user, PendingLoginService::remember($request)); + return $this->completeLogin( + $request, + $user, + PendingLoginService::remember($request) + ); } $request->validate([ @@ -233,35 +287,64 @@ class LoginController extends Controller $user->email_verified_at = null; $user->save(); - PendingLoginService::start($request, $user, PendingLoginService::remember($request), PendingLoginService::STEP_VERIFY); + PendingLoginService::start( + $request, + $user, + PendingLoginService::remember($request), + PendingLoginService::STEP_VERIFY + ); - $this->log($request, $user, 'account.email.changed', 'Email address changed before verification'); + $this->log( + $request, + $user, + 'account.email.changed', + 'Email address changed before verification' + ); EmailVerificationService::send($user); - return redirect()->route('login', ['step' => PendingLoginService::STEP_VERIFY]) - ->with('status', __('Verification email sent to').' '.$user->email); + return redirect() + ->route('login', [ + 'step' => PendingLoginService::STEP_VERIFY, + ]) + ->with( + 'status', + __('Verification email sent to') . ' ' . $user->email + ); } /** * Token-only confirmation, safe for guests. Completes the pending login * when the confirming browser is the one waiting on this address. */ - public function confirmEmail(Request $request, string $userToken, string $randomToken): Response - { - $user = EmailVerificationService::confirm($userToken, $randomToken); + public function confirmEmail( + Request $request, + string $userToken, + string $randomToken + ): Response { + $user = EmailVerificationService::confirm( + $userToken, + $randomToken + ); if (! $user) { if ($request->user() !== null) { return redirect($this->redirectPath()); } - return redirect()->route('login')->withErrors([ - 'login' => __('That verification link is invalid or has expired. Sign in to request a new one.'), - ]); + return redirect() + ->route('login') + ->withErrors([ + 'login' => __('That verification link is invalid or has expired. Sign in to request a new one.'), + ]); } - $this->log($request, $user, 'account.email.verified', 'Email address verified'); + $this->log( + $request, + $user, + 'account.email.verified', + 'Email address verified' + ); if ($request->user() !== null) { return redirect($this->redirectPath()); @@ -271,27 +354,65 @@ class LoginController extends Controller if ($pending && (int) $pending->id === (int) $user->id) { if (PendingLoginService::step($request) === PendingLoginService::STEP_VERIFY) { - return $this->completeLogin($request, $user, PendingLoginService::remember($request)); + return $this->completeLogin( + $request, + $user, + PendingLoginService::remember($request) + ); } - return redirect()->route('login', ['step' => PendingLoginService::step($request)]) + return redirect() + ->route('login', [ + 'step' => PendingLoginService::step($request), + ]) ->with('status', __('Email verified.')); } - return redirect()->route('login')->with('status', __('Email verified. Sign in to continue.')); + return redirect() + ->route('login') + ->with('status', __('Email verified. Sign in to continue.')); } /** * Final gate. Either parks the user on the verify step or creates the * session and hands off to sendLoginResponse (which consumes url.intended). */ - protected function completeLogin(Request $request, User $user, bool $remember): Response - { - if ($this->requiresEmailVerification($user)) { - PendingLoginService::start($request, $user, $remember, PendingLoginService::STEP_VERIFY); - EmailVerificationService::send($user, EmailVerificationService::AUTO_SEND_COOLDOWN_SECONDS); + protected function completeLogin( + Request $request, + User $user, + bool $remember + ): Response { + /* + * Defense-in-depth: completeLogin() can be reached from multiple + * authentication flows, including 2FA and email verification. + * + * Never allow an account that has become blocked during a pending + * login to receive an authenticated session. + */ + if ($this->loginBlocked($user)) { + PendingLoginService::clear($request); + + throw ValidationException::withMessages([ + $this->username() => [trans('auth.failed')], + ]); + } - return redirect()->route('login', ['step' => PendingLoginService::STEP_VERIFY]); + if ($this->requiresEmailVerification($user)) { + PendingLoginService::start( + $request, + $user, + $remember, + PendingLoginService::STEP_VERIFY + ); + + EmailVerificationService::send( + $user, + EmailVerificationService::AUTO_SEND_COOLDOWN_SECONDS + ); + + return redirect()->route('login', [ + 'step' => PendingLoginService::STEP_VERIFY, + ]); } $this->guard()->login($user, $remember); @@ -303,9 +424,27 @@ class LoginController extends Controller return $response; } + /** + * Determine whether the account is prohibited from authenticating. + */ + protected function loginBlocked(User $user): bool + { + return in_array( + $user->status, + [ + 'delete', + 'deleted', + 'suspend', + 'suspended', + ], + true + ); + } + protected function requiresEmailVerification(User $user): bool { - return (bool) config('pixelfed.enforce_email_verification') && is_null($user->email_verified_at); + return (bool) config('pixelfed.enforce_email_verification') + && is_null($user->email_verified_at); } protected function pendingUser(Request $request, string $step): ?User @@ -317,15 +456,20 @@ class LoginController extends Controller return PendingLoginService::user($request); } - protected function redirectToPendingStep(Request $request): RedirectResponse - { + protected function redirectToPendingStep( + Request $request + ): RedirectResponse { if ($step = PendingLoginService::step($request)) { - return redirect()->route('login', ['step' => $step]); + return redirect()->route('login', [ + 'step' => $step, + ]); } - return redirect()->route('login')->withErrors([ - 'login' => __('Your sign-in session expired. Please sign in again.'), - ]); + return redirect() + ->route('login') + ->withErrors([ + 'login' => __('Your sign-in session expired. Please sign in again.'), + ]); } /** @@ -336,29 +480,46 @@ class LoginController extends Controller { $intended = $request->session()->get('url.intended'); - if (! is_string($intended) || parse_url($intended, PHP_URL_PATH) !== '/oauth/authorize') { + if ( + ! is_string($intended) || + parse_url($intended, PHP_URL_PATH) !== '/oauth/authorize' + ) { return null; } - parse_str((string) parse_url($intended, PHP_URL_QUERY), $query); + parse_str( + (string) parse_url($intended, PHP_URL_QUERY), + $query + ); - if (empty($query['client_id']) || ! is_string($query['client_id'])) { + if ( + empty($query['client_id']) || + ! is_string($query['client_id']) + ) { return null; } try { - $name = Passport::client()->newQuery()->whereKey($query['client_id'])->value('name'); + $name = Passport::client() + ->newQuery() + ->whereKey($query['client_id']) + ->value('name'); } catch (\Throwable) { return null; } - return is_string($name) && $name !== '' ? $name : null; + return is_string($name) && $name !== '' + ? $name + : null; } protected function bounce(Request $request): void { if (config('pixelfed.bouncer.cloud_ips.ban_logins')) { - abort_if(BouncerService::checkIp($request->ip()), 404); + abort_if( + BouncerService::checkIp($request->ip()), + 404 + ); } } @@ -368,15 +529,18 @@ class LoginController extends Controller */ protected function throttleKey(Request $request): string { - $email = $request->input($this->username()) ?: PendingLoginService::email($request); + $email = $request->input($this->username()) + ?: PendingLoginService::email($request); - return Str::transliterate(Str::lower((string) $email).'|'.$request->ip()); + return Str::transliterate( + Str::lower((string) $email) . '|' . $request->ip() + ); } /** * Validate the user login request. * - * @param Request $request + * @param Request $request */ public function validateLogin($request): void { @@ -386,6 +550,7 @@ class LoginController extends Controller $this->username() => 'required|email', 'password' => 'required|string|min:6', ]; + $messages = []; if ( @@ -394,28 +559,34 @@ class LoginController extends Controller ( (bool) config_cache('captcha.triggers.login.enabled') && request()->session()->has('login_attempts') && - request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts') + request()->session()->get('login_attempts') >= + config('captcha.triggers.login.attempts') ) ) { - $rules['h-captcha-response'] = 'required|filled|captcha|min:5'; - $messages['h-captcha-response.required'] = 'The captcha must be filled'; + $rules['h-captcha-response'] = + 'required|filled|captcha|min:5'; + + $messages['h-captcha-response.required'] = + 'The captcha must be filled'; } + $request->validate($rules, $messages); } /** * The user has been authenticated. * - * @param Request $request - * @param mixed $user + * @param Request $request + * @param mixed $user */ protected function authenticated($request, $user): void { - if ($user->status == 'deleted') { - return; - } - - $this->log($request, $user, 'auth.login', 'Account Login'); + $this->log( + $request, + $user, + 'auth.login', + 'Account Login' + ); } /** @@ -423,14 +594,22 @@ class LoginController extends Controller * * @throws ValidationException */ - protected function sendFailedLoginResponse(Request $request): void - { + protected function sendFailedLoginResponse( + Request $request + ): void { if (config('captcha.triggers.login.enabled')) { if ($request->session()->has('login_attempts')) { $ct = $request->session()->get('login_attempts'); - $request->session()->put('login_attempts', $ct + 1); + + $request->session()->put( + 'login_attempts', + $ct + 1 + ); } else { - $request->session()->put('login_attempts', 1); + $request->session()->put( + 'login_attempts', + 1 + ); } } @@ -439,8 +618,12 @@ class LoginController extends Controller ]); } - protected function log(Request $request, User $user, string $action, string $message): void - { + protected function log( + Request $request, + User $user, + string $action, + string $message + ): void { $log = new AccountLog; $log->user_id = $user->id; $log->item_id = $user->id;