Files
tiku-backend.net/Tiku.IntegrationTests/Api/AuthRateLimitPolicyTests.cs

165 lines
5.9 KiB
C#

using System.Net;
using System.Reflection;
using System.Text;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Configuration;
using Tiku.Api.Controllers;
using Tiku.Api.Middleware;
using Tiku.Api.Options;
namespace Tiku.IntegrationTests.Api;
public sealed class AuthRateLimitPolicyTests
{
[Fact]
public void Authentication_rate_limits_bind_from_the_named_configuration_section()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"{AuthRateLimitOptions.SectionName}:PasswordPermitLimit"] = "7",
[$"{AuthRateLimitOptions.SectionName}:PasswordWindowSeconds"] = "600",
[$"{AuthRateLimitOptions.SectionName}:SmsPermitLimit"] = "3",
[$"{AuthRateLimitOptions.SectionName}:SmsWindowSeconds"] = "90"
})
.Build();
var options = configuration
.GetSection(AuthRateLimitOptions.SectionName)
.Get<AuthRateLimitOptions>();
Assert.NotNull(options);
Assert.Equal(7, options.PasswordPermitLimit);
Assert.Equal(600, options.PasswordWindowSeconds);
Assert.Equal(3, options.SmsPermitLimit);
Assert.Equal(90, options.SmsWindowSeconds);
}
[Fact]
public void Authentication_rate_limit_values_must_be_positive()
{
var options = new AuthRateLimitOptions
{
PasswordPermitLimit = 0,
SmsWindowSeconds = 0
};
var validationResults = new List<ValidationResult>();
var valid = Validator.TryValidateObject(
options,
new ValidationContext(options),
validationResults,
validateAllProperties: true);
Assert.False(valid);
Assert.Equal(2, validationResults.Count);
}
[Fact]
public void Password_login_uses_the_password_named_policy()
{
AssertPolicy(nameof(AuthController.LoginWithPassword), AuthRateLimitPolicies.Password);
}
[Fact]
public void Sms_send_uses_the_sms_named_policy()
{
AssertPolicy(nameof(AuthController.SendSmsCode), AuthRateLimitPolicies.Sms);
}
[Fact]
public void Required_password_change_uses_the_password_named_policy()
{
AssertPolicy(nameof(AuthController.ChangeRequiredPassword), AuthRateLimitPolicies.Password);
}
[Fact]
public async Task Password_partition_combines_account_and_ip_without_exposing_the_account()
{
var first = await CapturePartitionAsync(
AuthRateLimitPolicies.Password,
"""{"Phone":"13800000000","password":"secret"}""",
"127.0.0.1");
var same = await CapturePartitionAsync(
AuthRateLimitPolicies.Password,
"""{"phone":"13800000000","password":"different"}""",
"127.0.0.1");
var differentAccount = await CapturePartitionAsync(
AuthRateLimitPolicies.Password,
"""{"phone":"13900000000","password":"secret"}""",
"127.0.0.1");
var differentIp = await CapturePartitionAsync(
AuthRateLimitPolicies.Password,
"""{"phone":"13800000000","password":"secret"}""",
"127.0.0.2");
Assert.Equal(first, same);
Assert.NotEqual(first, differentAccount);
Assert.NotEqual(first, differentIp);
Assert.DoesNotContain("13800000000", first, StringComparison.Ordinal);
Assert.DoesNotContain("secret", first, StringComparison.Ordinal);
}
[Fact]
public async Task Sms_partition_combines_phone_and_ip_without_exposing_the_phone()
{
var first = await CapturePartitionAsync(
AuthRateLimitPolicies.Sms,
"""{"phone":"13800000000","deviceId":"device-one"}""",
"127.0.0.1");
var differentPhone = await CapturePartitionAsync(
AuthRateLimitPolicies.Sms,
"""{"phone":"13900000000","deviceId":"device-one"}""",
"127.0.0.1");
var differentIp = await CapturePartitionAsync(
AuthRateLimitPolicies.Sms,
"""{"phone":"13800000000","deviceId":"device-one"}""",
"127.0.0.2");
Assert.NotEqual(first, differentPhone);
Assert.NotEqual(first, differentIp);
Assert.DoesNotContain("13800000000", first, StringComparison.Ordinal);
}
private static void AssertPolicy(string methodName, string expectedPolicy)
{
var method = typeof(AuthController).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
var attribute = method?.GetCustomAttribute<EnableRateLimitingAttribute>();
Assert.NotNull(attribute);
Assert.Equal(expectedPolicy, attribute.PolicyName);
}
private static async Task<string> CapturePartitionAsync(
string policyName,
string json,
string ipAddress)
{
var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = IPAddress.Parse(ipAddress);
context.Request.Method = HttpMethods.Post;
context.Request.ContentType = "application/json";
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
context.SetEndpoint(new Endpoint(
_ => Task.CompletedTask,
new EndpointMetadataCollection(new EnableRateLimitingAttribute(policyName)),
"auth-rate-limit-test"));
string? partition = null;
var middleware = new AuthRateLimitPartitionMiddleware(async nextContext =>
{
partition = AuthRateLimitPartitionKey.Resolve(nextContext, policyName);
using var reader = new StreamReader(
nextContext.Request.Body,
Encoding.UTF8,
leaveOpen: true);
Assert.Equal(json, await reader.ReadToEndAsync());
});
await middleware.InvokeAsync(context);
return Assert.IsType<string>(partition);
}
}