laravel-sentinellog maintained by harryes
Laravel SentinelLog
Laravel SentinelLog is a powerful, all-in-one authentication logging and security package for Laravel. It provides advanced features like device tracking, 2FA, session management, brute force protection, geo-fencing, and SSO support, ensuring security while keeping users informed.
Features
- Authentication Logging: Logs login, logout, and failed attempts.
- Device & Geolocation Tracking: Tracks devices and locations for authentication events.
- Notifications: Alerts for new device logins and failed attempts.
- Two-Factor Authentication (2FA): TOTP-based 2FA with QR code support.
- Session Management: Tracks multiple sessions and detects hijacking.
- Brute Force Protection: Rate-limits login attempts and blocks suspicious IPs.
- Geo-Fencing: Restricts logins to specific countries.
- Single Sign-On (SSO): Token-based SSO for seamless authentication.
- New Location Verification: Detects logins from unrecognised locations and emails the user a verify/deny link, invalidating the session on denial.
Demo Project
Want to see Laravel SentinelLog in action? Check out our demo project:
Laravel SentinelLog Demo
This demo project showcases:
- Complete authentication system with SentinelLog integration
- Real-world implementation of all features
- Best practices for configuration and usage
- Example of custom notifications and event handling
- Interactive UI for testing various security features
To run the demo locally:
git clone https://github.com/Harish120/sentinel-test.git
cd sentinel-test
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan db:seed
php artisan serve
Visit http://localhost:8000 to explore the demo.
Installation
Prerequisites
- PHP 8.2 or higher
- Laravel 10.x, 11.x, 12.x, or 13.x
- Composer
Steps
- Install the Package
composer require harryes/laravel-sentinellog
- Publish Configuration
php artisan vendor:publish --tag=sentinel-log-config
2a. (Optional) Publish Views — only if you want to customise the verify/deny confirmation pages:
php artisan vendor:publish --tag=sentinel-log-views
This copies the Blade templates to resources/views/sentinel-log/location/.
Do not publish unless you intend to customise. The package serves the confirmation pages automatically via
loadViewsFrom()— no publishing step is required for them to work. Publishing a copy you never edit will silently shadow future package view updates.
- Run Migrations
php artisan migrate
- Add Trait to User Model
use Harryes\SentinelLog\Traits\NotifiesAuthenticationEvents;
class User extends Authenticatable
{
use NotifiesAuthenticationEvents;
protected $fillable = ['two_factor_secret', 'two_factor_enabled_at'];
protected $casts = [
'two_factor_enabled_at' => 'datetime',
'two_factor_secret' => 'encrypted', // encrypts the TOTP secret at rest
];
}
Security: The
encryptedcast uses your application'sAPP_KEYto encrypt the 2FA secret in the database. A database dump will not expose raw TOTP secrets. If you have existing unencrypted secrets, re-generate them after adding the cast — existing codes will stop working until the secret is re-saved through the encryption layer.
Configuration
Edit config/sentinel-log.php to customize the package. Key options:
General Settings
'enabled' => true,
'events' => ['login' => true, 'logout' => true, 'failed' => true],
'table_name' => 'authentication_logs',
Notifications
'new_device' => ['enabled' => true, 'channels' => ['mail'], 'threshold' => 1],
'failed_attempt' => ['enabled' => true, 'channels' => ['mail'], 'threshold' => 3, 'window' => 60],
'session_hijacking' => ['enabled' => true, 'channels' => ['mail']],
To also persist notifications to the database, add 'database' to the channels array for any notification. Your users table must have the notifications table from php artisan notifications:table.
'new_device' => ['enabled' => true, 'channels' => ['mail', 'database']],
Note: The
NewLocationLogindatabase payload storesverification_id(the record's primary key) rather than the raw token, so the verify/deny URLs cannot be reconstructed from the notifications table.
Two-Factor Authentication (2FA)
'two_factor' => [
'enabled' => false,
'required' => false, // when true, all TwoFactorAuthenticatable users must complete 2FA setup
'middleware' => 'sentinel-log.2fa',
'setup_route' => 'two-factor.setup',
],
enabled— registers thesentinel-log.2famiddleware alias so you can apply it to routesrequired— whentrue, the middleware redirects any user who has not set up 2FA to the setup route; whenfalse(default), only users who have already set up 2FA are prompted to verify
Important: The package does not register a
two-factor.setuproute — you must define it in your own application. If your route has a different name, setsetup_routeto match or use theSENTINEL_LOG_2FA_SETUP_ROUTEenv variable.
Sessions
'sessions' => ['enabled' => true, 'max_active' => 5],
Brute Force Protection
'brute_force' => ['enabled' => true, 'threshold' => 5, 'window' => 15, 'block_duration' => 24],
Geolocation Provider
// Defaults to ipwho.is — free, HTTPS, no API key required.
// Override to use your own provider; must return JSON compatible with ipwho.is response format.
'geo_provider_url' => 'https://ipwho.is',
Geo-Fencing
'geo_fencing' => ['enabled' => false, 'allowed_countries' => ['United States', 'Canada']],
SSO
'sso' => ['enabled' => false, 'client_id' => 'default_client', 'token_lifetime' => 24],
New Location Verification
'location_verification' => [
'enabled' => true,
'channels' => ['mail'],
'token_ttl' => 30, // Minutes until verify/deny links expire
'redirect_after_verify' => '/',
'redirect_after_deny' => '/',
],
Environment Variables
Add these to .env:
SENTINEL_LOG_ENABLED=true
SENTINEL_LOG_2FA_ENABLED=true
SENTINEL_LOG_2FA_REQUIRED=false
SENTINEL_LOG_2FA_SETUP_ROUTE=two-factor.setup
SENTINEL_LOG_GEO_PROVIDER_URL=https://ipwho.is
SENTINEL_LOG_GEO_FENCING_ENABLED=true
SENTINEL_LOG_GEO_FENCING_ALLOWED_COUNTRIES="United States,Canada"
SENTINEL_LOG_LOCATION_VERIFICATION_ENABLED=true
Usage Examples
2FA Setup
Generate a 2FA secret and QR code:
use Harryes\SentinelLog\Services\TwoFactorAuthenticationService;
$service = new TwoFactorAuthenticationService();
$user->update([
'two_factor_secret' => $service->generateSecret(),
'two_factor_enabled_at' => now(),
]);
$qrCodeUrl = $service->getQrCodeUrl($user->two_factor_secret, $user->email);
Protect routes with 2FA middleware:
Route::middleware('sentinel-log.2fa')->group(function () {
Route::get('/dashboard', fn() => 'Protected!');
});
Verify 2FA code:
Route::post('/2fa/verify', function (TwoFactorAuthenticationService $service) {
if ($service->verifyCode(auth()->user()->two_factor_secret, request('code'))) {
session(['2fa_verified' => true]);
return redirect('/dashboard');
}
return back()->withErrors(['code' => 'Invalid 2FA code']);
});
Failed Login Attempt Notifications
To receive notifications when a user's account hits the failed attempt threshold, implement the NotifiableWithFailedAttempt contract on your User model alongside the NotifiesAuthenticationEvents trait:
use Harryes\SentinelLog\Contracts\NotifiableWithFailedAttempt;
use Harryes\SentinelLog\Models\AuthenticationLog;
use Harryes\SentinelLog\Traits\NotifiesAuthenticationEvents;
class User extends Authenticatable implements NotifiableWithFailedAttempt
{
use NotifiesAuthenticationEvents;
public function notifyFailedAttempt(AuthenticationLog $log): void
{
$this->notify(new YourFailedAttemptNotification($log));
}
}
The method is called automatically by the LogFailedLogin listener once the threshold defined in notifications.failed_attempt.threshold is reached within the configured time window.
SSO Integration
Generate an SSO token:
use Harryes\SentinelLog\Services\SsoAuthenticationService;
$ssoService = new SsoAuthenticationService();
$token = $ssoService->generateToken(auth()->user(), 'client_app_1');
Handle SSO login in the client app:
Route::get('/sso/login', fn() => 'Logged in via SSO')->middleware('auth');
Device Recognition
SentinelLog uses a persistent cookie token as the primary device identity signal — the same approach used by GitHub, Google, and Stripe.
How it works:
- On first login from a browser, a cryptographically random 64-character token is generated and stored in a long-lived
sentinel_device_tokencookie (2 years, HttpOnly, SameSite=Lax) - On every subsequent login, the cookie is read and looked up in the login history
- If the token is not found → new device →
NewDeviceLoginnotification sent - If the token is found → recognised device → no notification
Why a cookie and not a header hash?
Header-based hashes that include the IP address break for mobile users (WiFi ↔ cellular), dynamic IPs, and VPN users. The cookie token is stable across all of these. A secondary header hash (User-Agent + Accept-Language + Accept-Encoding) is still stored in device_info alongside the token for forensic reference.
To enable new device notifications, set in config:
'notifications' => [
'new_device' => ['enabled' => true, 'channels' => ['mail']],
],
Upgrading from a previous version? Existing login records have no
tokenfield indevice_info. Each user will receive a single "new device" email on their first login after the upgrade — after which the cookie is set and recognition is stable.
Session Management
View active sessions:
$sessions = auth()->user()->authenticationLogs()->with('session')->get();
Brute Force & Geo-Fencing
Attempts are automatically rate-limited, and IPs are blocked after exceeding the threshold. Geo-fencing blocks logins from unallowed countries based on config/sentinel-log.php.
New Location Verification
When a user logs in from a city/country they have never used before, SentinelLog automatically sends them a NewLocationLogin notification with two action links:
- Yes, this was me — opens a confirmation page. The user clicks confirm which submits a
POSTrequest, marking the location as trusted and logging alocation_verifiedevent. - No, deny this login — opens a confirmation page showing the location and IP details. The user clicks confirm which submits a
POSTrequest to revoke the session, logging alocation_deniedevent.
Why confirmation pages for both links? Email security scanners (Outlook Safe Links, Apple Mail, Gmail) automatically follow every link in an email on delivery. Without a confirmation step, scanners would silently trust or revoke the session before the user even reads the email.
Both confirmation pages are Blade templates you can customise — see the installation steps above.
The links expire after token_ttl minutes (default 30). No application code changes are required — the check runs inside the LogSuccessfulLogin listener on every login.
To disable the feature:
SENTINEL_LOG_LOCATION_VERIFICATION_ENABLED=false
To prune expired, unactioned verification records:
use Harryes\SentinelLog\Services\LocationVerificationService;
app(LocationVerificationService::class)->pruneExpired();
Scheduled Maintenance
SentinelLog accumulates records over time. Add these to your scheduler to keep tables clean:
// routes/console.php (Laravel 11+) or App\Console\Kernel (Laravel 10)
use Harryes\SentinelLog\Models\AuthenticationLog;
use Harryes\SentinelLog\Services\BruteForceProtectionService;
use Harryes\SentinelLog\Services\LocationVerificationService;
Schedule::call(fn () => AuthenticationLog::pruneOlderThan())
->daily()
->name('sentinel-log:prune-auth-logs');
Schedule::call(fn () => app(BruteForceProtectionService::class)->pruneExpired())
->daily()
->name('sentinel-log:prune-blocked-ips');
Schedule::call(fn () => app(LocationVerificationService::class)->pruneExpired())
->daily()
->name('sentinel-log:prune-location-verifications');
| Method | What it cleans | Recommended frequency |
|---|---|---|
AuthenticationLog::pruneOlderThan() |
Auth log entries older than prune.days (default 30) |
Daily |
BruteForceProtectionService::pruneExpired() |
Expired IP block records from sentinel_blocked_ips |
Daily |
LocationVerificationService::pruneExpired() |
Expired unactioned location verification tokens | Daily |
You can override the retention period: AuthenticationLog::pruneOlderThan(90) keeps 90 days of history.
Note on IP blocks: A blocked IP is considered inactive once its
expires_attimestamp passes — no record deletion is needed for the block to stop working.pruneExpired()is purely a housekeeping concern.
Contributing
Submit issues or pull requests on GitHub. Feedback is welcome!
License
This package is open-sourced under the MIT License.