artisanpack-ui/security
Core Laravel security toolkit for ArtisanPack UI: sanitization, escaping (Laminas Escaper), KSES filtering, validation rules, security/CSP middleware, CSP builder with nonce & reporting, rate limiting, audit/scan commands, and testing helpers.
This guide covers advanced session security features including session binding, concurrent session management, session rotation, timeouts, and hijacking detection.
The ArtisanPack Security package provides comprehensive session security through:
Configure session security in config/artisanpack/security.php:
'advanced_sessions' => [
'enabled' => env('SECURITY_ADVANCED_SESSIONS_ENABLED', true),
'binding' => [
'enabled' => true,
'ip_address' => [
'enabled' => true,
'strictness' => 'subnet', // 'none', 'subnet', 'exact'
],
'user_agent' => [
'enabled' => true,
'strictness' => 'exact', // 'none', 'browser_only', 'exact'
],
'bind_to_device' => true, // Requires device fingerprinting
],
'concurrent_sessions' => [
'enabled' => true,
'max_sessions' => 5,
'strategy' => 'oldest', // 'oldest', 'newest'
],
'rotation' => [
'enabled' => true,
'interval_minutes' => 15,
'on_privilege_change' => true,
],
'timeouts' => [
'idle_minutes' => 30,
'idle_warning_minutes' => 25,
'absolute_minutes' => 480, // 8 hours
'extend_on_activity' => true,
],
'hijacking_detection' => [
'enabled' => true,
'action' => 'terminate', // 'terminate', 'require_reauth', 'notify'
],
],
The package can enforce session encryption in production:
// In config/artisanpack/security.php
'encrypt' => env('SESSION_ENCRYPT', true),
Apply the middleware to routes:
Route::middleware(['auth', 'session.encrypted'])->group(function () {
// These routes require encrypted sessions
});
php artisan security:check-session
This command verifies:
file in production)Session binding ties a session to specific client attributes, preventing session hijacking by token theft.
'ip_address' => [
'enabled' => true,
'strictness' => 'subnet', // Options below
],
Strictness Levels:
| Level | Description | Use Case |
|---|---|---|
none |
No IP binding | Mobile users, VPN users |
subnet |
Same /24 subnet required | Balance of security and usability |
exact |
Exact IP match required | High security environments |
'user_agent' => [
'enabled' => true,
'strictness' => 'exact',
],
Strictness Levels:
| Level | Description | Use Case |
|---|---|---|
none |
No UA binding | Maximum compatibility |
browser_only |
Browser name must match | Allows minor version updates |
exact |
Exact UA string required | High security |
When device fingerprinting is enabled, sessions can be bound to specific devices:
'bind_to_device' => true,
This requires the HasDevices trait on your User model and device fingerprint collection during login.
Route::middleware(['auth', 'session.binding'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
When a binding violation is detected:
use ArtisanPackUI\Security\Events\SessionHijackingAttempted;
Event::listen(SessionHijackingAttempted::class, function ($event) {
Log::warning('Session binding violation', [
'user_id' => $event->userId,
'session_id' => $event->sessionId,
'violation_type' => $event->violationType,
'expected' => $event->expectedValue,
'actual' => $event->actualValue,
]);
});
Limit the number of simultaneous active sessions per user.
'concurrent_sessions' => [
'enabled' => true,
'max_sessions' => 5,
'strategy' => 'oldest',
],
Strategies:
| Strategy | Behavior |
|---|---|
oldest |
Terminate the oldest session when limit reached |
newest |
Prevent new login, keep existing sessions |
use ArtisanPackUI\Security\Concerns\HasAdvancedSessions;
class User extends Authenticatable
{
use HasAdvancedSessions;
}
// Get all active sessions for a user
$sessions = $user->activeSessions();
// Get session details
foreach ($sessions as $session) {
echo $session->ip_address;
echo $session->user_agent;
echo $session->last_activity;
echo $session->getLocationDisplay(); // "San Francisco, US"
}
// Terminate a specific session
$user->terminateSession($sessionId);
// Terminate all other sessions (keep current)
$user->terminateOtherSessions();
// Terminate all sessions
$user->terminateAllSessions();
# Terminate all sessions for a user
php artisan session:terminate 1
# Terminate sessions by email
php artisan session:terminate user@example.com
# Terminate all sessions except current
php artisan session:terminate 1 --except-current
# Clean up expired sessions
php artisan session:cleanup
Display and manage sessions in the UI:
<livewire:session-manager />
This component shows:
Automatic session ID rotation prevents session fixation attacks.
'rotation' => [
'enabled' => true,
'interval_minutes' => 15, // Rotate every 15 minutes
'on_privilege_change' => true, // Rotate after login, role change, etc.
],
use ArtisanPackUI\Security\Authentication\Session\SessionSecurityService;
public function sensitiveAction(SessionSecurityService $sessionService)
{
// Rotate session before sensitive action
$sessionService->rotateSession();
// Perform action...
}
Sessions are automatically rotated on:
Configure idle and absolute session timeouts.
'timeouts' => [
'idle_minutes' => 30, // Expire after 30 min of inactivity
'idle_warning_minutes' => 25, // Warn 5 minutes before expiry
'absolute_minutes' => 480, // Max 8-hour session
'extend_on_activity' => true, // Reset idle timer on activity
],
Show a warning before the session expires:
// Listen for the warning event
window.addEventListener('session-expiring', (event) => {
const minutesLeft = event.detail.minutes;
showModal(`Your session expires in ${minutesLeft} minutes. Continue?`);
});
// Extend session on user action
async function extendSession() {
await fetch('/session/extend', { method: 'POST' });
}
Server-side endpoint:
Route::post('/session/extend', function () {
session()->regenerate();
return response()->json(['extended' => true]);
})->middleware('auth');
The absolute timeout ensures sessions don't last indefinitely, even with activity:
// In a middleware or listener
if ($session->created_at->diffInMinutes(now()) > 480) {
Auth::logout();
return redirect('/login')->with('message', 'Session expired. Please log in again.');
}
Detect and respond to potential session hijacking attempts.
'hijacking_detection' => [
'enabled' => true,
'action' => 'terminate',
],
Actions:
| Action | Behavior |
|---|---|
terminate |
Immediately terminate the session |
require_reauth |
Require password/2FA verification |
notify |
Log and notify but allow access |
The system monitors for:
use ArtisanPackUI\Security\Events\SessionHijackingAttempted;
Event::listen(SessionHijackingAttempted::class, function ($event) {
// Custom response logic
if ($event->severity === 'critical') {
// Lock the account
$event->user->lockAccount('suspicious_activity');
// Alert security team
Notification::route('slack', config('services.slack.security_channel'))
->notify(new SecurityAlertNotification($event));
}
});
Require re-authentication for sensitive actions.
'step_up_authentication' => [
'enabled' => env('SECURITY_STEP_UP_ENABLED', true),
'timeout_minutes' => 15,
'methods' => [
'password' => true,
'2fa' => true,
'webauthn' => true,
'biometric' => true,
],
'protected_actions' => [
'password_change',
'email_change',
'two_factor_disable',
'delete_account',
],
],
Route::middleware(['auth', 'step-up'])->group(function () {
Route::post('/account/password', [AccountController::class, 'updatePassword']);
Route::post('/account/email', [AccountController::class, 'updateEmail']);
Route::delete('/account', [AccountController::class, 'destroy']);
});
use ArtisanPackUI\Security\Authentication\Session\SessionSecurityService;
public function sensitiveAction(SessionSecurityService $session)
{
if (!$session->hasRecentVerification(15)) {
return redirect()->route('verify.identity')
->with('intended', url()->current());
}
// Proceed with sensitive action
}
<livewire:step-up-authentication-modal />
The session security system emits these events:
| Event | Trigger |
|---|---|
SessionTerminated |
Session was terminated |
SessionHijackingAttempted |
Potential hijacking detected |
SessionRotated |
Session ID was rotated |
ConcurrentSessionLimitReached |
Max sessions reached |
// Recommended production settings
'advanced_sessions' => [
'enabled' => true,
'binding' => [
'enabled' => true,
'ip_address' => [
'enabled' => true,
'strictness' => 'subnet', // Balance security/usability
],
'user_agent' => [
'enabled' => true,
'strictness' => 'browser_only', // Allow updates
],
'bind_to_device' => true,
],
'concurrent_sessions' => [
'enabled' => true,
'max_sessions' => 5,
'strategy' => 'oldest',
],
'rotation' => [
'enabled' => true,
'interval_minutes' => 15,
'on_privilege_change' => true,
],
'timeouts' => [
'idle_minutes' => 30,
'idle_warning_minutes' => 25,
'absolute_minutes' => 480,
'extend_on_activity' => true,
],
'hijacking_detection' => [
'enabled' => true,
'action' => 'terminate',
],
],
Use a secure session driver in production:
SESSION_DRIVER=database
# or
SESSION_DRIVER=redis
Avoid file driver in production for:
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
SESSION_HTTP_ONLY=true
idle_minutes configurationextend_on_activity is enabledMobile users may experience IP changes. Consider:
'ip_address' => [
'strictness' => 'none', // Disable for mobile
],
If legitimate users are being logged out:
concurrent_sessions.max_sessionsSee the Troubleshooting Guide for more solutions.
How can I help you explore Laravel packages today?