├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── authentication-log.php ├── database ├── factories │ └── AuthenticationLogFactory.php └── migrations │ └── create_authentication_log_table.php.stub ├── resources ├── lang │ ├── en.json │ ├── fr.json │ └── pt_BR.json └── views │ ├── .gitkeep │ └── emails │ ├── failed.blade.php │ └── new.blade.php └── src ├── Commands └── PurgeAuthenticationLogCommand.php ├── LaravelAuthenticationLogServiceProvider.php ├── Listeners ├── FailedLoginListener.php ├── LoginListener.php ├── LogoutListener.php └── OtherDeviceLogoutListener.php ├── Models └── AuthenticationLog.php ├── Notifications ├── FailedLogin.php └── NewDevice.php └── Traits └── AuthenticationLoggable.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Laravel Authentication Log` will be documented in this file. 4 | 5 | ### 4.0.0 - 2024-03-28 6 | 7 | - Laravel 11 Support (https://github.com/rappasoft/laravel-authentication-log/pull/100) 8 | - Add config listeners (https://github.com/rappasoft/laravel-authentication-log/pull/92) 9 | - Use real user IP behind Cloudflare 10 | - Check for AuthenticationLoggable trait on event (https://github.com/rappasoft/laravel-authentication-log/pull/94) 11 | - Added PHPDocs to allow autocompletion in IDE (https://github.com/rappasoft/laravel-authentication-log/pull/80) 12 | - Fixes the down method for php artisan migrate:rollback (https://github.com/rappasoft/laravel-authentication-log/pull/93) 13 | 14 | ### 3.0.0 - 2023-02-23 15 | 16 | - Laravel 10 Support - https://github.com/rappasoft/laravel-authentication-log/pull/70 17 | - Use null safe/chaining operator - https://github.com/rappasoft/laravel-authentication-log/pull/57 18 | - Optimize Other Devices Logout Listener - https://github.com/rappasoft/laravel-authentication-log/pull/52 19 | 20 | ### 2.0.0 - 2022-02-19 21 | 22 | ### Added 23 | 24 | - Laravel 9 Support 25 | 26 | ### 1.3.0 - 2022-01-17 27 | 28 | ### Changed 29 | 30 | - Added missing `hasTranslations()` - https://github.com/rappasoft/laravel-authentication-log/pull/30 31 | - Improve translation strings - https://github.com/rappasoft/laravel-authentication-log/pull/31 32 | 33 | ### 1.2.1 - 2021-12-02 34 | 35 | ### Added 36 | 37 | - Added latestAuthentication relationship - https://github.com/rappasoft/laravel-authentication-log/pull/24 38 | 39 | ### Changed 40 | 41 | - Fixed issue with PHP 7.4 - https://github.com/rappasoft/laravel-authentication-log/pull/22 42 | 43 | ### 1.2.0 - 2021-11-21 44 | 45 | ### Added 46 | 47 | - Fire a successful login after a failed login on an unknown (new) device. - https://github.com/rappasoft/laravel-authentication-log/pull/15 48 | - Make the events the package is listening for configurable in the config file 49 | - Added French translation and missing location translations - https://github.com/rappasoft/laravel-authentication-log/pull/18 50 | - PHP 7.4 Support 51 | 52 | ### 1.1.1 - 2021-10-20 53 | 54 | ### Changed 55 | 56 | - Logout listener bug fix - https://github.com/rappasoft/laravel-authentication-log/pull/10 57 | 58 | ### 1.1.0 - 2021-10-11 59 | 60 | ### Added 61 | 62 | - Known issues section to readme 63 | - Ability to set DB connection type - https://github.com/rappasoft/laravel-authentication-log/pull/4 64 | 65 | ## 1.0.0 - 2021-09-30 66 | 67 | - Initial Release 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Anthony Rappa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Package Logo](https://banners.beyondco.de/Laravel%20Authentication%20Log.png?theme=dark&packageManager=composer+require&packageName=rappasoft%2Flaravel-authentication-log&pattern=hideout&style=style_1&description=Log+user+authentication+details+and+send+new+device+notifications.&md=1&showWatermark=0&fontSize=100px&images=lock-closed) 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/rappasoft/laravel-authentication-log.svg?style=flat-square)](https://packagist.org/packages/rappasoft/laravel-authentication-log) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/rappasoft/laravel-authentication-log.svg?style=flat-square)](https://packagist.org/packages/rappasoft/laravel-authentication-log) 5 | 6 | Laravel Authentication Log is a package which tracks your user's authentication information such as login/logout time, IP, Browser, Location, etc. as well as sends out notifications via mail, slack, or sms for new devices and failed logins. 7 | 8 | ## Documentation, Installation, and Usage Instructions 9 | 10 | See the [documentation](https://rappasoft.com/docs/laravel-authentication-log) for detailed installation and usage instructions. 11 | 12 | ## Version Compatibility 13 | 14 | Laravel | Authentication Log 15 | :---------|:------------------ 16 | 8.x | 1.x 17 | 9.x | 2.x 18 | 10.x | 3.x 19 | 11.x | 4.x 20 | 21 | ## Installation 22 | 23 | ```bash 24 | composer require rappasoft/laravel-authentication-log 25 | ``` 26 | 27 | ## Testing 28 | 29 | ```bash 30 | composer test 31 | ``` 32 | 33 | ## Changelog 34 | 35 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 36 | 37 | ## Contributing 38 | 39 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 40 | 41 | ## Security Vulnerabilities 42 | 43 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 44 | 45 | ## Credits 46 | 47 | - [Anthony Rappa](https://github.com/rappasoft) 48 | - [yadahan/laravel-authentication-log](https://github.com/yadahan/laravel-authentication-log) 49 | - [All Contributors](../../contributors) 50 | 51 | ## License 52 | 53 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rappasoft/laravel-authentication-log", 3 | "description": "Log user authentication details and send new device notifications.", 4 | "keywords": [ 5 | "rappasoft", 6 | "laravel", 7 | "laravel-authentication-log" 8 | ], 9 | "homepage": "https://github.com/rappasoft/laravel-authentication-log", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Anthony Rappa", 14 | "email": "rappa819@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "illuminate/contracts": "^10.0|^11.0", 21 | "spatie/laravel-package-tools": "^1.4.3" 22 | }, 23 | "require-dev": { 24 | "nunomaduro/collision": "^6.0", 25 | "orchestra/testbench": "^7.0", 26 | "pestphp/pest": "^1.21", 27 | "pestphp/pest-plugin-laravel": "^1.2", 28 | "spatie/laravel-ray": "^1.29", 29 | "vimeo/psalm": "^4.20" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Rappasoft\\LaravelAuthenticationLog\\": "src", 34 | "Rappasoft\\LaravelAuthenticationLog\\Database\\Factories\\": "database/factories" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Rappasoft\\LaravelAuthenticationLog\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "./vendor/bin/pest --no-coverage", 44 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 45 | }, 46 | "config": { 47 | "sort-packages": true, 48 | "allow-plugins": { 49 | "pestphp/pest-plugin": true 50 | } 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "providers": [ 55 | "Rappasoft\\LaravelAuthenticationLog\\LaravelAuthenticationLogServiceProvider" 56 | ] 57 | } 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true 61 | } 62 | -------------------------------------------------------------------------------- /config/authentication-log.php: -------------------------------------------------------------------------------- 1 | 'authentication_log', 7 | 8 | // The database connection where the authentication_log table resides. Leave empty to use the default 9 | 'db_connection' => null, 10 | 11 | // The events the package listens for to log 12 | 'events' => [ 13 | 'login' => \Illuminate\Auth\Events\Login::class, 14 | 'failed' => \Illuminate\Auth\Events\Failed::class, 15 | 'logout' => \Illuminate\Auth\Events\Logout::class, 16 | 'logout-other-devices' => \Illuminate\Auth\Events\OtherDeviceLogout::class, 17 | ], 18 | 19 | 'listeners' => [ 20 | 'login' => \Rappasoft\LaravelAuthenticationLog\Listeners\LoginListener::class, 21 | 'failed' => \Rappasoft\LaravelAuthenticationLog\Listeners\FailedLoginListener::class, 22 | 'logout' => \Rappasoft\LaravelAuthenticationLog\Listeners\LogoutListener::class, 23 | 'logout-other-devices' => \Rappasoft\LaravelAuthenticationLog\Listeners\OtherDeviceLogoutListener::class, 24 | ], 25 | 26 | 'notifications' => [ 27 | 'new-device' => [ 28 | // Send the NewDevice notification 29 | 'enabled' => env('NEW_DEVICE_NOTIFICATION', true), 30 | 31 | // Use torann/geoip to attempt to get a location 32 | 'location' => true, 33 | 34 | // The Notification class to send 35 | 'template' => \Rappasoft\LaravelAuthenticationLog\Notifications\NewDevice::class, 36 | ], 37 | 'failed-login' => [ 38 | // Send the FailedLogin notification 39 | 'enabled' => env('FAILED_LOGIN_NOTIFICATION', false), 40 | 41 | // Use torann/geoip to attempt to get a location 42 | 'location' => true, 43 | 44 | // The Notification class to send 45 | 'template' => \Rappasoft\LaravelAuthenticationLog\Notifications\FailedLogin::class, 46 | ], 47 | ], 48 | 49 | // When the clean-up command is run, delete old logs greater than `purge` days 50 | // Don't schedule the clean-up command if you want to keep logs forever. 51 | 'purge' => 365, 52 | 53 | // If you are behind an CDN proxy, set 'behind_cdn.http_header_field' to the corresponding http header field of your cdn 54 | // For cloudflare you can have look at: https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/ 55 | // 'behind_cdn' => [ 56 | // 'http_header_field' => 'HTTP_CF_CONNECTING_IP' // used by Cloudflare 57 | // ], 58 | 59 | // If you are not a cdn user, use false 60 | 'behind_cdn' => false, 61 | ]; 62 | -------------------------------------------------------------------------------- /database/factories/AuthenticationLogFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('authenticatable'); 14 | $table->string('ip_address', 45)->nullable(); 15 | $table->text('user_agent')->nullable(); 16 | $table->timestamp('login_at')->nullable(); 17 | $table->boolean('login_successful')->default(false); 18 | $table->timestamp('logout_at')->nullable(); 19 | $table->boolean('cleared_by_user')->default(false); 20 | $table->json('location')->nullable(); 21 | }); 22 | } 23 | 24 | public function down(): void 25 | { 26 | Schema::dropIfExists(config('authentication-log.table_name')); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /resources/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "A failed login to your account": "A failed login to your account", 3 | "Account:": "Account:", 4 | "Browser:": "Browser:", 5 | "Hello!": "Hello!", 6 | "If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.": "If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.", 7 | "IP Address:": "IP Address:", 8 | "Location:": "Location:", 9 | "Regards,": "Regards,", 10 | "There has been a failed login attempt to your :app account.": "There has been a failed login attempt to your :app account.", 11 | "Time:": "Time:", 12 | "Unknown City": "Unknown City", 13 | "Unknown State": "Unknown State", 14 | "Your :app account logged in from a new device.": "Your :app account logged in from a new device." 15 | } 16 | -------------------------------------------------------------------------------- /resources/lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "A failed login to your account": "Échec de la connexion à votre compte", 3 | "Account:": "Compte :", 4 | "Browser:": "Navigateur :", 5 | "Hello!": "Bonjour,", 6 | "If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.": "Si c’était vous, vous pouvez ignorer cette alerte. Si vous soupçonnez une activité suspecte sur votre compte, veuillez modifier votre mot de passe.", 7 | "IP Address:": "Adresse IP :", 8 | "Location:": "Emplacement :", 9 | "Regards,": "Cordialement,", 10 | "There has been a failed login attempt to your :app account.": "Une tentative de connexion à votre compte sur :app a échoué.", 11 | "Time:": "Heure :", 12 | "Unknown City": "Ville inconnue", 13 | "Unknown State": "État inconnu", 14 | "Your :app account logged in from a new device.": "Connexion à votre compte sur :app depuis un nouvel appareil." 15 | } 16 | -------------------------------------------------------------------------------- /resources/lang/pt_BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "A failed login to your account": "Um login com falha em sua conta", 3 | "Account:": "Conta", 4 | "Browser:": "Navegador:", 5 | "If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.": "Se foi você, pode ignorar este alerta. Se você suspeitar de qualquer atividade suspeita em sua conta, altere sua senha.", 6 | "IP Address:": "Endereço de IP:", 7 | "Location:": "Localização:", 8 | "Regards,": "Obrigado,", 9 | "There has been a failed login attempt to your :app account.": "Houve uma falha na tentativa de login em seu :app conta.", 10 | "Time:": "Data:", 11 | "Unknown City": "Cidade Desconhecida", 12 | "Unknown State": "Estado Desconhecido", 13 | "Your :app account logged in from a new device.": "Sua :app foi acessada em um novo dispositivo" 14 | } 15 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolf620/laravel_authentication_log/2352984f0b94e478fc32b3faf75cbc30323727ac/resources/views/.gitkeep -------------------------------------------------------------------------------- /resources/views/emails/failed.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # @lang('Hello!') 3 | 4 | @lang('There has been a failed login attempt to your :app account.', ['app' => config('app.name')]) 5 | 6 | > **@lang('Account:')** {{ $account->email }}
7 | > **@lang('Time:')** {{ $time->toCookieString() }}
8 | > **@lang('IP Address:')** {{ $ipAddress }}
9 | > **@lang('Browser:')** {{ $browser }}
10 | @if ($location && $location['default'] === false) 11 | > **@lang('Location:')** {{ $location['city'] ?? __('Unknown City') }}, {{ $location['state'], __('Unknown State') }} 12 | @endif 13 | 14 | @lang('If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.') 15 | 16 | @lang('Regards,')
17 | {{ config('app.name') }} 18 | @endcomponent 19 | -------------------------------------------------------------------------------- /resources/views/emails/new.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # @lang('Hello!') 3 | 4 | @lang('Your :app account logged in from a new device.', ['app' => config('app.name')]) 5 | 6 | > **@lang('Account:')** {{ $account->email }}
7 | > **@lang('Time:')** {{ $time->toCookieString() }}
8 | > **@lang('IP Address:')** {{ $ipAddress }}
9 | > **@lang('Browser:')** {{ $browser }}
10 | @if ($location && $location['default'] === false) 11 | > **@lang('Location:')** {{ $location['city'] ?? __('Unknown City') }}, {{ $location['state'], __('Unknown State') }} 12 | @endif 13 | 14 | @lang('If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.') 15 | 16 | @lang('Regards,')
17 | {{ config('app.name') }} 18 | @endcomponent 19 | -------------------------------------------------------------------------------- /src/Commands/PurgeAuthenticationLogCommand.php: -------------------------------------------------------------------------------- 1 | comment('Clearing authentication log...'); 17 | 18 | $deleted = AuthenticationLog::where('login_at', '<', now()->subDays(config('authentication-log.purge'))->format('Y-m-d H:i:s'))->delete(); 19 | 20 | $this->info($deleted . ' authentication logs cleared.'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/LaravelAuthenticationLogServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-authentication-log') 24 | ->hasConfigFile() 25 | ->hasTranslations() 26 | ->hasViews() 27 | ->hasMigration('create_authentication_log_table') 28 | ->hasCommand(PurgeAuthenticationLogCommand::class); 29 | 30 | $events = $this->app->make(Dispatcher::class); 31 | $events->listen(config('authentication-log.events.login', Login::class), config('authentication-log.listeners.login', LoginListener::class)); 32 | $events->listen(config('authentication-log.events.failed', Failed::class), config('authentication-log.listeners.failed', FailedLoginListener::class)); 33 | $events->listen(config('authentication-log.events.logout', Logout::class), config('authentication-log.listeners.logout', LogoutListener::class)); 34 | $events->listen(config('authentication-log.events.other-device-logout', OtherDeviceLogout::class), config('authentication-log.listeners.other-device-logout', OtherDeviceLogoutListener::class)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Listeners/FailedLoginListener.php: -------------------------------------------------------------------------------- 1 | request = $request; 17 | } 18 | 19 | public function handle($event): void 20 | { 21 | $listener = config('authentication-log.events.failed', Failed::class); 22 | 23 | if (! $event instanceof $listener) { 24 | return; 25 | } 26 | 27 | if ($event->user) { 28 | if(! in_array(AuthenticationLoggable::class, class_uses_recursive(get_class($event->user)))) { 29 | return; 30 | } 31 | 32 | if (config('authentication-log.behind_cdn')) { 33 | $ip = $this->request->server(config('authentication-log.behind_cdn.http_header_field')); 34 | } else { 35 | $ip = $this->request->ip(); 36 | } 37 | 38 | $log = $event->user->authentications()->create([ 39 | 'ip_address' => $ip, 40 | 'user_agent' => $this->request->userAgent(), 41 | 'login_at' => now(), 42 | 'login_successful' => false, 43 | 'location' => config('authentication-log.notifications.new-device.location') ? optional(geoip()->getLocation($ip))->toArray() : null, 44 | ]); 45 | 46 | if (config('authentication-log.notifications.failed-login.enabled')) { 47 | $failedLogin = config('authentication-log.notifications.failed-login.template') ?? FailedLogin::class; 48 | $event->user->notify(new $failedLogin($log)); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Listeners/LoginListener.php: -------------------------------------------------------------------------------- 1 | request = $request; 18 | } 19 | 20 | public function handle($event): void 21 | { 22 | $listener = config('authentication-log.events.login', Login::class); 23 | 24 | if (! $event instanceof $listener) { 25 | return; 26 | } 27 | 28 | if ($event->user) { 29 | if(! in_array(AuthenticationLoggable::class, class_uses_recursive(get_class($event->user)))) { 30 | return; 31 | } 32 | 33 | if (config('authentication-log.behind_cdn')) { 34 | $ip = $this->request->server(config('authentication-log.behind_cdn.http_header_field')); 35 | } else { 36 | $ip = $this->request->ip(); 37 | } 38 | 39 | $user = $event->user; 40 | $userAgent = $this->request->userAgent(); 41 | $known = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->whereLoginSuccessful(true)->first(); 42 | $newUser = Carbon::parse($user->{$user->getCreatedAtColumn()})->diffInMinutes(Carbon::now()) < 1; 43 | 44 | $log = $user->authentications()->create([ 45 | 'ip_address' => $ip, 46 | 'user_agent' => $userAgent, 47 | 'login_at' => now(), 48 | 'login_successful' => true, 49 | 'location' => config('authentication-log.notifications.new-device.location') ? optional(geoip()->getLocation($ip))->toArray() : null, 50 | ]); 51 | 52 | if (! $known && ! $newUser && config('authentication-log.notifications.new-device.enabled')) { 53 | $newDevice = config('authentication-log.notifications.new-device.template') ?? NewDevice::class; 54 | $user->notify(new $newDevice($log)); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Listeners/LogoutListener.php: -------------------------------------------------------------------------------- 1 | request = $request; 17 | } 18 | 19 | public function handle($event): void 20 | { 21 | $listener = config('authentication-log.events.logout', Logout::class); 22 | 23 | if (! $event instanceof $listener) { 24 | return; 25 | } 26 | 27 | if ($event->user) { 28 | if(! in_array(AuthenticationLoggable::class, class_uses_recursive(get_class($event->user)))) { 29 | return; 30 | } 31 | 32 | $user = $event->user; 33 | 34 | if (config('authentication-log.behind_cdn')) { 35 | $ip = $this->request->server(config('authentication-log.behind_cdn.http_header_field')); 36 | } else { 37 | $ip = $this->request->ip(); 38 | } 39 | 40 | $userAgent = $this->request->userAgent(); 41 | $log = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->orderByDesc('login_at')->first(); 42 | 43 | if (! $log) { 44 | $log = new AuthenticationLog([ 45 | 'ip_address' => $ip, 46 | 'user_agent' => $userAgent, 47 | ]); 48 | } 49 | 50 | $log->logout_at = now(); 51 | 52 | $user->authentications()->save($log); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Listeners/OtherDeviceLogoutListener.php: -------------------------------------------------------------------------------- 1 | request = $request; 17 | } 18 | 19 | public function handle($event): void 20 | { 21 | $listener = config('authentication-log.events.other-device-logout', OtherDeviceLogout::class); 22 | 23 | if (! $event instanceof $listener) { 24 | return; 25 | } 26 | 27 | if ($event->user) { 28 | if(! in_array(AuthenticationLoggable::class, class_uses_recursive(get_class($event->user)))) { 29 | return; 30 | } 31 | 32 | $user = $event->user; 33 | 34 | if (config('authentication-log.behind_cdn')) { 35 | $ip = $this->request->server(config('authentication-log.behind_cdn.http_header_field')); 36 | } else { 37 | $ip = $this->request->ip(); 38 | } 39 | 40 | $userAgent = $this->request->userAgent(); 41 | $authenticationLog = $user->authentications()->whereIpAddress($ip)->whereUserAgent($userAgent)->first(); 42 | 43 | if (! $authenticationLog) { 44 | $authenticationLog = new AuthenticationLog([ 45 | 'ip_address' => $ip, 46 | 'user_agent' => $userAgent, 47 | ]); 48 | } 49 | 50 | foreach ($user->authentications()->whereLoginSuccessful(true)->whereNull('logout_at')->get() as $log) { 51 | if ($log->id !== $authenticationLog->id) { 52 | $log->update([ 53 | 'cleared_by_user' => true, 54 | 'logout_at' => now(), 55 | ]); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Models/AuthenticationLog.php: -------------------------------------------------------------------------------- 1 | 'boolean', 38 | 'location' => 'array', 39 | 'login_successful' => 'boolean', 40 | 'login_at' => 'datetime', 41 | 'logout_at' => 'datetime', 42 | ]; 43 | 44 | public function __construct(array $attributes = []) 45 | { 46 | if (! isset($this->connection)) { 47 | $this->setConnection(config('authentication-log.db_connection')); 48 | } 49 | 50 | parent::__construct($attributes); 51 | } 52 | 53 | public function getTable() 54 | { 55 | return config('authentication-log.table_name', parent::getTable()); 56 | } 57 | 58 | public function authenticatable(): MorphTo 59 | { 60 | return $this->morphTo(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Notifications/FailedLogin.php: -------------------------------------------------------------------------------- 1 | authenticationLog = $authenticationLog; 22 | } 23 | 24 | public function via($notifiable) 25 | { 26 | return $notifiable->notifyAuthenticationLogVia(); 27 | } 28 | 29 | public function toMail($notifiable) 30 | { 31 | return (new MailMessage()) 32 | ->subject(__('A failed login to your account')) 33 | ->markdown('authentication-log::emails.failed', [ 34 | 'account' => $notifiable, 35 | 'time' => $this->authenticationLog->login_at, 36 | 'ipAddress' => $this->authenticationLog->ip_address, 37 | 'browser' => $this->authenticationLog->user_agent, 38 | 'location' => $this->authenticationLog->location, 39 | ]); 40 | } 41 | 42 | public function toSlack($notifiable) 43 | { 44 | return (new SlackMessage()) 45 | ->from(config('app.name')) 46 | ->warning() 47 | ->content(__('There has been a failed login attempt to your :app account.', ['app' => config('app.name')])) 48 | ->attachment(function ($attachment) use ($notifiable) { 49 | $attachment->fields([ 50 | __('Account') => $notifiable->email, 51 | __('Time') => $this->authenticationLog->login_at->toCookieString(), 52 | __('IP Address') => $this->authenticationLog->ip_address, 53 | __('Browser') => $this->authenticationLog->user_agent, 54 | __('Location') => 55 | $this->authenticationLog->location && 56 | $this->authenticationLog->location['default'] === false ? 57 | ($this->authenticationLog->location['city'] ?? 'N/A') . ', ' . ($this->authenticationLog->location['state'] ?? 'N/A') : 58 | 'Unknown', 59 | ]); 60 | }); 61 | } 62 | 63 | public function toNexmo($notifiable) 64 | { 65 | return (new NexmoMessage()) 66 | ->content(__('There has been a failed login attempt to your :app account.', ['app' => config('app.name')])); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Notifications/NewDevice.php: -------------------------------------------------------------------------------- 1 | authenticationLog = $authenticationLog; 22 | } 23 | 24 | public function via($notifiable) 25 | { 26 | return $notifiable->notifyAuthenticationLogVia(); 27 | } 28 | 29 | public function toMail($notifiable) 30 | { 31 | return (new MailMessage()) 32 | ->subject(__('Your :app account logged in from a new device.', ['app' => config('app.name')])) 33 | ->markdown('authentication-log::emails.new', [ 34 | 'account' => $notifiable, 35 | 'time' => $this->authenticationLog->login_at, 36 | 'ipAddress' => $this->authenticationLog->ip_address, 37 | 'browser' => $this->authenticationLog->user_agent, 38 | 'location' => $this->authenticationLog->location, 39 | ]); 40 | } 41 | 42 | public function toSlack($notifiable) 43 | { 44 | return (new SlackMessage()) 45 | ->from(config('app.name')) 46 | ->warning() 47 | ->content(__('Your :app account logged in from a new device.', ['app' => config('app.name')])) 48 | ->attachment(function ($attachment) use ($notifiable) { 49 | $attachment->fields([ 50 | __('Account') => $notifiable->email, 51 | __('Time') => $this->authenticationLog->login_at->toCookieString(), 52 | __('IP Address') => $this->authenticationLog->ip_address, 53 | __('Browser') => $this->authenticationLog->user_agent, 54 | __('Location') => 55 | $this->authenticationLog->location && 56 | $this->authenticationLog->location['default'] === false ? 57 | ($this->authenticationLog->location['city'] ?? 'N/A') . ', ' . ($this->authenticationLog->location['state'] ?? 'N/A') : 58 | 'Unknown', 59 | ]); 60 | }); 61 | } 62 | 63 | public function toNexmo($notifiable) 64 | { 65 | return (new NexmoMessage()) 66 | ->content(__('Your :app account logged in from a new device.', ['app' => config('app.name')])); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Traits/AuthenticationLoggable.php: -------------------------------------------------------------------------------- 1 | morphMany(AuthenticationLog::class, 'authenticatable')->latest('login_at'); 12 | } 13 | 14 | public function latestAuthentication() 15 | { 16 | return $this->morphOne(AuthenticationLog::class, 'authenticatable')->latestOfMany('login_at'); 17 | } 18 | 19 | public function notifyAuthenticationLogVia(): array 20 | { 21 | return ['mail']; 22 | } 23 | 24 | public function lastLoginAt() 25 | { 26 | return $this->authentications()->first()?->login_at; 27 | } 28 | 29 | public function lastSuccessfulLoginAt() 30 | { 31 | return $this->authentications()->whereLoginSuccessful(true)->first()?->login_at; 32 | } 33 | 34 | public function lastLoginIp() 35 | { 36 | return $this->authentications()->first()?->ip_address; 37 | } 38 | 39 | public function lastSuccessfulLoginIp() 40 | { 41 | return $this->authentications()->whereLoginSuccessful(true)->first()?->ip_address; 42 | } 43 | 44 | public function previousLoginAt() 45 | { 46 | return $this->authentications()->skip(1)->first()?->login_at; 47 | } 48 | 49 | public function previousLoginIp() 50 | { 51 | return $this->authentications()->skip(1)->first()?->ip_address; 52 | } 53 | } 54 | --------------------------------------------------------------------------------