Unknown user warning.

This commit is contained in:
James Cole 2024-12-12 07:09:17 +01:00
parent d995bfc081
commit 0e5eb036b0
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
11 changed files with 252 additions and 23 deletions

View File

@ -0,0 +1,39 @@
<?php
/*
* UnknownUserAttemptedLogin.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 Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class UnknownUserAttemptedLogin
{
use SerializesModels;
public string $address;
public function __construct(string $address)
{
$this->address = $address;
}
}

View File

@ -25,7 +25,9 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\NewVersionAvailable;
use FireflyIII\Events\Security\UnknownUserAttemptedLogin;
use FireflyIII\Events\Test\TestNotificationChannel;
use FireflyIII\Notifications\Admin\UnknownUserLoginAttempt;
use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
@ -41,6 +43,28 @@ use Illuminate\Support\Facades\Notification;
*/
class AdminEventHandler
{
public function sendLoginAttemptNotification(UnknownUserAttemptedLogin $event): void {
try {
$owner = new OwnerNotifiable();
Notification::send($owner, new UnknownUserLoginAttempt($event->address));
} 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 sendInvitationNotification(InvitationCreated $event): void
{
$sendMail = app('fireflyconfig')->get('notification_invite_created', true)->data;

View File

@ -25,9 +25,12 @@ namespace FireflyIII\Http\Controllers\Auth;
use Cookie;
use FireflyIII\Events\ActuallyLoggedIn;
use FireflyIII\Events\Security\UnknownUserAttemptedLogin;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Providers\RouteServiceProvider;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
@ -57,6 +60,7 @@ class LoginController extends Controller
* Where to redirect users after login.
*/
protected string $redirectTo = RouteServiceProvider::HOME;
private UserRepositoryInterface $repository;
private string $username;
@ -68,6 +72,7 @@ class LoginController extends Controller
parent::__construct();
$this->username = 'email';
$this->middleware('guest')->except('logout');
$this->repository = app(UserRepositoryInterface::class);
}
/**
@ -122,6 +127,11 @@ class LoginController extends Controller
return $this->sendLoginResponse($request);
}
app('log')->warning('Login attempt failed.');
$username = (string) $request->get($this->username());
if(null === $this->repository->findByEmail($username)) {
// send event to owner.
event(new UnknownUserAttemptedLogin($username));
}
// Copied directly from AuthenticatesUsers, but with logging added:
// If the login attempt was unsuccessful we will increment the number of attempts

View File

@ -25,7 +25,6 @@ namespace FireflyIII\Http\Controllers;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use FireflyIII\Events\NewVersionAvailable;
use FireflyIII\Events\RequestedVersionCheckStatus;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
@ -63,8 +62,8 @@ class HomeController extends Controller
*/
public function dateRange(Request $request): JsonResponse
{
$stringStart = '';
$stringEnd = '';
$stringStart = '';
$stringEnd = '';
try {
$stringStart = e((string) $request->get('start'));
@ -99,7 +98,7 @@ class HomeController extends Controller
app('log')->debug('Range is now marked as "custom".');
}
$diff = $start->diffInDays($end, true) + 1;
$diff = $start->diffInDays($end, true) + 1;
if ($diff > 366) {
$request->session()->flash('warning', (string) trans('firefly.warning_much_data', ['days' => (int) $diff]));
@ -154,13 +153,13 @@ class HomeController extends Controller
}
/** @var Carbon $start */
$start = session('start', today(config('app.timezone'))->startOfMonth());
$start = session('start', today(config('app.timezone'))->startOfMonth());
/** @var Carbon $end */
$end = session('end', today(config('app.timezone'))->endOfMonth());
$accounts = $repository->getAccountsById($frontpageArray);
$today = today(config('app.timezone'));
$accounts = $accounts->sortBy('order'); // sort frontpage accounts by order
$end = session('end', today(config('app.timezone'))->endOfMonth());
$accounts = $repository->getAccountsById($frontpageArray);
$today = today(config('app.timezone'));
$accounts = $accounts->sortBy('order'); // sort frontpage accounts by order
app('log')->debug('Frontpage accounts are ', $frontpageArray);
@ -170,14 +169,14 @@ class HomeController extends Controller
// collect groups for each transaction.
foreach ($accounts as $account) {
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->withAccountInformation()->setRange($start, $end)->setLimit(10)->setPage(1);
$set = $collector->getExtractedJournals();
$transactions[] = ['transactions' => $set, 'account' => $account];
}
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
event(new RequestedVersionCheckStatus($user));
return view('index', compact('count', 'subTitle', 'transactions', 'billCount', 'start', 'end', 'today', 'pageTitle'));
@ -188,11 +187,11 @@ class HomeController extends Controller
$subTitle = (string) trans('firefly.welcome_back');
$pageTitle = (string) trans('firefly.main_dashboard_page_title');
$start = session('start', today(config('app.timezone'))->startOfMonth());
$end = session('end', today(config('app.timezone'))->endOfMonth());
$start = session('start', today(config('app.timezone'))->startOfMonth());
$end = session('end', today(config('app.timezone'))->endOfMonth());
/** @var User $user */
$user = auth()->user();
$user = auth()->user();
event(new RequestedVersionCheckStatus($user));
return view('index', compact('subTitle', 'start', 'end', 'pageTitle'));

View File

@ -111,9 +111,10 @@ class PreferencesController extends Controller
// notification preferences (single value for each):
$notifications = [];
die('fix the reference to the available notifications.');
foreach (config('firefly.available_notifications') as $notification) {
$notifications[$notification] = app('preferences')->get(sprintf('notification_%s', $notification), true)->data;
foreach (config('notifications.notifications.user') as $key => $info) {
if($info['enabled']) {
$notifications[$key] = app('preferences')->get(sprintf('notification_%s', $key), true)->data;
}
}
ksort($languages);

View File

@ -0,0 +1,128 @@
<?php
/*
* UnknownUserLoginAttempt.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\Admin;
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
use FireflyIII\Notifications\ReturnsAvailableChannels;
use FireflyIII\Notifications\ReturnsSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use NotificationChannels\Pushover\PushoverMessage;
use Ntfy\Message;
class UnknownUserLoginAttempt extends Notification
{
use Queueable;
private string $address;
/**
* Create a new notification instance.
*/
public function __construct(string $address)
{
$this->address = $address;
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
*
* @return array
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function toArray(OwnerNotifiable $notifiable)
{
return [
];
}
/**
* Get the mail representation of the notification.
*
* @return MailMessage
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function toMail(OwnerNotifiable $notifiable): MailMessage
{
return new MailMessage()
->markdown('emails.owner.unknown-user', ['address' => $this->address])
->subject((string) trans('email.unknown_user_subject'));
}
/**
* Get the Slack representation of the notification.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function toSlack(OwnerNotifiable $notifiable): SlackMessage
{
return new SlackMessage()->content(
(string) trans('email.unknown_user_body', ['address' => $this->address])
);
}
public function toPushover(OwnerNotifiable $notifiable): PushoverMessage
{
return PushoverMessage::create((string) trans('email.unknown_user_message', ['address' => $this->address]))
->title((string) trans('email.unknown_user_subject'));
}
public function toNtfy(OwnerNotifiable $notifiable): Message
{
$settings = ReturnsSettings::getSettings('ntfy', 'owner', null);
// overrule config.
config(['ntfy-notification-channel.server' => $settings['ntfy_server']]);
config(['ntfy-notification-channel.topic' => $settings['ntfy_topic']]);
if ($settings['ntfy_auth']) {
// overrule auth as well.
config(['ntfy-notification-channel.authentication.enabled' => true]);
config(['ntfy-notification-channel.authentication.username' => $settings['ntfy_user']]);
config(['ntfy-notification-channel.authentication.password' => $settings['ntfy_pass']]);
}
$message = new Message();
$message->topic($settings['ntfy_topic']);
$message->title((string) trans('email.unknown_user_subject'));
$message->body((string) trans('email.unknown_user_message', ['address' => $this->address]));
return $message;
}
/**
* Get the notification's delivery channels.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function via(OwnerNotifiable $notifiable)
{
return ReturnsAvailableChannels::returnChannels('owner');
}
}

View File

@ -47,6 +47,7 @@ use FireflyIII\Events\Security\MFABackupNoLeft;
use FireflyIII\Events\Security\MFAManyFailedAttempts;
use FireflyIII\Events\Security\MFANewBackupCodes;
use FireflyIII\Events\Security\MFAUsedBackupCode;
use FireflyIII\Events\Security\UnknownUserAttemptedLogin;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\Test\TestNotificationChannel;
@ -146,6 +147,9 @@ class EventServiceProvider extends ServiceProvider
'FireflyIII\Handlers\Events\AdminEventHandler@sendInvitationNotification',
'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationInvite',
],
UnknownUserAttemptedLogin::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendLoginAttemptNotification',
],
// is a Transaction Journal related event.
StoredTransactionGroup::class => [

View File

@ -31,11 +31,18 @@ return [
],
'notifications' => [
'user' => [
'some_notification' => [
'enabled' => true,
'email' => '',
'slack' => '',
],
'bill_reminder' => ['enabled' => true, 'configurable' => true],
'new_access_token' => ['enabled' => true, 'configurable' => true],
'transaction_creation' => ['enabled' => true, 'configurable' => true],
'user_login' => ['enabled' => true, 'configurable' => true],
'rule_action_failures' => ['enabled' => true, 'configurable' => true],
'new_password' => ['enabled' => true, 'configurable' => false],
'enabled_mfa' => ['enabled' => true, 'configurable' => false],
'disabled_mfa' => ['enabled' => true, 'configurable' => false],
'few_left_mfa' => ['enabled' => true, 'configurable' => false],
'no_left_mfa' => ['enabled' => true, 'configurable' => false],
'many_failed_mfa' => ['enabled' => true, 'configurable' => false],
'new_backup_codes' => ['enabled' => true, 'configurable' => false],
],
'owner' => [
//'invitation_created' => ['enabled' => true],
@ -45,6 +52,7 @@ return [
'new_version' => ['enabled' => true],
'invite_created' => ['enabled' => true],
'invite_redeemed' => ['enabled' => true],
'unknown_user_attempt' => ['enabled' => true],
],
],
// // notifications

View File

@ -61,6 +61,11 @@ return [
'access_token_created_explanation' => 'With this token, they can access **all** of your financial records through the Firefly III API.',
'access_token_created_revoke' => 'If this wasn\'t you, please revoke this token as soon as possible at :url',
// unknown user login attempt
'unknown_user_subject' => 'An unknown user tried to log in',
'unknown_user_body' => 'An unknown user tried to log in to Firefly III. The email address they used was ":address".',
'unknown_user_message' => 'The email address they used was ":address".',
// registered
'registered_subject' => 'Welcome to Firefly III!',
'registered_subject_admin' => 'A new user has registered',

View File

@ -1376,7 +1376,14 @@ return [
'pref_notification_new_access_token' => 'Alert when a new API access token is created',
'pref_notification_transaction_creation' => 'Alert when a transaction is created automatically',
'pref_notification_user_login' => 'Alert when you login from a new location',
'pref_notification_rule_action_failures' => 'Alert when rule actions fail to execute (Slack or Discord only)',
'pref_notification_rule_action_failures' => 'Alert when rule actions fail to execute (not over email)',
'pref_notification_new_password' => 'Your password changed',
'pref_notification_enabled_mfa' => 'Multi factor authentication is enabled',
'pref_notification_disabled_mfa' => 'Multi factor authentication is disabled',
'pref_notification_few_left_mfa' => 'You have just a few backup codes left',
'pref_notification_no_left_mfa' => 'You have no backup codes left',
'pref_notification_many_failed_mfa' => 'The multi factor authentication check keeps failing',
'pref_notification_new_backup_codes' => 'New backup codes have been generated',
'pref_notifications' => 'Notifications',
'pref_notifications_help' => 'Indicate if these are notifications you would like to get. Some notifications may contain sensitive financial information.',
'slack_webhook_url' => 'Slack Webhook URL',
@ -2492,6 +2499,7 @@ return [
'owner_notification_check_new_version' => 'A new version is available',
'owner_notification_check_invite_created' => 'A user is invited to Firefly III',
'owner_notification_check_invite_redeemed' => 'A user invitation is redeemed',
'owner_notification_check_unknown_user_attempt' => 'An unknown user tries to login',
'all_invited_users' => 'All invited users',
'save_notification_settings' => 'Save settings',
'notification_settings' => 'Settings for notifications',

View File

@ -0,0 +1,3 @@
@component('mail::message')
{{ trans('email.unknown_user_body', ['address' => $address]) }}
@endcomponent