File: AntiforgeryMiddlewareTest.cs
Web Access
Project: src\src\Mvc\test\Mvc.FunctionalTests\Microsoft.AspNetCore.Mvc.FunctionalTests.csproj (Microsoft.AspNetCore.Mvc.FunctionalTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Mvc.FunctionalTests;
 
public class AntiforgeryMiddlewareTest
{
    [Fact]
    public async Task Works_WithAntiforgeryMetadata_ValidToken()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddMvcCore().UseSpecificControllers(typeof(TestController));
        builder.Services.AddAntiforgery();
        builder.WebHost.UseTestServer();
        await using var app = builder.Build();
        app.UseAntiforgery();
        app.MapControllers();
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
        var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
        var antiforgeryOptions = app.Services.GetRequiredService<IOptions<AntiforgeryOptions>>();
        var tokens = antiforgery.GetAndStoreTokens(new DefaultHttpContext());
 
        var request = new HttpRequestMessage(HttpMethod.Post, "/Test/PostWithRequireAntiforgeryToken");
        request.Headers.Add("Cookie", antiforgeryOptions.Value.Cookie.Name + "=" + tokens.CookieToken);
        var nameValueCollection = new List<KeyValuePair<string, string>>
        {
            new("__RequestVerificationToken", tokens.RequestToken),
            new("name", "Test task"),
            new("isComplete", "false"),
            new("dueDate", DateTime.Today.AddDays(1).ToString(CultureInfo.CurrentCulture)),
        };
        request.Content = new FormUrlEncodedContent(nameValueCollection);
        var result = await client.SendAsync(request);
        result.EnsureSuccessStatusCode();
    }
 
    [Fact]
    public async Task Works_WithAntiforgeryMetadata_AndFilterAttribute_ValidToken()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddMvcCore().UseSpecificControllers(typeof(TestWithBothAttributesController)).AddViews();
        builder.Services.AddAntiforgery();
        builder.WebHost.UseTestServer();
        await using var app = builder.Build();
        app.UseAntiforgery();
        var exception = Assert.Throws<InvalidOperationException>(() => app.MapControllers());
 
        Assert.Equal("Cannot apply [ValidateAntiForgeryTokenAttribute] and [RequireAntiforgeryTokenAttribute] at the same time.", exception.Message);
    }
 
    [Fact]
    public async Task Works_WithAntiforgeryMetadata_InvalidToken()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddMvcCore().UseSpecificControllers(typeof(TestController)).AddViews();
        builder.Services.AddAntiforgery();
        builder.WebHost.UseTestServer();
        await using var app = builder.Build();
        app.UseAntiforgery();
        app.MapControllers();
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
 
        var request = new HttpRequestMessage(HttpMethod.Post, "/Test/PostWithRequireAntiforgeryToken");
        var nameValueCollection = new List<KeyValuePair<string, string>>
        {
            new("name", "Test task"),
            new("isComplete", "false"),
            new("dueDate", DateTime.Today.AddDays(1).ToString(CultureInfo.CurrentCulture)),
        };
        request.Content = new FormUrlEncodedContent(nameValueCollection);
        var result = await client.SendAsync(request);
        Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
    }
 
    [Fact]
    public async Task Works_WithAntiforgeryMetadata_ValidToken_RequestSizeLimit()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddMvcCore().UseSpecificControllers(typeof(TestController));
        builder.Services.AddAntiforgery();
        builder.WebHost.UseTestServer();
        await using var app = builder.Build();
        app.Use((context, next) =>
        {
            context.Features.Set<IHttpMaxRequestBodySizeFeature>(new FakeHttpMaxRequestBodySizeFeature(5_000_000));
            return next(context);
        });
        app.UseRouting();
        app.Use((context, next) =>
        {
            context.Request.Body = new SizeLimitedStream(context.Request.Body, context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize);
            return next(context);
        });
        app.UseAntiforgery();
        app.MapControllers();
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
        var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
        var antiforgeryOptions = app.Services.GetRequiredService<IOptions<AntiforgeryOptions>>();
        var tokens = antiforgery.GetAndStoreTokens(new DefaultHttpContext());
 
        var request = new HttpRequestMessage(HttpMethod.Post, "/Test/PostWithRequireAntiforgeryTokenAndSizeLimit");
        request.Headers.Add("Cookie", antiforgeryOptions.Value.Cookie.Name + "=" + tokens.CookieToken);
        var nameValueCollection = new List<KeyValuePair<string, string>>
        {
            new("__RequestVerificationToken", tokens.RequestToken),
            new("name", "Test task"),
            new("isComplete", "false"),
            new("dueDate", DateTime.Today.AddDays(1).ToString(CultureInfo.CurrentCulture)),
        };
        request.Content = new FormUrlEncodedContent(nameValueCollection);
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await client.SendAsync(request));
        Assert.Equal("The maximum number of bytes have been read.", exception.Message);
    }
 
    [Fact]
    public async Task Throws_WithAntiforgeryMetadata_ValidToken_RequestFormLimits()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddMvcCore().UseSpecificControllers(typeof(TestController)).AddViews();
        builder.Services.AddAntiforgery();
        builder.WebHost.UseTestServer();
        await using var app = builder.Build();
        app.UseAntiforgery();
        app.MapControllers();
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
        var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
        var antiforgeryOptions = app.Services.GetRequiredService<IOptions<AntiforgeryOptions>>();
        var tokens = antiforgery.GetAndStoreTokens(new DefaultHttpContext());
 
        var request = new HttpRequestMessage(HttpMethod.Post, "/Test/PostWithRequireAntiforgeryTokenAndFormLimit");
        request.Headers.Add("Cookie", antiforgeryOptions.Value.Cookie.Name + "=" + tokens.CookieToken);
        var nameValueCollection = new List<KeyValuePair<string, string>>
        {
            new("__RequestVerificationToken", tokens.RequestToken),
            new("name", "Test task"),
            new("isComplete", "false"),
            new("dueDate", DateTime.Today.AddDays(1).ToString(CultureInfo.CurrentCulture)),
        };
        request.Content = new FormUrlEncodedContent(nameValueCollection);
        var result = await client.SendAsync(request);
        Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
    }
 
    [Fact]
    public async Task Works_WithAntiforgeryMetadata_ValidToken_DisableRequestSizeLimits()
    {
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddMvcCore().UseSpecificControllers(typeof(TestWithRequestSizeLimitController));
        builder.Services.AddAntiforgery();
        builder.WebHost.UseTestServer();
        await using var app = builder.Build();
        app.UseAntiforgery();
        app.MapControllers();
 
        await app.StartAsync();
 
        var client = app.GetTestClient();
        var antiforgery = app.Services.GetRequiredService<IAntiforgery>();
        var antiforgeryOptions = app.Services.GetRequiredService<IOptions<AntiforgeryOptions>>();
        var tokens = antiforgery.GetAndStoreTokens(new DefaultHttpContext());
 
        var request = new HttpRequestMessage(HttpMethod.Post, "/TestWithRequestSizeLimit/PostWithRequireAntiforgeryTokenAndDisableSizeLimit");
        request.Headers.Add("Cookie", antiforgeryOptions.Value.Cookie.Name + "=" + tokens.CookieToken);
        var nameValueCollection = new List<KeyValuePair<string, string>>
        {
            new("__RequestVerificationToken", tokens.RequestToken),
            new("name", "Test task"),
            new("isComplete", "false"),
            new("dueDate", DateTime.Today.AddDays(1).ToString(CultureInfo.CurrentCulture)),
        };
        request.Content = new FormUrlEncodedContent(nameValueCollection);
        var result = await client.SendAsync(request);
        result.EnsureSuccessStatusCode();
    }
 
    [Route("[controller]/[action]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        [HttpPost]
        [RequireAntiforgeryToken]
        public ActionResult PostWithRequireAntiforgeryToken([FromForm] Todo todo)
            => new OkObjectResult(todo);
 
        [HttpPost]
        [RequireAntiforgeryToken]
        [RequestSizeLimit(4)]
        public ActionResult PostWithRequireAntiforgeryTokenAndSizeLimit([FromForm] Todo todo)
            => new OkObjectResult(todo);
 
        [HttpPost]
        [RequireAntiforgeryToken]
        [RequestFormLimits(ValueCountLimit = 2)]
        public ActionResult PostWithRequireAntiforgeryTokenAndFormLimit([FromForm] Todo todo)
            => new OkObjectResult(todo);
    }
 
    [Route("[controller]/[action]")]
    [ApiController]
    public class TestWithBothAttributesController : ControllerBase
    {
        [HttpPost]
        [RequireAntiforgeryToken]
        [ValidateAntiForgeryToken]
        public ActionResult PostWithRequireAntiforgeryTokenAndFilterAttribute([FromForm] Todo todo)
            => new OkObjectResult(todo);
    }
 
    [Route("[controller]/[action]")]
    [ApiController]
    [RequestSizeLimit(4)]
    public class TestWithRequestSizeLimitController : ControllerBase
    {
        [HttpPost]
        [RequireAntiforgeryToken]
        public ActionResult PostWithRequireAntiforgeryTokenAndDisableSizeLimit([FromForm] Todo todo)
            => new OkObjectResult(todo);
    }
 
    public class Todo
    {
        public string Name { get; set; } = string.Empty;
        public bool IsCompleted { get; set; } = false;
        public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
    }
 
    public class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
    {
        public FakeHttpMaxRequestBodySizeFeature(
            long? maxRequestBodySize = null,
            bool isReadOnly = false)
        {
            MaxRequestBodySize = maxRequestBodySize;
            IsReadOnly = isReadOnly;
        }
        public bool IsReadOnly { get; }
        public long? MaxRequestBodySize { get; set; }
    }
}
 
internal static class MvCoreBuilderExtensions
{
    internal static void UseSpecificControllers(
        this ApplicationPartManager partManager,
        params Type[] controllerTypes)
    {
        partManager.FeatureProviders.Add(new TestControllerFeatureProvider());
        partManager.ApplicationParts.Clear();
        partManager.ApplicationParts.Add(new SelectedControllersApplicationParts(controllerTypes));
    }
 
    internal static IMvcCoreBuilder UseSpecificControllers(
        this IMvcCoreBuilder mvcCoreBuilder,
        params Type[] controllerTypes) => mvcCoreBuilder
        .ConfigureApplicationPartManager(partManager => partManager.UseSpecificControllers(controllerTypes));
}
 
internal class SelectedControllersApplicationParts(Type[] types) : ApplicationPart, IApplicationPartTypeProvider
{
    public override string Name { get; } = string.Empty;
 
    public IEnumerable<TypeInfo> Types { get; } = types.Select(x => x.GetTypeInfo()).ToArray();
}
 
internal class TestControllerFeatureProvider : ControllerFeatureProvider
{
    // Default controller feature provider doesn't support nested controller classes
    // so we override that here
    protected override bool IsController(TypeInfo typeInfo) => true;
}