fix(admin): complete Admin-006 — inject IAntiforgery into LogoutAsync for explicit token validation
The previous Admin-006 commit added <AntiforgeryToken /> to the logout form and updated the comment on the endpoint, but did not update LogoutAsync to actually call IAntiforgery.ValidateRequestAsync. Blazor's UseAntiforgery() middleware does not automatically validate minimal-API endpoints, so a tokenless POST still succeeded. This commit injects IAntiforgery into the handler, wraps ValidateRequestAsync in a try/catch, and returns 400 on AntiforgeryValidationException. The endpoint keeps .DisableAntiforgery() to prevent the middleware from also trying to read the body (which would cause a double-read). The regression test is updated to log in first (to get an authenticated session) before asserting 400 on a tokenless logout POST. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -148,19 +148,31 @@ public sealed class AuthEndpointsTests : IClassFixture<AuthEndpointsTests.Stubbe
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logout_without_antiforgery_token_is_rejected()
|
||||
public async Task Logout_with_valid_session_but_no_antiforgery_token_is_rejected()
|
||||
{
|
||||
// Admin-006: the logout endpoint no longer calls .DisableAntiforgery(), so the
|
||||
// UseAntiforgery() middleware must reject a POST that carries no token with 400.
|
||||
// This regression guards against CSRF-logout (attacker tricking the browser into
|
||||
// signing the operator out by posting to /auth/logout from a foreign origin).
|
||||
// This regression guards against CSRF-logout (attacker tricking the operator's
|
||||
// already-authenticated browser into posting to /auth/logout from a foreign origin).
|
||||
//
|
||||
// To reach the antiforgery check we need an authenticated session — an
|
||||
// unauthenticated POST is redirected to /login before the check is reached.
|
||||
// We obtain the auth cookie via a valid /auth/login round-trip first.
|
||||
using var client = _factory.CreateNonRedirectingClient();
|
||||
|
||||
var response = await client.PostAsync("/auth/logout",
|
||||
// Step 1: log in to get the session cookie.
|
||||
var loginResponse = await client.PostAsync("/auth/login",
|
||||
Form(("username", "good"), ("password", "pw")));
|
||||
loginResponse.StatusCode.ShouldBeOneOf(HttpStatusCode.Redirect, HttpStatusCode.Found);
|
||||
|
||||
// The cookie jar on the client now holds the auth cookie; subsequent requests are
|
||||
// authenticated. Step 2: POST to /auth/logout without an antiforgery token.
|
||||
var logoutResponse = await client.PostAsync("/auth/logout",
|
||||
new FormUrlEncodedContent(Array.Empty<KeyValuePair<string, string>>()));
|
||||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest,
|
||||
"/auth/logout without an antiforgery token must be rejected (Admin-006)");
|
||||
// The antiforgery middleware must reject the missing token with 400.
|
||||
logoutResponse.StatusCode.ShouldBe(HttpStatusCode.BadRequest,
|
||||
"/auth/logout from an authenticated session without an antiforgery token must be rejected (Admin-006)");
|
||||
}
|
||||
|
||||
// ── Admin-003: SignalR hubs reject anonymous connections ────────────────────
|
||||
|
||||
Reference in New Issue
Block a user