<?php
declare(strict_types=1);
namespace App\Security;
final class LimitResult
{
public function __construct(
public readonly bool $exceeded,
public readonly int $hits,
public readonly int $limit,
public readonly int $retryAfter
) {
}
}
final class SlidingWindowLimiter
{
public function __construct(
private \Redis $redis,
private int $limit = 5,
private int $windowSeconds = 60
) {
}
public function hit(string $key): LimitResult
{
$now = microtime(true);
$cutoff = $now - $this->windowSeconds;
$redisKey = 'throttle:' . $key;
$this->redis->multi(\Redis::PIPELINE);
$this->redis->zRemRangeByScore($redisKey, '0', (string) $cutoff);
$this->redis->zAdd($redisKey, $now, sprintf('%.6f-%s', $now, bin2hex(random_bytes(4))));
$this->redis->zCard($redisKey);
$this->redis->expire($redisKey, $this->windowSeconds);
$result = $this->redis->exec();
$hits = (int) ($result[2] ?? 0);
return new LimitResult(
exceeded: $hits > $this->limit,
hits: $hits,
limit: $this->limit,
retryAfter: $this->windowSeconds
);
}
}
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Security\SlidingWindowLimiter;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Nyholm\Psr7\Factory\Psr17Factory;
final class ThrottleLoginMiddleware implements MiddlewareInterface
{
public function __construct(
private SlidingWindowLimiter $limiter,
private Psr17Factory $responses,
private bool $trustForwardedFor = false
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (strtoupper($request->getMethod()) !== 'POST') {
return $handler->handle($request);
}
$body = (array) $request->getParsedBody();
$username = strtolower(trim((string) ($body['username'] ?? 'unknown')));
$key = $this->clientIp($request) . '|' . $username;
$result = $this->limiter->hit($key);
if ($result->exceeded) {
return $this->responses->createResponse(429)
->withHeader('Retry-After', (string) $result->retryAfter)
->withHeader('Content-Type', 'application/json')
->withBody($this->responses->createStream(
json_encode(['error' => 'Too many login attempts. Try again later.'])
));
}
return $handler->handle($request);
}
private function clientIp(ServerRequestInterface $request): string
{
if ($this->trustForwardedFor) {
$forwarded = $request->getHeaderLine('X-Forwarded-For');
if ($forwarded !== '') {
return trim(explode(',', $forwarded)[0]);
}
}
return $request->getServerParams()['REMOTE_ADDR'] ?? '0.0.0.0';
}
}
<?php
declare(strict_types=1);
use App\Http\Controllers\SessionController;
use App\Http\Middleware\ThrottleLoginMiddleware;
use App\Security\SlidingWindowLimiter;
use Nyholm\Psr7\Factory\Psr17Factory;
$redis = new Redis();
$redis->connect((string) getenv('REDIS_HOST') ?: '127.0.0.1', 6379);
$limiter = new SlidingWindowLimiter($redis, limit: 5, windowSeconds: 60);
$throttle = new ThrottleLoginMiddleware($limiter, new Psr17Factory(), trustForwardedFor: true);
$router->get('/login', [SessionController::class, 'showForm']);
$router->post('/login', [SessionController::class, 'authenticate'])
->middleware($throttle);
$router->post('/logout', [SessionController::class, 'destroy']);
This snippet implements per-identity rate limiting for a login route using a PSR-15 middleware backed by Redis. The goal is to slow down credential-stuffing and brute-force attacks without locking out legitimate users, and to do it in a place that every login request must pass through regardless of controller logic.
The SlidingWindowLimiter class in the first tab implements the counting strategy. A simple fixed-window counter (one key per minute) suffers from a burst problem: an attacker can send the full quota at the end of one window and again at the start of the next, effectively doubling the allowed rate at the boundary. To avoid that, hit uses a Redis sorted set where each request is stored with its timestamp as the score. On each call it removes entries older than the window with zRemRangeByScore, adds the current request via zAdd, and reads the count with zCard. The whole sequence runs inside a MULTI/EXEC pipeline so concurrent requests cannot interleave and undercount, and an expire keeps the key from lingering. The returned LimitResult exposes whether the caller exceeded the limit plus a retryAfter so callers can advise clients when to try again.
The ThrottleLoginMiddleware in the second tab wires the limiter into the request pipeline. It builds a composite key from the client IP and the submitted username, so an attacker rotating usernames from one IP and one rotating IP against one account are both constrained. When the limit is exceeded it short-circuits with a 429 Too Many Requests, sets a Retry-After header, and never touches the authentication handler. Note that it only throttles POST requests to the login path — GET requests that render the form pass straight through.
The third tab, routes.php, shows the middleware bound only to the login endpoint rather than applied globally, which keeps the hot path for normal traffic cheap.
A key trade-off is that keying on IP is imperfect behind NAT or shared proxies, so clientIp prefers a trusted forwarded header when configured. The sorted-set approach costs more memory than a plain counter because it stores one member per request, which is why the window and limit should stay small. For production, pairing this network-layer throttle with progressive delays or CAPTCHA after repeated failures gives defense in depth.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
Share this code
Here's the card — post it anywhere.