fix(admin): resolve Medium code-review finding (Admin-006)

Emit <AntiforgeryToken /> in the MainLayout sign-out form and remove
.DisableAntiforgery() from the /auth/logout endpoint so UseAntiforgery()
validates the token. A tokenless POST now returns 400, preventing CSRF-logout.
Regression-guarded by AuthEndpointsTests.Logout_without_antiforgery_token_is_rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 07:26:34 -04:00
parent 55c2a5a209
commit af454c6af6
4 changed files with 21 additions and 11 deletions

View File

@@ -148,16 +148,19 @@ public sealed class AuthEndpointsTests : IClassFixture<AuthEndpointsTests.Stubbe
}
[Fact]
public async Task Logout_endpoint_clears_the_cookie_and_redirects_to_login()
public async Task Logout_without_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).
using var client = _factory.CreateNonRedirectingClient();
var response = await client.PostAsync("/auth/logout",
new FormUrlEncodedContent(Array.Empty<KeyValuePair<string, string>>()));
// No antiforgery 400 — the endpoint opts out (Admin-006 note in AuthEndpoints).
response.StatusCode.ShouldBeOneOf(HttpStatusCode.Redirect, HttpStatusCode.Found);
response.Headers.Location!.OriginalString.ShouldContain("/login");
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest,
"/auth/logout without an antiforgery token must be rejected (Admin-006)");
}
// ── Admin-003: SignalR hubs reject anonymous connections ────────────────────