From 0999ad7ff06e90f01b8d77a7d577dce27baafbab Mon Sep 17 00:00:00 2001 From: DaneEveritt Date: Sat, 28 May 2022 17:03:58 -0400 Subject: [PATCH] Add activity logging for authentication events --- .../Auth/ProvidedAuthenticationToken.php | 18 +++++++ .../Events/Contracts/SubscribesToEvents.php | 10 ++++ app/Facades/Activity.php | 11 +++-- .../Auth/LoginCheckpointController.php | 6 +++ app/Http/Controllers/Auth/LoginController.php | 3 ++ app/Listeners/Auth/AuthenticationListener.php | 40 ++++++++++++++++ app/Listeners/Auth/PasswordResetListener.php | 25 ++++++++++ app/Listeners/Auth/TwoFactorListener.php | 17 +++++++ app/Models/User.php | 6 +++ app/Providers/EventServiceProvider.php | 14 ++++-- app/Services/Activity/ActivityLogService.php | 47 ++++++++++++++----- 11 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 app/Events/Auth/ProvidedAuthenticationToken.php create mode 100644 app/Extensions/Illuminate/Events/Contracts/SubscribesToEvents.php create mode 100644 app/Listeners/Auth/AuthenticationListener.php create mode 100644 app/Listeners/Auth/PasswordResetListener.php create mode 100644 app/Listeners/Auth/TwoFactorListener.php diff --git a/app/Events/Auth/ProvidedAuthenticationToken.php b/app/Events/Auth/ProvidedAuthenticationToken.php new file mode 100644 index 000000000..71a1c6797 --- /dev/null +++ b/app/Events/Auth/ProvidedAuthenticationToken.php @@ -0,0 +1,18 @@ +user = $user; + $this->recovery = $recovery; + } +} diff --git a/app/Extensions/Illuminate/Events/Contracts/SubscribesToEvents.php b/app/Extensions/Illuminate/Events/Contracts/SubscribesToEvents.php new file mode 100644 index 000000000..9d78252c9 --- /dev/null +++ b/app/Extensions/Illuminate/Events/Contracts/SubscribesToEvents.php @@ -0,0 +1,10 @@ +input('recovery_token'))) { if ($this->isValidRecoveryToken($user, $recoveryToken)) { + Event::dispatch(new ProvidedAuthenticationToken($user, true)); + return $this->sendLoginResponse($user, $request); } } else { $decrypted = $this->encrypter->decrypt($user->totp_secret); if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { + Event::dispatch(new ProvidedAuthenticationToken($user)); + return $this->sendLoginResponse($user, $request); } } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index b3ee4b2a4..f26e53849 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -7,6 +7,7 @@ use Illuminate\Support\Str; use Illuminate\Http\Request; use Pterodactyl\Models\User; use Illuminate\Http\JsonResponse; +use Pterodactyl\Facades\Activity; use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -71,6 +72,8 @@ class LoginController extends AbstractLoginController return $this->sendLoginResponse($user, $request); } + Activity::event('login.checkpoint')->withRequestMetadata()->subject($user)->log(); + $request->session()->put('auth_confirmation_token', [ 'user_id' => $user->id, 'token_value' => $token = Str::random(64), diff --git a/app/Listeners/Auth/AuthenticationListener.php b/app/Listeners/Auth/AuthenticationListener.php new file mode 100644 index 000000000..e19c828ff --- /dev/null +++ b/app/Listeners/Auth/AuthenticationListener.php @@ -0,0 +1,40 @@ +user) { + $activity = $activity->subject($event->user); + } + + if ($event instanceof Failed) { + foreach ($event->credentials as $key => $value) { + $activity = $activity->property($key, $value); + } + } + + $activity->event($event instanceof Failed ? 'login.failed' : 'login.success')->log(); + } + + public function subscribe(Dispatcher $events): void + { + $events->listen(Failed::class, self::class); + $events->listen(Login::class, self::class); + } +} diff --git a/app/Listeners/Auth/PasswordResetListener.php b/app/Listeners/Auth/PasswordResetListener.php new file mode 100644 index 000000000..54acbc0cf --- /dev/null +++ b/app/Listeners/Auth/PasswordResetListener.php @@ -0,0 +1,25 @@ +request = $request; + } + + public function handle(PasswordReset $event) + { + Activity::event('login.password-reset') + ->withRequestMetadata() + ->subject($event->user) + ->log(); + } +} diff --git a/app/Listeners/Auth/TwoFactorListener.php b/app/Listeners/Auth/TwoFactorListener.php new file mode 100644 index 000000000..468c5da8d --- /dev/null +++ b/app/Listeners/Auth/TwoFactorListener.php @@ -0,0 +1,17 @@ +recovery ? 'login.recovery-token' : 'login.token') + ->withRequestMetadata() + ->subject($event->user) + ->log(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index fff28b9b1..c8fe64214 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Models; use Pterodactyl\Rules\Username; +use Pterodactyl\Facades\Activity; use Illuminate\Support\Collection; use Illuminate\Validation\Rules\In; use Illuminate\Auth\Authenticatable; @@ -214,6 +215,11 @@ class User extends Model implements */ public function sendPasswordResetNotification($token) { + Activity::event('login.reset-password') + ->withRequestMetadata() + ->subject($this) + ->log('sending password reset email'); + $this->notify(new ResetPasswordNotification($token)); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index bf8dfc43d..9a02cca3d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -10,6 +10,7 @@ use Pterodactyl\Observers\UserObserver; use Pterodactyl\Observers\ServerObserver; use Pterodactyl\Observers\SubuserObserver; use Pterodactyl\Observers\EggVariableObserver; +use Pterodactyl\Listeners\Auth\AuthenticationListener; use Pterodactyl\Events\Server\Installed as ServerInstalledEvent; use Pterodactyl\Notifications\ServerInstalled as ServerInstalledNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -22,9 +23,11 @@ class EventServiceProvider extends ServiceProvider * @var array */ protected $listen = [ - ServerInstalledEvent::class => [ - ServerInstalledNotification::class, - ], + ServerInstalledEvent::class => [ServerInstalledNotification::class], + ]; + + protected $subscribe = [ + AuthenticationListener::class, ]; /** @@ -39,4 +42,9 @@ class EventServiceProvider extends ServiceProvider Subuser::observe(SubuserObserver::class); EggVariable::observe(EggVariableObserver::class); } + + public function shouldDiscoverEvents() + { + return true; + } } diff --git a/app/Services/Activity/ActivityLogService.php b/app/Services/Activity/ActivityLogService.php index d34262a33..42293b7e4 100644 --- a/app/Services/Activity/ActivityLogService.php +++ b/app/Services/Activity/ActivityLogService.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Pterodactyl\Models\ActivityLog; use Illuminate\Contracts\Auth\Factory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Request; use Illuminate\Database\ConnectionInterface; class ActivityLogService @@ -55,7 +56,7 @@ class ActivityLogService /** * Set the description for this activity. */ - public function withDescription(?string $description): self + public function description(?string $description): self { $this->getActivity()->description = $description; @@ -65,7 +66,7 @@ class ActivityLogService /** * Sets the subject model instance. */ - public function withSubject(Model $subject): self + public function subject(Model $subject): self { $this->getActivity()->subject()->associate($subject); @@ -75,7 +76,7 @@ class ActivityLogService /** * Sets the actor model instance. */ - public function withActor(Model $actor): self + public function actor(Model $actor): self { $this->getActivity()->actor()->associate($actor); @@ -99,28 +100,52 @@ class ActivityLogService * * @param mixed $value */ - public function withProperty(string $key, $value): self + public function property(string $key, $value): self { $this->getActivity()->properties = $this->getActivity()->properties->put($key, $value); return $this; } + /** + * Attachs the instance request metadata to the activity log event. + */ + public function withRequestMetadata(): self + { + $this->property('ip', Request::getClientIp()); + $this->property('useragent', Request::userAgent()); + + return $this; + } + /** * Logs an activity log entry with the set values and then returns the * model instance to the caller. */ - public function log(string $description): ActivityLog + public function log(string $description = null): ActivityLog { - $this->withDescription($description); + $activity = $this->getActivity(); + + if (!is_null($description)) { + $activity->description = $description; + } - $activity = $this->activity; $activity->save(); + $this->activity = null; return $activity; } + /** + * Returns a cloned instance of the service allowing for the creation of a base + * activity log with the ability to change values on the fly without impact. + */ + public function clone(): self + { + return clone $this; + } + /** * Executes the provided callback within the scope of a database transaction * and will only save the activity log entry if everything else succesfully @@ -133,7 +158,7 @@ class ActivityLogService public function transaction(\Closure $callback, string $description = null) { if (!is_null($description)) { - $this->withDescription($description); + $this->description($description); } return $this->connection->transaction(function () use ($callback) { @@ -161,14 +186,14 @@ class ActivityLogService ]); if ($subject = $this->targetable->subject()) { - $this->withSubject($subject); + $this->subject($subject); } if ($actor = $this->targetable->actor()) { - $this->withActor($actor); + $this->actor($actor); } elseif ($user = $this->manager->guard()->user()) { if ($user instanceof Model) { - $this->withActor($user); + $this->actor($user); } }