mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2024-11-21 16:38:36 -06:00
Add warning when the user fails to use MFA for a few times in a row. https://github.com/firefly-iii/firefly-iii/issues/9183
This commit is contained in:
parent
1e472ee095
commit
ec60194110
45
app/Events/Security/MFAManyFailedAttempts.php
Normal file
45
app/Events/Security/MFAManyFailedAttempts.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFA.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Events\Security;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MFAManyFailedAttempts extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public User $user;
|
||||
public int $count;
|
||||
|
||||
public function __construct(null|Authenticatable|User $user, int $count)
|
||||
{
|
||||
if ($user instanceof User) {
|
||||
$this->user = $user;
|
||||
}
|
||||
$this->count = $count;
|
||||
}
|
||||
}
|
@ -27,12 +27,14 @@ use FireflyIII\Events\Security\DisabledMFA;
|
||||
use FireflyIII\Events\Security\EnabledMFA;
|
||||
use FireflyIII\Events\Security\MFABackupFewLeft;
|
||||
use FireflyIII\Events\Security\MFABackupNoLeft;
|
||||
use FireflyIII\Events\Security\MFAManyFailedAttempts;
|
||||
use FireflyIII\Events\Security\MFANewBackupCodes;
|
||||
use FireflyIII\Events\Security\MFAUsedBackupCode;
|
||||
use FireflyIII\Notifications\Security\DisabledMFANotification;
|
||||
use FireflyIII\Notifications\Security\EnabledMFANotification;
|
||||
use FireflyIII\Notifications\Security\MFABackupFewLeftNotification;
|
||||
use FireflyIII\Notifications\Security\MFABackupNoLeftNotification;
|
||||
use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification;
|
||||
use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification;
|
||||
use FireflyIII\Notifications\Security\NewBackupCodesNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
@ -115,6 +117,32 @@ class MFAHandler
|
||||
}
|
||||
}
|
||||
|
||||
public function sendMFAFailedAttemptsMail(MFAManyFailedAttempts $event): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now in %s', __METHOD__));
|
||||
|
||||
$user = $event->user;
|
||||
$count = $event->count;
|
||||
|
||||
try {
|
||||
Notification::send($user, new MFAManyFailedAttemptsNotification($user, $count));
|
||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Bcc')) {
|
||||
app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (str_contains($message, 'RFC 2822')) {
|
||||
app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.');
|
||||
|
||||
return;
|
||||
}
|
||||
app('log')->error($e->getMessage());
|
||||
app('log')->error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function sendBackupNoLeftMail(MFABackupNoLeft $event): void
|
||||
{
|
||||
|
@ -25,6 +25,7 @@ namespace FireflyIII\Http\Controllers\Auth;
|
||||
|
||||
use FireflyIII\Events\Security\MFABackupFewLeft;
|
||||
use FireflyIII\Events\Security\MFABackupNoLeft;
|
||||
use FireflyIII\Events\Security\MFAManyFailedAttempts;
|
||||
use FireflyIII\Events\Security\MFAUsedBackupCode;
|
||||
use FireflyIII\Http\Controllers\Controller;
|
||||
use FireflyIII\User;
|
||||
@ -76,10 +77,26 @@ class TwoFactorController extends Controller
|
||||
/** @var Authenticator $authenticator */
|
||||
$authenticator = app(Authenticator::class)->boot($request);
|
||||
|
||||
// if not OK, save error.
|
||||
if (!$authenticator->isAuthenticated()) {
|
||||
$user = auth()->user();
|
||||
$this->addToMFAFailureCounter();
|
||||
$counter = $this->getMFAFailureCounter();
|
||||
if (3 === $counter || 10 === $counter) {
|
||||
// do not reset MFA failure counter, but DO send a warning to the user.
|
||||
Log::channel('audit')->info(sprintf('User "%s" has had %d failed MFA attempts.', $user->email, $counter));
|
||||
event(new MFAManyFailedAttempts($user, $counter));
|
||||
}
|
||||
unset($user);
|
||||
}
|
||||
|
||||
if ($authenticator->isAuthenticated()) {
|
||||
// save MFA in preferences
|
||||
$this->addToMFAHistory($mfaCode);
|
||||
|
||||
// reset failure count
|
||||
$this->resetMFAFailureCounter();
|
||||
|
||||
// otp auth success!
|
||||
return redirect(route('home'));
|
||||
}
|
||||
@ -89,6 +106,9 @@ class TwoFactorController extends Controller
|
||||
$this->removeFromBackupCodes($mfaCode);
|
||||
$authenticator->login();
|
||||
|
||||
// reset failure count
|
||||
$this->resetMFAFailureCounter();
|
||||
|
||||
session()->flash('info', trans('firefly.mfa_backup_code'));
|
||||
// send user notification.
|
||||
$user = auth()->user();
|
||||
@ -185,13 +205,13 @@ class TwoFactorController extends Controller
|
||||
$newList = array_values(array_diff($list, [$mfaCode]));
|
||||
|
||||
// if the list is 3 or less, send a notification.
|
||||
if(count($newList) <= 3 && count($newList) > 0) {
|
||||
if (count($newList) <= 3 && count($newList) > 0) {
|
||||
$user = auth()->user();
|
||||
Log::channel('audit')->info(sprintf('User "%s" has used a backup code. They have %d backup codes left.', $user->email, count($newList)));
|
||||
event(new MFABackupFewLeft($user, count($newList)));
|
||||
}
|
||||
// if the list is empty, send notification
|
||||
if(0 === count($newList)) {
|
||||
if (0 === count($newList)) {
|
||||
$user = auth()->user();
|
||||
Log::channel('audit')->info(sprintf('User "%s" has used their last backup code.', $user->email));
|
||||
event(new MFABackupNoLeft($user));
|
||||
@ -199,4 +219,25 @@ class TwoFactorController extends Controller
|
||||
|
||||
app('preferences')->set('mfa_recovery', $newList);
|
||||
}
|
||||
|
||||
private function addToMFAFailureCounter(): void
|
||||
{
|
||||
$preference = (int) app('preferences')->get('mfa_failure_count', 0)->data;
|
||||
$preference++;
|
||||
Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference));
|
||||
app('preferences')->set('mfa_failure_count', $preference);
|
||||
}
|
||||
|
||||
private function getMFAFailureCounter(): int
|
||||
{
|
||||
$value = (int) app('preferences')->get('mfa_failure_count', 0)->data;
|
||||
Log::channel('audit')->info(sprintf('MFA failure count is %d.', $value));
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function resetMFAFailureCounter(): void
|
||||
{
|
||||
app('preferences')->set('mfa_failure_count', 0);
|
||||
Log::channel('audit')->info('MFA failure count is set to zero.');
|
||||
}
|
||||
}
|
||||
|
119
app/Notifications/Security/MFAManyFailedAttemptsNotification.php
Normal file
119
app/Notifications/Security/MFAManyFailedAttemptsNotification.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/*
|
||||
* EnabledMFANotification.php
|
||||
* Copyright (c) 2024 james@firefly-iii.org.
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Notifications\Security;
|
||||
|
||||
use FireflyIII\Support\Notifications\UrlValidator;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MFAManyFailedAttemptsNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private User $user;
|
||||
private int $count;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(User $user, int $count)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return MailMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$subject = (string)trans('email.mfa_many_failed_subject', ['count' => $this->count]);
|
||||
|
||||
return (new MailMessage())->markdown('emails.security.many-failed-attempts', ['user' => $this->user, 'count' => $this->count])->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return SlackMessage
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function toSlack($notifiable)
|
||||
{
|
||||
$message = (string)trans('email.mfa_many_failed_slack', ['email' => $this->user->email, 'count' => $this->count]);
|
||||
|
||||
return (new SlackMessage())->content($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
/** @var null|User $user */
|
||||
$user = auth()->user();
|
||||
$slackUrl = null === $user ? '' : app('preferences')->getForUser(auth()->user(), 'slack_webhook_url', '')->data;
|
||||
if (is_array($slackUrl)) {
|
||||
$slackUrl = '';
|
||||
}
|
||||
if (UrlValidator::isValidWebhookURL((string)$slackUrl)) {
|
||||
return ['mail', 'slack'];
|
||||
}
|
||||
|
||||
return ['mail'];
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ use FireflyIII\Events\Security\DisabledMFA;
|
||||
use FireflyIII\Events\Security\EnabledMFA;
|
||||
use FireflyIII\Events\Security\MFABackupFewLeft;
|
||||
use FireflyIII\Events\Security\MFABackupNoLeft;
|
||||
use FireflyIII\Events\Security\MFAManyFailedAttempts;
|
||||
use FireflyIII\Events\Security\MFANewBackupCodes;
|
||||
use FireflyIII\Events\Security\MFAUsedBackupCode;
|
||||
use FireflyIII\Events\StoredAccount;
|
||||
@ -231,6 +232,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
MFABackupNoLeft::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendBackupNoLeftMail',
|
||||
],
|
||||
MFAManyFailedAttempts::class => [
|
||||
'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAFailedAttemptsMail',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -169,5 +169,11 @@ return [
|
||||
'no_backup_codes_intro' => 'You (:email) have used ALL of your backup codes. Please generate new ones as soon as possible.',
|
||||
'no_backup_codes_warning' => 'Without backup codes, you cannot recover your MFA login if you lose access to your code generator.',
|
||||
|
||||
// many failed MFA attempts
|
||||
'mfa_many_failed_subject' => 'You have tried and failed to use multi-factor authentication :count times now!',
|
||||
'mfa_many_failed_slack' => 'You (:email) have tried and failed to use multi-factor authentication :count times now. Is this not correct? Check your settings!',
|
||||
'mfa_many_failed_attempts_intro' => 'You (:email) have tried :count times to use a multi-factor authentication code, but these login attempts have failed. Are you sure you are using the right MFA code? Are you sure the time on the server is correct?',
|
||||
'mfa_many_failed_attempts_warning' => 'If you did not do this, please contact your administrator immediately or check out the Firefly III documentation.',
|
||||
|
||||
];
|
||||
// Ignore this comment
|
||||
|
@ -0,0 +1,6 @@
|
||||
@component('mail::message')
|
||||
{{ trans('email.mfa_many_failed_attempts_intro', ['email' => $user->email, 'count' => $count]) }}
|
||||
|
||||
{{ trans('email.mfa_many_failed_attempts_warning') }}
|
||||
|
||||
@endcomponent
|
Loading…
Reference in New Issue
Block a user