Task 21: Implement Rate Limiting
Role
BackendOverview
Implement robust rate limiting on authentication endpoints to prevent brute force attacks, credential stuffing, and denial of service attempts. Use sliding window algorithm for accurate request counting.
Objectives
- Prevent brute force password attacks
- Protect against credential stuffing
- Mitigate denial of service (DoS) attempts
- Implement per-IP and per-user rate limits
- Provide clear feedback when limits are exceeded
- Log rate limit violations for security monitoring
Rate Limiting Strategy
Login Endpoint Rate Limits
| Limit Type | Threshold | Window | Action |
|---|---|---|---|
| Per IP | 10 attempts | 15 minutes | Block IP for 15 minutes |
| Per User | 5 attempts | 15 minutes | Lock account temporarily |
| Global | 1000 requests | 1 minute | Rate limit response |
Response When Limited
HTTP 429 Too Many Requests:
{
"success": false,
"error": {
"code": "TOO_MANY_ATTEMPTS",
"message": "Too many login attempts. Please try again in 15 minutes.",
"retryAfter": 900,
"limitType": "ip_based"
}
}
Response Headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000000
Retry-After: 900
Implementation (C# / ASP.NET Core)
1. Rate Limiting Middleware
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly IRateLimitService _rateLimitService;
private readonly ILogger<RateLimitingMiddleware> _logger;
public RateLimitingMiddleware(
RequestDelegate next,
IRateLimitService rateLimitService,
ILogger<RateLimitingMiddleware> logger)
{
_next = next;
_rateLimitService = rateLimitService;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Only apply to auth endpoints
if (!context.Request.Path.StartsWithSegments("/api/v1/auth"))
{
await _next(context);
return;
}
var ipAddress = GetClientIpAddress(context);
var endpoint = context.Request.Path.Value;
// Check IP-based rate limit
var ipLimit = await _rateLimitService.CheckIpLimitAsync(ipAddress, endpoint);
if (ipLimit.IsLimited)
{
_logger.LogWarning(
"Rate limit exceeded for IP {IpAddress} on endpoint {Endpoint}",
ipAddress,
endpoint
);
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.Response.Headers["X-RateLimit-Limit"] = ipLimit.Limit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = "0";
context.Response.Headers["X-RateLimit-Reset"] = ipLimit.ResetAt.ToString();
context.Response.Headers["Retry-After"] = ipLimit.RetryAfter.ToString();
await context.Response.WriteAsJsonAsync(new
{
success = false,
error = new
{
code = "TOO_MANY_ATTEMPTS",
message = $"Too many requests. Please try again in {ipLimit.RetryAfter} seconds.",
retryAfter = ipLimit.RetryAfter,
limitType = "ip_based"
}
});
return;
}
// Add rate limit headers
context.Response.OnStarting(() =>
{
context.Response.Headers["X-RateLimit-Limit"] = ipLimit.Limit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = ipLimit.Remaining.ToString();
context.Response.Headers["X-RateLimit-Reset"] = ipLimit.ResetAt.ToString();
return Task.CompletedTask;
});
await _next(context);
}
private string GetClientIpAddress(HttpContext context)
{
// Check for X-Forwarded-For header (proxy/load balancer)
if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedFor))
{
return forwardedFor.ToString().Split(',').First().Trim();
}
// Check for X-Real-IP header
if (context.Request.Headers.TryGetValue("X-Real-IP", out var realIp))
{
return realIp.ToString();
}
// Fall back to remote IP address
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
}
2. Rate Limit Service (Using Redis)
public interface IRateLimitService
{
Task<RateLimitResult> CheckIpLimitAsync(string ipAddress, string endpoint);
Task<RateLimitResult> CheckUserLimitAsync(string userId, string endpoint);
Task IncrementAttemptAsync(string key);
}
public class RateLimitService : IRateLimitService
{
private readonly IDistributedCache _cache;
private readonly ILogger<RateLimitService> _logger;
// Rate limit configuration
private const int LoginIpLimit = 10;
private const int LoginUserLimit = 5;
private const int WindowSeconds = 900; // 15 minutes
public RateLimitService(
IDistributedCache cache,
ILogger<RateLimitService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<RateLimitResult> CheckIpLimitAsync(string ipAddress, string endpoint)
{
var key = $"ratelimit:ip:{ipAddress}:{endpoint}";
return await CheckLimitAsync(key, LoginIpLimit);
}
public async Task<RateLimitResult> CheckUserLimitAsync(string userId, string endpoint)
{
var key = $"ratelimit:user:{userId}:{endpoint}";
return await CheckLimitAsync(key, LoginUserLimit);
}
private async Task<RateLimitResult> CheckLimitAsync(string key, int maxAttempts)
{
var attemptsStr = await _cache.GetStringAsync(key);
var attempts = string.IsNullOrEmpty(attemptsStr) ? 0 : int.Parse(attemptsStr);
var resetAt = DateTimeOffset.UtcNow.AddSeconds(WindowSeconds).ToUnixTimeSeconds();
var remaining = Math.Max(0, maxAttempts - attempts);
if (attempts >= maxAttempts)
{
// Get TTL to calculate retry after
var ttl = await GetTtlAsync(key);
return new RateLimitResult
{
IsLimited = true,
Limit = maxAttempts,
Remaining = 0,
ResetAt = resetAt,
RetryAfter = ttl > 0 ? ttl : WindowSeconds
};
}
return new RateLimitResult
{
IsLimited = false,
Limit = maxAttempts,
Remaining = remaining,
ResetAt = resetAt,
RetryAfter = 0
};
}
public async Task IncrementAttemptAsync(string key)
{
var current = await _cache.GetStringAsync(key);
if (string.IsNullOrEmpty(current))
{
// First attempt - set counter with expiry
await _cache.SetStringAsync(
key,
"1",
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(WindowSeconds)
}
);
}
else
{
// Increment counter
var attempts = int.Parse(current) + 1;
await _cache.SetStringAsync(key, attempts.ToString());
}
}
private async Task<int> GetTtlAsync(string key)
{
// Implementation depends on cache provider
// For Redis: TTL command
// For in-memory: calculate from expiry time
return WindowSeconds; // Fallback
}
}
public class RateLimitResult
{
public bool IsLimited { get; set; }
public int Limit { get; set; }
public int Remaining { get; set; }
public long ResetAt { get; set; }
public int RetryAfter { get; set; }
}
3. Configure in Startup
public void ConfigureServices(IServiceCollection services)
{
// Add distributed cache (Redis recommended)
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
options.InstanceName = "MicDots:";
});
services.AddScoped<IRateLimitService, RateLimitService>();
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<RateLimitingMiddleware>();
// ... other middleware
}
Alternative: ASP.NET Core Rate Limiting (Built-in)
For .NET 7+, you can use built-in rate limiting:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("login", opt =>
{
opt.PermitLimit = 10;
opt.Window = TimeSpan.FromMinutes(15);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 0;
});
});
app.UseRateLimiter();
// Apply to endpoint
[EnableRateLimiting("login")]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
// ...
}
Monitoring & Logging
Log Rate Limit Events
_logger.LogWarning(
"Rate limit exceeded. IP: {IpAddress}, Endpoint: {Endpoint}, Attempts: {Attempts}",
ipAddress,
endpoint,
attempts
);
Metrics to Track
- Total rate limit violations per hour
- Top IPs being rate limited
- Rate limit violations by endpoint
- Average requests per IP
- Locked user accounts due to rate limits
Acceptance Criteria
- IP-based rate limiting implemented (10 attempts / 15 min)
- User-based rate limiting implemented (5 attempts / 15 min)
- 429 response returned when limit exceeded
- Retry-After header included in 429 response
- X-RateLimit headers added to all auth responses
- Rate limit state stored in Redis/distributed cache
- Failed attempts are tracked and incremented
- Successful login resets rate limit counter
- Rate limits automatically expire after window
- Client IP correctly extracted (handles proxies)
- Rate limit violations are logged
- Metrics are tracked for monitoring
- Configuration is externalized (appsettings.json)
- Unit tests cover rate limiting logic
- Integration tests verify rate limiting behavior
Testing Checklist
Unit Tests
- Test rate limit calculation
- Test sliding window logic
- Test IP extraction from headers
- Test cache interactions
- Test TTL expiration
Integration Tests
- Make 10 requests - 11th should be blocked
- Wait for window to expire - should reset
- Test different IPs are tracked separately
- Test user limits work independently of IP limits
- Test headers are correct
Load Tests
- Test under high concurrent load
- Verify Redis performance
- Check for race conditions
Estimated Time
1 day (8 hours)
Dependencies
- Redis or distributed cache setup
- Task 18: Auth endpoint must exist
- Task 22: Login attempt tracking
Related Content
Related Tasks
- Task 18: Create Authentication API Endpoint
- Task 22: Add Login Attempt Tracking (Coming Soon)
- Task 29: Add Account Lockout (Coming Soon)