如何在需要身份验证和防伪令牌验证的 ASP.NET Core API 控制器端点上执行集成测试

问题描述 投票:0回答:1

描述

我有一个 ASP.NET Core API 控制器端点,需要:

  • 经过身份验证的用户,并且
  • 防伪令牌的验证

我想对此端点执行集成测试。

问题

我无法发送同时具有经过身份验证的用户和必要的防伪令牌/cookie 和身份验证 cookie 的请求。因此,端点在到达处理程序之前会继续返回

Bad Request

问题

如何在需要身份验证和防伪令牌验证的端点上执行集成测试?

代码

为了帮助解决这个问题,我创建了一个示例应用程序来演示我遇到的问题。

API 控制器端点

示例应用程序有一个 API 控制器,带有四个 POST 端点:

1。未经身份验证(匿名)端点 - 不需要防伪验证

[AllowAnonymous]
[HttpPost("Anonymous/{name}")]
public IActionResult AnonymousPost(string name)
{
    return Ok(name);
}

2。经过身份验证的端点 - 不需要防伪验证

[HttpPost("Authenticated/{name}")]
public IActionResult AuthenticatedPost(string name)
{
    return Ok(name);
}

3.未经身份验证(匿名)端点 - 需要防伪验证

[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost("Anonymous/Antiforgery/{name}")]
public IActionResult AnonymousAntiforgeryPost(string name)
{
    return Ok(name);
}

4。经过身份验证的端点 - 需要防伪验证

[ValidateAntiForgeryToken]
[HttpPost("Authenticated/Antiforgery/{name}")]
public IActionResult AuthenticatedAntiforgeryPost(string name)
{
    return Ok(name);
}

端点#4,需要经过身份验证的用户以及防伪令牌的验证,是我无法成功测试的端点。

认证

应用程序使用 cookie 身份验证并需要经过身份验证的用户。

// Add authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityConstants.ApplicationScheme;
}).AddCookie(IdentityConstants.ApplicationScheme, options =>
    {
        options.LoginPath = new PathString("/Login");
    }).AddTwoFactorRememberMeCookie();

// Add authorization
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

测试项目配置测试认证方案

public static IWebHostBuilder ConfigureTestAuthenticationScheme(this IWebHostBuilder builder, string scheme)
{
    ArgumentNullException.ThrowIfNull(builder);

    return builder.ConfigureTestServices(services =>
    {
        services.AddAuthentication(defaultScheme: "TestScheme")
        .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestScheme", options => { });
    });
}

其中

TestAuthHandler
继承自
AuthenticatonHandler
并重写
HandleAuthenticateAsync
方法。

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
    Claim[] claims = 
    [
        new Claim(ClaimTypes.Name, "testuser"),
        new Claim(ClaimTypes.NameIdentifier, "testuser")
    ];
    ClaimsIdentity identity = new (claims, "Test");
    ClaimsPrincipal principal = new (identity);
    AuthenticationTicket ticket = new (principal, "TestScheme");

    AuthenticateResult result = AuthenticateResult.Success(ticket);

    return Task.FromResult(result);
}

可以为测试创建经过身份验证的客户端,如下所示

public HttpClient GetAuthenticatedClient(CookieContainerHandler? cookieHandler = default)
{
    cookieHandler ??= new();

    string testScheme = "TestScheme";

    HttpClient client = WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestAuthenticationScheme(testScheme);
    })
    .CreateDefaultClient(cookieHandler);
    
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(scheme: testScheme);

    return client;
}

防伪

ValidateAntiForgeryToken
属性用于需要防伪令牌验证的两个端点(#3 和 #4)。

测试项目在 AntiforgeryController

 上添加了 
IWebHostBuilder
,它返回一个包含有效防伪令牌的 JSON 对象。

public static IWebHostBuilder ConfigureAntiforgeryTokenResource(this IWebHostBuilder builder)
{
    ArgumentNullException.ThrowIfNull(builder);

    return builder.ConfigureTestServices((services) =>
    {
        services.AddControllers()
                .AddApplicationPart(typeof(AntiforgeryTokenController).Assembly);
    });
}
public IActionResult GetAntiforgeryTokens(
    [FromServices] IAntiforgery antiforgery,
    [FromServices] IOptions<AntiforgeryOptions> options)
{
    ArgumentNullException.ThrowIfNull(antiforgery);
    ArgumentNullException.ThrowIfNull(options);

    AntiforgeryTokenSet tokens = antiforgery.GetTokens(HttpContext);

    AntiforgeryTokens model = new()
    {
        CookieName = options.Value!.Cookie!.Name!,
        CookieValue = tokens.CookieToken!,
        FormFieldName = options.Value.FormFieldName,
        HeaderName = tokens.HeaderName!,
        RequestToken = tokens.RequestToken!
    };

    return Json(model);
}

CustomWebApplicationFactory
提供了一种方法
GetAntiForgeryTokensAsync
,用于在测试方法中 ping
AntiforgeryTokenController

public async Task<AntiforgeryTokens> GetAntiforgeryTokensAsync(
    Func<HttpClient>? httpClientFactory = null,
    CancellationToken cancellationToken = default)
{
    using HttpClient httpClient = httpClientFactory?.Invoke() ?? CreateDefaultClient();

    AntiforgeryTokens? tokens = await httpClient.GetFromJsonAsync<AntiforgeryTokens>(
        AntiforgeryTokenController.GetTokensUri,
        cancellationToken);

    return tokens!;
}

集成测试

我能够成功测试前三个端点,但是,当我需要测试第四个端点时,需要经过身份验证的用户和防伪令牌验证,则会返回

Bad Request

1。测试未经身份验证(匿名)的端点 - 不需要防伪验证

public async Task Unauthenticated_request_to_anonymous_endpoint_returns_ok()
{
    // Arrange
    HttpRequestMessage message = new()
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("/api/anonymous/testname", UriKind.Relative)
    };

    // Act
    HttpResponseMessage response = await _client.SendAsync(message);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);
}

2。测试经过身份验证的端点 - 不需要防伪验证

public async Task Authenticated_request_to_autheticated_endpoint_returns_ok()
{
    // Arrange
    HttpClient client = _factory.GetAuthenticatedClient();

    HttpRequestMessage message = new()
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("/api/authenticated/testname", UriKind.Relative)
    };

    // Act
    HttpResponseMessage response = await client.SendAsync(message);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);
}

3.测试未经身份验证(匿名)的端点 - 需要防伪验证

public async Task Unauthenticated_request_to_anonymous_antiforgery_endpoint_with_tokens_returns_ok()
{
    // Arrange
    AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync();

    CookieContainerHandler cookieHandler = new();
    cookieHandler.Container.Add(
        _factory.Server.BaseAddress,
        new Cookie(tokens.CookieName, tokens.CookieValue));

    HttpClient client = _factory.CreateDefaultClient(cookieHandler);

    client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
    
    HttpRequestMessage message = new()
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("/api/anonymous/antiforgery/testname", UriKind.Relative)
    };

    // Act
    HttpResponseMessage response = await client.SendAsync(message);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);   
}

4。测试经过身份验证的端点 - 需要防伪验证

我读过浏览器会自动从服务器响应标头中提取任何 cookie,并将它们附加到下一个请求。为了通过 CSRF 进行成功验证,需要对此进行模拟。因此,此测试调用

GetAuthenticationCookies
方法,该方法登录应用程序并从响应中提取身份验证 cookie。

public async Task<List<string>> GetAuthenticationCookies(CookieContainerHandler cookieHandler, AntiforgeryTokens tokens)
{
    CancellationToken cancellationToken = new CancellationTokenSource().Token;

    HttpClient client = _factory.CreateDefaultClient(cookieHandler);

    Uri uri = new($"{client.BaseAddress!.AbsoluteUri}login");

    Dictionary<string, string> postData = new()
    {
        { "Input.UserName", "testuser" },
        { "Input.Password", "password" },
        { tokens!.FormFieldName, tokens.RequestToken }
    };

    HttpContent formContent = new FormUrlEncodedContent(postData);

    HttpResponseMessage response = await client.PostAsync(uri, formContent, cancellationToken);

    return response.Headers.GetValues("Set-Cookie").ToList();
}

然后,除了防伪令牌之外,测试还将这些 cookie 添加到客户端请求标头中。

public async Task Authenticated_request_to_authenticated_antiforgery_endpoint_with_tokens_returns_ok()
{
    // Arrange
    AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync();

    CookieContainerHandler cookieHandler = new();
    cookieHandler.Container.Add(
        _factory.Server.BaseAddress,
        new Cookie(tokens.CookieName, tokens.CookieValue));

    HttpClient client = _factory.GetAuthenticatedClient(cookieHandler);

    List<string> cookies = await GetAuthenticationCookies(cookieHandler, tokens);

    client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
    client.DefaultRequestHeaders.Add("Cookie", cookies);

    HttpRequestMessage message = new()
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("/api/authenticated/antiforgery/testname", UriKind.Relative)
    };

    // Act
    HttpResponseMessage response = await client.SendAsync(message);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.OK);   
}

不幸的是,此测试返回

Bad Request
,并且永远不会到达处理程序。

c# asp.net-core integration-testing antiforgerytoken
1个回答
0
投票

您遇到的错误来自这样一个事实:在您的测试中,防伪令牌是在未经身份验证的情况下获取的,因此当向您的 API 发出经过身份验证的请求时,防伪令牌无效。

解决此问题的一种方法是更改 GetAntiforgeryTokensAsync 以使用基于参数的经过身份验证的客户端:

    public async Task<AntiforgeryTokens> GetAntiforgeryTokensAsync(
        Func<HttpClient>? httpClientFactory = null,
        bool isAuthenticated = false,
        CancellationToken cancellationToken = default)
    {
        using HttpClient httpClient = isAuthenticated 
            ? this.GetAuthenticatedClient() 
            : (httpClientFactory?.Invoke() ?? CreateDefaultClient());

        AntiforgeryTokens? tokens = await httpClient.GetFromJsonAsync<AntiforgeryTokens>(
            AntiforgeryTokenController.GetTokensUri,
            cancellationToken);

        return tokens!;
    }

然后您可以更改需要经过身份验证的防伪令牌的测试来设置此参数

    [Fact]
    public async Task Authenticated_request_to_authenticated_antiforgery_endpoint_with_tokens_returns_ok()
    {
        // Arrange
        List<string> cookies = await GetAuthenticationCookies();

        AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync(isAuthenticated: true);

        CookieContainerHandler cookieHandler = new();
        cookieHandler.Container.Add(
            _factory.Server.BaseAddress,
            new Cookie(tokens.CookieName, tokens.CookieValue));

        HttpClient client = _factory.GetAuthenticatedClient(cookieHandler);
        
        client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
        client.DefaultRequestHeaders.Add("Cookie", cookies);

        HttpRequestMessage message = new()
        {
            Method = HttpMethod.Post,
            RequestUri = new Uri("/api/authenticated/antiforgery/testname", UriKind.Relative)
        };
    
        // Act
        HttpResponseMessage response = await client.SendAsync(message);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);   
    }
© www.soinside.com 2019 - 2024. All rights reserved.