using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; namespace Baya.WebFramework.ServiceConfiguration; public static class RateLimitingServiceExtension { /// Per-IP baseline applied to every endpoint that doesn't opt into a named policy. public const string GlobalPolicy = "global"; /// Tighter limit for OTP request/verify endpoints (applied in backend-phase-2). public const string OtpPolicy = "otp"; /// Limit for login/refresh and other auth endpoints. public const string AuthPolicy = "auth"; /// Limit for money-sensitive actions (refund/payout) applied in later phases. public const string SensitivePolicy = "sensitive"; /// /// Registers the built-in rate limiter with a per-IP global limit plus named policies that /// auth/OTP/sensitive endpoints opt into via [EnableRateLimiting(name)]. Over-limit /// requests get 429 Too Many Requests. Pair with app.UseRateLimiter() placed /// before app.UseAuthentication(). /// public static IServiceCollection AddRateLimitingPolicies(this IServiceCollection services) { services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.GlobalLimiter = PartitionedRateLimiter.Create(context => RateLimitPartition.GetFixedWindowLimiter( PartitionKey(context), _ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 })); AddFixedWindowPolicy(options, OtpPolicy, permitLimit: 5, windowSeconds: 60); AddFixedWindowPolicy(options, AuthPolicy, permitLimit: 10, windowSeconds: 60); AddFixedWindowPolicy(options, SensitivePolicy, permitLimit: 20, windowSeconds: 60); // A deliberately tiny policy used by the phase-0 ping endpoint to demonstrate 429s. AddFixedWindowPolicy(options, GlobalPolicy, permitLimit: 5, windowSeconds: 10); }); return services; } private static void AddFixedWindowPolicy(RateLimiterOptions options, string name, int permitLimit, int windowSeconds) { options.AddPolicy(name, context => RateLimitPartition.GetFixedWindowLimiter( PartitionKey(context), _ => new FixedWindowRateLimiterOptions { PermitLimit = permitLimit, Window = TimeSpan.FromSeconds(windowSeconds), QueueLimit = 0 })); } private static string PartitionKey(HttpContext context) => context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; }