У меня есть конечная точка контроллера API ASP.NET Core, для которой требуется:
- аутентифицированный пользователь, и
- проверка токенов защиты от подделки.
Проблема
Я не могу отправить запрос, в котором есть как аутентифицированный пользователь, так и необходимые токены защиты от подделки/файлы cookie и файлы cookie аутентификации. Таким образом, конечная точка продолжает возвращать неверный запрос, прежде чем достигнет обработчика.
Вопрос
Как вы выполняете интеграционный тест на конечная точка, которая требует как аутентификации, так и проверки токенов защиты от подделки?
Код
Чтобы помочь разобраться в этом вопросе, я создал пример приложения, демонстрирующий проблема, с которой я столкнулся.
Конечные точки контроллера API
В примере приложения есть контроллер API с четырьмя конечными точками POST:
1. Неаутентифицированная (анонимная) конечная точка – проверка на подделку НЕ требуется
Код: Выделить всё
[AllowAnonymous]
[HttpPost("Anonymous/{name}")]
public IActionResult AnonymousPost(string name)
{
return Ok(name);
}
Код: Выделить всё
[HttpPost("Authenticated/{name}")]
public IActionResult AuthenticatedPost(string name)
{
return Ok(name);
}
Код: Выделить всё
[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost("Anonymous/Antiforgery/{name}")]
public IActionResult AnonymousAntiforgeryPost(string name)
{
return Ok(name);
}
Код: Выделить всё
[ValidateAntiForgeryToken]
[HttpPost("Authenticated/Antiforgery/{name}")]
public IActionResult AuthenticatedAntiforgeryPost(string name)
{
return Ok(name);
}
Аутентификация
Приложение использует аутентификацию с использованием файлов 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("TestScheme", options => { });
});
}
Код: Выделить всё
protected override Task 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), которые требуют проверки токена защиты от подделки.< /p>
Тестовый проект добавляет 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 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);
}
Код: Выделить всё
public async Task GetAntiforgeryTokensAsync(
Func? httpClientFactory = null,
CancellationToken cancellationToken = default)
{
using HttpClient httpClient = httpClientFactory?.Invoke() ?? CreateDefaultClient();
AntiforgeryTokens? tokens = await httpClient.GetFromJsonAsync(
AntiforgeryTokenController.GetTokensUri,
cancellationToken);
return tokens!;
}
Я могу успешно протестировать первые три конечные точки, однако, когда мне нужно протестировать четвертую конечную точку, требуется как аутентифицированный при проверке пользователя и токена защиты от подделки возвращается неверный запрос.
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);
}
Код: Выделить всё
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);
}
Код: Выделить всё
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);
}
Я читал, что браузер автоматически извлекает все файлы cookie из заголовков ответов сервера и присоединяет их к следующему запросу. Чтобы проверка с помощью CSRF прошла успешно, ее необходимо смоделировать. Поэтому этот тест вызывает метод GetAuthenticationCookies, который выполняет вход в приложение и извлекает файлы cookie аутентификации из ответа.
Код: Выделить всё
public async Task GetAuthenticationCookies(CookieContainerHandler cookieHandler, AntiforgeryTokens tokens)
{
CancellationToken cancellationToken = new CancellationTokenSource().Token;
HttpClient client = _factory.CreateDefaultClient(cookieHandler);
Uri uri = new($"{client.BaseAddress!.AbsoluteUri}login");
Dictionary 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();
}
Код: Выделить всё
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 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);
}
Подробнее здесь: https://stackoverflow.com/questions/787 ... t-that-req