fix(dashboard)!: move login POST to /auth/login to resolve AmbiguousMatchException
The themed Blazor <LoginCard> page (Components/Pages/Login.razor, @page "/login")
registers a Razor Components endpoint that matches ALL HTTP methods. The credential
form POSTed to /login, where MapPost("/login") also matched — so every POST /login
threw Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException (HTTP 500),
breaking dashboard login for every user. It was latent because the dashboard was only
ever reached via the AllowAnonymousLocalhost bypass on the host box.
Move the credential POST to a distinct /auth/login route (mirroring ScadaBridge, which
never collided because it posts to /auth/login). GET /login stays the Blazor page; the
cookie LoginPath stays /login. Adds a registration assertion pinning DashboardLoginPost
to /auth/login as the regression guard.
Files: Login.razor (LoginCard Action), DashboardEndpointRouteBuilderExtensions (MapPost
route), GatewayApplicationTests (route assertion).
This commit is contained in:
@@ -6,13 +6,19 @@
|
||||
cookie scheme's LoginPath="/login" redirect lands here for unauthenticated users.
|
||||
|
||||
The card is the shared kit's <LoginCard>: it renders a NATIVE static
|
||||
<form method="post" action="/login"> (username/password + hidden returnUrl). A native
|
||||
form submit is not a Blazor event, so it reaches the minimal-API POST /login endpoint
|
||||
<form method="post" action="/auth/login"> (username/password + hidden returnUrl). A native
|
||||
form submit is not a Blazor event, so it reaches the minimal-API POST /auth/login endpoint
|
||||
regardless of this app's InteractiveServer render mode. <AntiforgeryToken/> supplies the
|
||||
token that PostLoginAsync's antiforgery.ValidateRequestAsync checks. *@
|
||||
token that PostLoginAsync's antiforgery.ValidateRequestAsync checks.
|
||||
|
||||
NOTE: the POST target is /auth/login, NOT /login. This @page lives at "/login" and the
|
||||
Razor Components endpoint matches ALL methods, so a POST to /login collided with the
|
||||
minimal-API MapPost("/login") and threw AmbiguousMatchException (HTTP 500). Posting to a
|
||||
distinct /auth/login path (mirroring ScadaBridge) keeps the GET page and POST handler from
|
||||
sharing a route. *@
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
<LoginCard Product="MXAccess Gateway" Action="/login" ReturnUrl="@ReturnUrl" Error="@Error">
|
||||
<LoginCard Product="MXAccess Gateway" Action="/auth/login" ReturnUrl="@ReturnUrl" Error="@Error">
|
||||
<AntiforgeryToken />
|
||||
</LoginCard>
|
||||
|
||||
|
||||
@@ -29,8 +29,14 @@ public static class DashboardEndpointRouteBuilderExtensions
|
||||
// kit's <LoginCard>. Its [AllowAnonymous] attribute overrides the
|
||||
// RequireAuthorization(ViewerPolicy) that MapRazorComponents<App>() applies,
|
||||
// so the cookie scheme's LoginPath="/login" redirect resolves for anonymous users.
|
||||
//
|
||||
// The credential POST is mapped to /auth/login, NOT /login. The @page "/login"
|
||||
// Razor Components endpoint matches ALL HTTP methods, so a MapPost("/login") shared
|
||||
// the "/login" route with it and every POST threw AmbiguousMatchException (HTTP 500).
|
||||
// A distinct /auth/login path (as ScadaBridge does) keeps the GET page and the POST
|
||||
// handler on separate routes. The <LoginCard Action="/auth/login"> form posts here.
|
||||
endpoints.MapPost(
|
||||
"/login",
|
||||
"/auth/login",
|
||||
(HttpContext httpContext, IAntiforgery antiforgery, IDashboardAuthenticator authenticator) =>
|
||||
PostLoginAsync(httpContext, antiforgery, authenticator))
|
||||
.AllowAnonymous()
|
||||
|
||||
@@ -87,13 +87,18 @@ public sealed class GatewayApplicationTests
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/events");
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/settings");
|
||||
|
||||
// GET /login is now served by the [AllowAnonymous] Blazor <Login> component
|
||||
// GET /login is served by the [AllowAnonymous] Blazor <Login> component
|
||||
// (Components/Pages/Login.razor → @page "/login"), not a named minimal-API
|
||||
// endpoint. The form still POSTs to the minimal-API DashboardLoginPost endpoint.
|
||||
// endpoint. The credential POST goes to the DashboardLoginPost endpoint at
|
||||
// /auth/login — a DISTINCT route. The Blazor component endpoint matches all HTTP
|
||||
// methods, so sharing the "/login" route with MapPost previously made POST /login
|
||||
// ambiguous (AmbiguousMatchException → HTTP 500). Pinning the POST to /auth/login
|
||||
// is the regression guard for that fix.
|
||||
Assert.Contains(endpoints, endpoint => endpoint.RoutePattern.RawText == "/login"
|
||||
&& endpoint.Metadata.GetMetadata<Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata>() is not null);
|
||||
Assert.Contains(endpoints, endpoint =>
|
||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLoginPost");
|
||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLoginPost"
|
||||
&& endpoint.RoutePattern.RawText == "/auth/login");
|
||||
Assert.Contains(endpoints, endpoint =>
|
||||
endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName == "DashboardLogout");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user