Dashboard: handle GET /logout (was 405) by signing out + redirecting to /login

Browsers that navigate directly to /logout via the address bar issued a GET
against a POST-only route and got 405 Method Not Allowed. Logout is
self-destructive, so the GET path can skip antiforgery; the existing POST
form (used by the layout's Sign out button) is unchanged and still
antiforgery-protected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-25 23:40:39 -04:00
parent 54480dde61
commit 7fc1955287
2 changed files with 19 additions and 1 deletions
@@ -44,6 +44,15 @@ public static class DashboardEndpointRouteBuilderExtensions
.AllowAnonymous()
.WithName("DashboardLogout");
// Browsers that navigate directly to /logout issue a GET. Sign out and
// redirect to /login so the URL works as users expect. Logout is
// self-destructive, so the GET path intentionally skips antiforgery.
endpoints.MapGet(
"/logout",
(Delegate)(static (HttpContext httpContext) => GetLogoutAsync(httpContext)))
.AllowAnonymous()
.WithName("DashboardLogoutGet");
endpoints.MapGet("/denied", () => Results.Content(
RenderPage("Access denied", "<p>The signed-in user is not authorized for dashboard access.</p>"),
"text/html"))
@@ -140,6 +149,15 @@ public static class DashboardEndpointRouteBuilderExtensions
return Results.LocalRedirect("/login");
}
private static async Task<IResult> GetLogoutAsync(HttpContext httpContext)
{
await httpContext
.SignOutAsync(DashboardAuthenticationDefaults.AuthenticationScheme)
.ConfigureAwait(false);
return Results.LocalRedirect("/login");
}
private static string RenderLoginPage(
HttpContext httpContext,
IAntiforgery antiforgery,
@@ -63,7 +63,7 @@ public sealed class GatewayApplicationTests
IReadOnlyList<RouteEndpoint> endpoints = GetRouteEndpoints(app);
string[] anonymousEndpointNames =
["DashboardLogin", "DashboardLoginPost", "DashboardLogout", "DashboardAccessDenied"];
["DashboardLogin", "DashboardLoginPost", "DashboardLogout", "DashboardLogoutGet", "DashboardAccessDenied"];
foreach (string endpointName in anonymousEndpointNames)
{
RouteEndpoint endpoint = Assert.Single(