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:
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Antiforgery;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -30,12 +31,14 @@ public static class AuthEndpoints
|
||||
.AllowAnonymous()
|
||||
.DisableAntiforgery();
|
||||
|
||||
// Admin-006: the logout form in MainLayout.razor emits <AntiforgeryToken /> so the
|
||||
// middleware validates the token. This prevents a cross-site logout (CSRF-logout) where
|
||||
// an attacker tricks the operator's browser into posting to /auth/logout. The endpoint
|
||||
// intentionally does NOT call .DisableAntiforgery() — the token must be present and
|
||||
// valid; the middleware rejects forged or missing tokens with 400.
|
||||
endpoints.MapPost("/auth/logout", (Delegate)LogoutAsync);
|
||||
// Admin-006: the logout form in MainLayout.razor emits <AntiforgeryToken />.
|
||||
// The endpoint validates the token explicitly via IAntiforgery.ValidateRequestAsync
|
||||
// (minimal API endpoints do not participate in the UseAntiforgery() pipeline by default).
|
||||
// Calling .DisableAntiforgery() suppresses the middleware pass so the manual check in
|
||||
// LogoutAsync is the single validation point — duplicating both would cause double-reads
|
||||
// of the request body.
|
||||
endpoints.MapPost("/auth/logout", (Delegate)LogoutAsync)
|
||||
.DisableAntiforgery();
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
@@ -84,8 +87,17 @@ public static class AuthEndpoints
|
||||
return Results.Redirect(SafeReturnUrl(returnUrl));
|
||||
}
|
||||
|
||||
private static async Task<IResult> LogoutAsync(HttpContext ctx)
|
||||
private static async Task<IResult> LogoutAsync(HttpContext ctx, IAntiforgery antiforgery)
|
||||
{
|
||||
try
|
||||
{
|
||||
await antiforgery.ValidateRequestAsync(ctx);
|
||||
}
|
||||
catch (AntiforgeryValidationException)
|
||||
{
|
||||
return Results.BadRequest("Invalid or missing antiforgery token.");
|
||||
}
|
||||
|
||||
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return Results.Redirect("/login");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user