2

I have a problem to call default endpoint '/api/values' from xUnit test project. Web api is default .net core project. I always get bad request - 400 even I add header with value from AF cookie on each request.

First i setup antiforgery in Startup class.

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(
             opt =>
             {
                 opt.Filters.Add(new ValidateAntiForgeryTokenAttribute());
             }
            ).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        services.AddAntiforgery(options =>
        {
            options.HeaderName = "X-XSRF-TOKEN";
        });
    }

Then add separate controller and action to create AF cookie

    [IgnoreAntiforgeryToken]
    [AllowAnonymous]
    [HttpGet("antiforgery")]  
    public IActionResult GenerateAntiForgeryTokens()
    {
        //generate the tokens/cookie values
        //it modifies the response so that the Set-Cookie statement is added to it (that’s why it needs HttpContext as an argument).
        var tokens = _antiForgery.GetAndStoreTokens(HttpContext);
        Response.Cookies.Append("XSRF-REQUEST-TOKEN", tokens.RequestToken, new CookieOptions
        {
            HttpOnly = false,

        });

        return NoContent();
    }

Then I setup test class

 public UnitTest1()
    {
        _server = new TestServer(
          WebHost.CreateDefaultBuilder()
           .ConfigureAppConfiguration((builderContext, config) =>
           {
               config.AddJsonFile("appsettings.test.json", optional: false, reloadOnChange: true);
           })
          .UseStartup<Startup>()
          .UseEnvironment("Development")
          );
        _client = _server.CreateClient();
        _client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
    }

and add method in test class to get value from AF cookie for AF header

  protected async Task<string> EnsureAntiforgeryToken()
    {
        string _antiforgeryToken = string.Empty;

            var response = await _client.GetAsync("/api/AntiForgery/antiforgery");
            response.EnsureSuccessStatusCode();
            if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable<string> values))
            {
                var _antiforgeryCookie = Microsoft.Net.Http.Headers.SetCookieHeaderValue.ParseList(values.ToList()).SingleOrDefault(c => c.Name.StartsWith(XSRF_TOKEN, StringComparison.InvariantCultureIgnoreCase));
                _antiforgeryToken = _antiforgeryCookie.Value.ToString();
            }

        return await Task.FromResult<string>(_antiforgeryToken);
    }

and in my test method I try to call endpoint

[Fact]
    public async Task Test1Async()
    {
        _antiforgeryCookie = await EnsureAntiforgeryToken();
        _client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", _antiforgeryCookie);
        var result = await _client.GetAsync("/api/values"); //always get error 400
        Assert.True(true, "");
    }
plamtod
  • 79
  • 3
  • 9

1 Answers1

1

What's happening is that cookies in browsers or debugging tools like postman are automatically stored when received, then sent with every subsequent request to URLs with the same domain they were received from. This is not the case when you try to write and make requests in code.

So, you need to add the cookies, as cookies, to requests which hit an endpoint with anti-forgery validation.

When you get the response with the XSRF token, you have the cookies array which you are retrieving from a token generation response as such:

response.Headers.TryGetValues("Set-Cookie", out IEnumerable<string> values)

And you're also parsing the XSRF-TOKEN cookie to get its value, which is great. But you also need both cookies unparsed as well.

So, you could introduce:

public class AntiForgeryToken
{
    public string XsrfToken { get; set; }
    public string[] Cookies { get; set; }
}

And modify EnsureAntiforgeryToken to populate it as such:

protected async Task<AntiForgeryToken> EnsureAntiforgeryToken()
{
    var antiForgerytoken = new AntiForgeryToken();

    var response = await _client.GetAsync("/api/AntiForgery/antiforgery");
    response.EnsureSuccessStatusCode();

    if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable<string> values))
    {
        var cookies = SetCookieHeaderValue.ParseList(values.ToList());

        var _antiforgeryCookie = cookies.SingleOrDefault(c =>
            c.Name.StartsWith(XSRF_TOKEN, StringComparison.OrdinalIgnoreCase));

        // Value of XSRF token cookie
        antiForgerytoken.XsrfToken = _antiforgeryCookie.Value.ToString();

        // and the cookies unparsed (both XSRF-TOKEN and .AspNetCore.Antiforgery.{someId})
        antiForgerytoken.Cookies = values.ToArray();
    }

    return antiForgerytoken;
}

We return both the XSRF Token that you are already parsing, in addition to cookies array which was returned with the response. The cookies are strings with their values and all the metadata that makes them cookies.

You then add the X-XSRF-TOKEN header and both cookies to your HttpClient, as such:

public async Task Test1Async()
{
    _antiForgeryToken = await EnsureAntiforgeryToken();

    // the bit you have and will still need
    _client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", _antiForgeryToken.XsrfToken);

    // the bit you're missing
    _client.DefaultRequestHeaders.Add("Cookie", _antiForgeryToken.Cookies[0]);
    _client.DefaultRequestHeaders.Add("Cookie", _antiForgeryToken.Cookies[1]);

    var result = await _client.GetAsync("/api/values"); // no more 400

    Assert.True(result.IsSuccessStatusCode);
}

Which ends up mimicking the behaviour of a browser or debugging tool, where cookies received are stored and automatically sent back with every request to URLs with the same domain it was received from.

KiwiProg
  • 11
  • 1
  • 3