PHPackages                             joe-nassar-tech/laravel-exponential-lockout - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. joe-nassar-tech/laravel-exponential-lockout

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

joe-nassar-tech/laravel-exponential-lockout
===========================================

Laravel package for implementing exponential lockout on failed authentication attempts with configurable contexts

1.4.6(9mo ago)011MITPHPPHP ^8.0

Since Aug 6Pushed 9mo agoCompare

[ Source](https://github.com/joe-nassar-tech/laravel-exponential-lockout)[ Packagist](https://packagist.org/packages/joe-nassar-tech/laravel-exponential-lockout)[ Docs](https://github.com/joe-nassar-tech/laravel-exponential-lockout)[ RSS](/packages/joe-nassar-tech-laravel-exponential-lockout/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (6)Versions (12)Used By (0)

Laravel Exponential Lockout
===========================

[](#laravel-exponential-lockout)

A comprehensive Laravel package for implementing exponential lockout functionality on failed authentication attempts with configurable contexts and response handling.

Features
--------

[](#features)

- 🎯 **Perfect Exponential Lockout**: Grace attempts system - exactly 1 attempt allowed after each lockout period
- ✅ **100% Automatic Middleware**: Zero code changes needed - just add middleware to routes
- ✅ **Smart Delay Progression**: Configurable delays (default: 1min → 5min → 15min → 30min → 2hr → 6hr → 12hr → 24hr)
- ✅ **Configurable Free Attempts**: Set how many attempts before first lockout (default: 3)
- ✅ **Multiple Contexts**: Different rules for `login`, `otp`, `admin`, `pin`, etc.
- ✅ **Context Inheritance**: Reusable templates for consistent security policies
- ✅ **Flexible Key Extraction**: Track by email, phone, username, IP, or custom logic
- ✅ **Auto-Detection**: Automatically detects 4xx/5xx failures and 2xx success
- ✅ **Manual API Control**: Full programmatic control when needed
- ✅ **Smart Response Handling**: Auto-detect JSON/redirect responses with proper headers
- ✅ **Persistent Attempt History**: Remembers failures across lockout periods
- ✅ **Cache-Based Storage**: Uses Laravel's cache system (Redis, File, Database, etc.)
- ✅ **Artisan Commands**: CLI tools for lockout management and debugging
- ✅ **Blade Directives**: Template helpers for lockout status display
- ✅ **Laravel 9-12+ Compatible**: Full support for all modern Laravel versions

Installation
------------

[](#installation)

Install via Composer:

```
composer require joe-nassar-tech/laravel-exponential-lockout
```

Publish the configuration file:

```
php artisan vendor:publish --tag=exponential-lockout-config
```

Configuration
-------------

[](#configuration)

The package comes with sensible defaults, but you can customize everything in `config/exponential-lockout.php`:

```
return [
    // Cache configuration
    'cache' => [
        'store' => null, // Uses default cache store
        'prefix' => 'exponential_lockout',
    ],

    // Default delay sequence (in seconds) - 1min → 5min → 15min → 30min → 2hr → 6hr → 12hr → 24hr
    'default_delays' => [60, 300, 900, 1800, 7200, 21600, 43200, 86400],

    // Response handling
    'default_response_mode' => 'auto', // 'auto', 'json', 'redirect', 'callback'
    'default_redirect_route' => 'login',

    // Context templates for inheritance
    'context_templates' => [
        'strict' => [
            'enabled' => true,
            'min_attempts' => 1, // Lock immediately after 1st failure
            'delays' => [300, 900, 1800, 7200, 21600], // 5min → 15min → 30min → 2hr → 6hr
            'reset_after_hours' => 48, // Keep attempts longer
        ],
        'api' => [
            'enabled' => true,
            'response_mode' => 'json',
            'min_attempts' => 3,
            'delays' => [60, 300, 900, 1800, 7200],
            'reset_after_hours' => 24,
        ],
        'mfa' => [
            'enabled' => true,
            'min_attempts' => 2, // Stricter for MFA
            'delays' => [30, 60, 120, 300, 600], // Quick cycles for time-sensitive MFA
            'reset_after_hours' => 12, // Reset faster for MFA
        ],
    ],

    // Context-specific configurations
    'contexts' => [
        'login' => [
            'extends' => 'api', // Inherit API template
            'key' => 'email',
            'redirect_route' => 'login',
        ],
        'otp' => [
            'extends' => 'mfa', // Inherit MFA template
            'key' => 'phone',
            'response_mode' => 'json',
        ],
        'admin' => [
            'extends' => 'strict', // Inherit strict template
            'key' => 'email',
            'redirect_route' => 'admin.login',
        ],
        // ... more contexts
    ],
];
```

How It Works
------------

[](#how-it-works)

### 🎯 **Perfect Exponential Lockout Behavior**

[](#-perfect-exponential-lockout-behavior)

The package implements a **grace attempt system** that provides exactly 1 attempt after each lockout period:

AttemptResultBehavior1st-3rd ❌✅ **Free attempts**No lockout (configurable with `min_attempts`)4th ❌🚫 **Block 60s**First lockout (using default delays)*After 60s*🎁 **1 grace attempt**Exactly 1 try allowedGrace ❌🚫 **Block 300s**Second lockout (5 minutes)*After 300s*🎁 **1 grace attempt**Exactly 1 try allowedGrace ❌🚫 **Block 900s**Third lockout (15 minutes)**Any Success ✅**🔄 **Complete Reset**Back to 3 free attempts### 🔑 **Key Features:**

[](#-key-features)

- **Configurable free attempts** (default: 3) before first lockout
- **Progressive delays** that increase exponentially
- **Grace attempts** - exactly 1 attempt allowed after each lockout expires
- **Automatic reset** on any successful authentication
- **Persistent memory** - remembers attempt history across sessions

Basic Usage
-----------

[](#basic-usage)

### 1. Middleware Protection

[](#1-middleware-protection)

Protect routes with middleware:

```
use Illuminate\Support\Facades\Route;

// Login route protection
Route::post('/login', [LoginController::class, 'login'])
    ->middleware('exponential.lockout:login');

// OTP verification protection
Route::post('/verify-otp', [OtpController::class, 'verify'])
    ->middleware('exponential.lockout:otp');

// PIN validation protection
Route::post('/validate-pin', [PinController::class, 'validate'])
    ->middleware('exponential.lockout:pin');
```

### 2. Manual Lockout Management

[](#2-manual-lockout-management)

Use the `Lockout` facade for manual control:

```
use ExponentialLockout\Facades\Lockout;

class LoginController extends Controller
{
    public function login(Request $request)
    {
        // Check if locked out (optional - middleware handles this automatically)
        if (Lockout::isLockedOut('login', $request->email)) {
            $remaining = Lockout::getRemainingTime('login', $request->email);
            return response()->json(['error' => 'Locked', 'retry_after' => $remaining], 429);
        }

        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            // Clear lockout on successful login (optional - middleware does this automatically)
            Lockout::clear('login', $request->email);
            return response()->json(['success' => true], 200);
        }

        // Record failed attempt (optional - middleware does this automatically)
        Lockout::recordFailure('login', $request->email);
        return response()->json(['error' => 'Invalid credentials'], 401);
    }
}
```

### 3. OTP Verification Example

[](#3-otp-verification-example)

```
class OtpController extends Controller
{
    public function verify(Request $request)
    {
        $phone = $request->input('phone');
        $otp = $request->input('otp');

        if ($this->isValidOtp($phone, $otp)) {
            // Clear lockout on successful verification
            Lockout::clear('otp', $phone);

            return response()->json(['message' => 'OTP verified successfully']);
        }

        // Record failed attempt
        Lockout::recordFailure('otp', $phone);

        return response()->json([
            'error' => 'Invalid OTP',
            'attempts' => Lockout::getAttemptCount('otp', $phone)
        ], 401);
    }
}
```

Advanced Usage
--------------

[](#advanced-usage)

### Custom Key Extraction

[](#custom-key-extraction)

Define custom key extractors in the config:

```
'key_extractors' => [
    'user_session' => function ($request) {
        return $request->session()->getId();
    },
    'device_fingerprint' => function ($request) {
        return hash('sha256', $request->userAgent() . $request->ip());
    },
],

'contexts' => [
    'admin_login' => [
        'key' => 'device_fingerprint',
        'delays' => [300, 900, 1800, 7200],
    ],
],
```

### Custom Response Handling

[](#custom-response-handling)

Implement custom response logic:

```
'custom_response_callback' => function ($context, $key, $remainingTime) {
    return response()->json([
        'error' => 'Account temporarily locked',
        'context' => $context,
        'retry_after' => $remainingTime,
        'retry_after_human' => gmdate('H:i:s', $remainingTime),
    ], 429);
},
```

### Check Lockout Status

[](#check-lockout-status)

Check if a user is locked out before processing:

```
if (Lockout::isLockedOut('login', $email)) {
    $remainingTime = Lockout::getRemainingTime('login', $email);

    return response()->json([
        'error' => 'Account locked',
        'retry_after' => $remainingTime
    ], 429);
}
```

### Get Detailed Lockout Information

[](#get-detailed-lockout-information)

```
$info = Lockout::getLockoutInfo('login', $email);
/*
Returns:
[
    'context' => 'login',
    'key' => 'user@example.com',
    'attempts' => 3,
    'is_locked_out' => true,
    'remaining_time' => 840,
    'locked_until' => Carbon instance,
    'last_attempt' => Carbon instance,
]
*/
```

Blade Directives
----------------

[](#blade-directives)

Use Blade directives in your templates:

```
{{-- Check if user is locked out --}}
@lockout('login', $user->email)

        Your account is temporarily locked. Please try again later.

@endlockout

{{-- Show content when NOT locked out --}}
@notlockout('login', $user->email)

@endnotlockout

{{-- Get lockout information --}}
@lockoutinfo($lockoutInfo, 'login', $user->email)
@if($lockoutInfo['is_locked_out'])
    Locked for {{ gmdate('H:i:s', $lockoutInfo['remaining_time']) }} more
@endif

{{-- Get remaining time --}}
@lockouttime($remainingSeconds, 'login', $user->email)
@if($remainingSeconds > 0)
    Try again in {{ $remainingSeconds }} seconds
@endif
```

Artisan Commands
----------------

[](#artisan-commands)

### Clear Specific Lockout

[](#clear-specific-lockout)

```
# Clear lockout for specific context and key
php artisan lockout:clear login user@example.com

# Clear with force (no confirmation)
php artisan lockout:clear login user@example.com --force
```

### Clear All Lockouts for Context

[](#clear-all-lockouts-for-context)

```
# Clear all lockouts for a context
php artisan lockout:clear login --all

# With force flag
php artisan lockout:clear login --all --force
```

API Reference
-------------

[](#api-reference)

### Lockout Facade Methods

[](#lockout-facade-methods)

```
// Record a failed attempt
Lockout::recordFailure(string $context, string $key): int

// Check if locked out
Lockout::isLockedOut(string $context, string $key): bool

// Get remaining lockout time in seconds
Lockout::getRemainingTime(string $context, string $key): int

// Clear lockout
Lockout::clear(string $context, string $key): bool

// Clear all lockouts for context
Lockout::clearContext(string $context): bool

// Get attempt count
Lockout::getAttemptCount(string $context, string $key): int

// Extract key from request
Lockout::extractKeyFromRequest(string $context, Request $request): string

// Get detailed lockout information
Lockout::getLockoutInfo(string $context, string $key): array
```

Context Configuration
---------------------

[](#context-configuration)

Each context can be configured independently or inherit from templates:

### Using Templates (Recommended)

[](#using-templates-recommended)

```
'context_templates' => [
    'strict' => [
        'enabled' => true,
        'min_attempts' => 1, // Lock immediately after 1st failure
        'delays' => [300, 900, 1800, 7200, 21600], // 5min → 15min → 30min → 2hr → 6hr
        'reset_after_hours' => 48, // Keep attempts longer
    ],
    'api' => [
        'enabled' => true,
        'response_mode' => 'json',
        'min_attempts' => 3,
        'delays' => [60, 300, 900, 1800, 7200],
        'reset_after_hours' => 24,
    ],
],

'contexts' => [
    'login' => [
        'extends' => 'api', // Inherit API template
        'key' => 'email',
        'redirect_route' => 'login', // Override specific setting
    ],
    'admin' => [
        'extends' => 'strict', // Inherit strict template
        'key' => 'email',
        'redirect_route' => 'admin.login',
    ],
],
```

### Direct Configuration

[](#direct-configuration)

```
'contexts' => [
    'login' => [
        'enabled' => true,                    // Enable/disable this context
        'key' => 'email',                     // Key extraction method
        'delays' => [60, 300, 900],          // Custom delay sequence
        'response_mode' => 'auto',            // Response handling mode
        'redirect_route' => 'login',          // Redirect route for web requests
        'max_attempts' => null,               // Max attempts (null = use delay sequence length)
        'min_attempts' => 3,                  // Attempts before first lockout
        'reset_after_hours' => 24,            // Reset attempts after inactivity
    ],
],
```

### Available Key Extractors

[](#available-key-extractors)

- `email` - Extract from `email` input field
- `phone` - Extract from `phone` input field
- `user_id` - Extract from authenticated user ID
- `ip` - Use client IP address
- `username` - Extract from `username` input field
- Custom callable - Define your own extraction logic

### Response Modes

[](#response-modes)

- `auto` - Auto-detect JSON or redirect based on request
- `json` - Always return JSON response
- `redirect` - Always redirect to specified route
- `callback` - Use custom callback function

Delay Sequences
---------------

[](#delay-sequences)

Default sequence provides exponential backoff:

```
[60, 300, 900, 1800, 7200, 21600, 43200, 86400]
// 1min, 5min, 15min, 30min, 2hr, 6hr, 12hr, 24hr
```

Customize per context:

```
'contexts' => [
    'otp' => [
        'delays' => [30, 60, 180, 300, 600], // Shorter for OTP
    ],
    'admin' => [
        'delays' => [600, 1800, 7200, 21600], // Longer for admin
    ],
],
```

Error Handling
--------------

[](#error-handling)

The package includes comprehensive error handling:

```
try {
    Lockout::recordFailure('invalid_context', $key);
} catch (InvalidArgumentException $e) {
    // Context not configured or disabled
    Log::error('Lockout error: ' . $e->getMessage());
}
```

Cache Considerations
--------------------

[](#cache-considerations)

### Cache Store Selection

[](#cache-store-selection)

Configure the cache store in your config:

```
'cache' => [
    'store' => 'redis', // Use specific store
    'prefix' => 'app_lockout',
],
```

### TTL Management

[](#ttl-management)

Cache entries automatically expire after lockout duration + 1 hour buffer.

### Redis Optimization

[](#redis-optimization)

For Redis, consider using a dedicated database:

```
// config/cache.php
'stores' => [
    'lockout_redis' => [
        'driver' => 'redis',
        'connection' => 'lockout',
    ],
],

// config/database.php
'redis' => [
    'lockout' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'database' => 2, // Dedicated database
    ],
],
```

Testing
-------

[](#testing)

The package includes comprehensive test coverage. Run tests with:

```
composer test
```

### Testing Lockouts in Your App

[](#testing-lockouts-in-your-app)

```
class LoginTest extends TestCase
{
    public function test_user_gets_locked_out_after_failures()
    {
        // Simulate multiple failed attempts
        for ($i = 0; $i < 3; $i++) {
            $this->post('/login', ['email' => 'test@example.com', 'password' => 'wrong']);
        }

        // Verify lockout is active
        $this->assertTrue(Lockout::isLockedOut('login', 'test@example.com'));

        // Test lockout response
        $response = $this->post('/login', ['email' => 'test@example.com', 'password' => 'correct']);
        $response->assertStatus(429);
    }
}
```

Performance Considerations
--------------------------

[](#performance-considerations)

- **Cache Efficiency**: Uses single cache key per context/user combination
- **TTL Optimization**: Automatic cleanup of expired lockouts
- **Memory Usage**: Minimal data storage per lockout entry
- **Lookup Speed**: O(1) cache lookups for lockout status

Security Best Practices
-----------------------

[](#security-best-practices)

1. **Rate Limiting**: Combine with Laravel's rate limiting for comprehensive protection
2. **IP Tracking**: Use IP-based lockouts for anonymous endpoints
3. **Context Separation**: Use different contexts for different authentication methods
4. **Cache Security**: Secure your cache store (Redis AUTH, etc.)
5. **Key Hashing**: Keys are automatically hashed for privacy

Troubleshooting
---------------

[](#troubleshooting)

### Common Issues

[](#common-issues)

**Lockouts not working:**

- Check context is enabled in config
- Verify cache store is working
- Ensure middleware is applied to routes

**Lockouts not clearing:**

- Check cache connectivity
- Verify context and key match exactly
- Use Artisan command to manually clear

**Wrong response format:**

- Check `response_mode` in context config
- Verify request headers for JSON detection
- Test with custom response callback

### Debug Mode

[](#debug-mode)

Enable debug logging:

```
// In your controller
Log::info('Lockout status', [
    'context' => 'login',
    'key' => $email,
    'info' => Lockout::getLockoutInfo('login', $email)
]);
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

License
-------

[](#license)

This package is open-sourced software licensed under the [MIT license](LICENSE.md).

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for version history and updates.

Support
-------

[](#support)

- **Documentation**: This README and inline code comments
- **Issues**: GitHub Issues for bug reports and feature requests
- **Discussions**: GitHub Discussions for questions and community support

---

About the Developer
-------------------

[](#about-the-developer)

**Joe Nassar**
Email:

**Made with ❤️ for the Laravel community**

###  Health Score

31

—

LowBetter than 68% of packages

Maintenance58

Moderate activity, may be stable

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

11

Last Release

279d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3589dfc22b1dade689cc332141ec39deeb4277d8fe98aef520919e80b68948cf?d=identicon)[joe-nassar-tech](/maintainers/joe-nassar-tech)

---

Top Contributors

[![joe-nassar-tech](https://avatars.githubusercontent.com/u/120932481?v=4)](https://github.com/joe-nassar-tech "joe-nassar-tech (16 commits)")

---

Tags

middlewarelaravelsecurityAuthenticationcacherate limitinglockoutexponentialbrute force protection

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/joe-nassar-tech-laravel-exponential-lockout/health.svg)

```
[![Health](https://phpackages.com/badges/joe-nassar-tech-laravel-exponential-lockout/health.svg)](https://phpackages.com/packages/joe-nassar-tech-laravel-exponential-lockout)
```

###  Alternatives

[spatie/laravel-responsecache

Speed up a Laravel application by caching the entire response

2.8k8.2M51](/packages/spatie-laravel-responsecache)[php-open-source-saver/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

8359.8M53](/packages/php-open-source-saver-jwt-auth)[alajusticia/laravel-logins

Session management in Laravel apps, user notifications on new access, support for multiple separate remember tokens, IP geolocation, User-Agent parser

2011.0k](/packages/alajusticia-laravel-logins)[hosseinhezami/laravel-permission-manager

Advanced permission manager for Laravel.

403.3k](/packages/hosseinhezami-laravel-permission-manager)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)[wnikk/laravel-access-rules

Simple system of ACR (access control rules) for Laravel, with roles, groups, unlimited inheritance and possibility of multiplayer use.

103.6k1](/packages/wnikk-laravel-access-rules)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
