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