Update LoginController

pull/7243/head
Daniel Supernault 1 week ago
parent 96acfb3d91
commit 538a11cccd
No known key found for this signature in database
GPG Key ID: 23740873EE6F76A1

@ -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;

Loading…
Cancel
Save