- 
      
 - 
        
Save volkar1/0215e1416c5a2d9e413242ee9b253a6f to your computer and use it in GitHub Desktop.  
    Track login attempts and prevent brute forcing
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | <?php | |
| declare(strict_types=1); | |
| namespace App\Security; | |
| use DateTimeImmutable; | |
| use Generator; | |
| use Predis\Client; | |
| class Lockout | |
| { | |
| private const KEY_ATTEMPT = 'lockout:%s:attempts'; | |
| private const KEY_LOCK = 'lockout:%s:lock'; | |
| /** | |
| * @var Client | |
| */ | |
| private $redis; | |
| /** | |
| * @var int | |
| */ | |
| private $window; | |
| /** | |
| * @var int | |
| */ | |
| private $threshold; | |
| /** | |
| * @var int | |
| */ | |
| private $duration; | |
| public function __construct(Client $redis, int $window, int $threshold, int $duration) | |
| { | |
| $this->redis = $redis; | |
| $this->window = $window; | |
| $this->threshold = $threshold; | |
| $this->duration = $duration; | |
| } | |
| public function locked(string $key): bool | |
| { | |
| return (bool) $this->redis->exists(sprintf(self::KEY_LOCK, $key)); | |
| } | |
| /** | |
| * @return int seconds until lock expires, -1 means no lock exists | |
| */ | |
| public function expires(string $key): int | |
| { | |
| $expires = $this->redis->ttl(sprintf(self::KEY_LOCK, $key)); | |
| return max($expires, -1); | |
| } | |
| public function attempts(string $key): array | |
| { | |
| $now = new DateTimeImmutable(); | |
| $attempts = $this->redis->zrange(sprintf(self::KEY_ATTEMPT, $key), 0, -1, ['WITHSCORES' => true]); | |
| return array_map(function ($ts, $data) use ($now) { | |
| $attempt = json_decode($data, true); | |
| $attempt['timestamp'] = $now->setTimestamp($ts / 1000); | |
| return $attempt; | |
| }, $attempts, array_keys($attempts)); | |
| } | |
| /** | |
| * @return Generator<string, array> | |
| */ | |
| public function all(): Generator | |
| { | |
| foreach ($this->redis->keys(sprintf(self::KEY_LOCK, '*')) as $key) { | |
| $id = explode(':', $key)[1]; | |
| yield $id => $this->attempts($id); | |
| } | |
| } | |
| public function add(string $key, array $data = []): bool | |
| { | |
| $now = (int) round(microtime(true) * 1000); | |
| $redisKey = sprintf(self::KEY_ATTEMPT, $key); | |
| $rangeStart = $now - ($this->window * 1000); | |
| $data['_'] = bin2hex(random_bytes(4)); | |
| $results = $this->redis->transaction() | |
| ->zadd($redisKey, $now, json_encode($data)) | |
| ->expire($redisKey, $this->duration) | |
| ->zrangebyscore($redisKey, $rangeStart, $now, ['WITHSCORES' => true]) | |
| ->execute(); | |
| $attempts = array_pop($results); | |
| if (count($attempts) < $this->threshold) { | |
| return false; | |
| } | |
| $this->lock($key); | |
| return true; | |
| } | |
| public function lock(string $key): void | |
| { | |
| $this->redis->setex(sprintf(self::KEY_LOCK, $key), $this->duration, true); | |
| } | |
| public function unlock(string $key): void | |
| { | |
| $this->redis->del([sprintf(self::KEY_LOCK, $key)]); | |
| } | |
| } | |
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment