Skip to main content

Task 21: Implement Rate Limiting

Role​

Backend

Overview​

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 TypeThresholdWindowAction
Per IP10 attempts15 minutesBlock IP for 15 minutes
Per User5 attempts15 minutesLock account temporarily
Global1000 requests1 minuteRate 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

External Resources​