Enforce dashboard authorization on all component routes

Fixes code-review findings Server-001 (Critical) and Server-003 (High).

Server-001: the dashboard Razor components were mapped with no
authorization policy, so every dashboard page — including the API Keys
page — was reachable unauthenticated. MapRazorComponents<App>() now
requires DashboardAuthenticationDefaults.AuthorizationPolicy;
unauthenticated requests are challenged by the cookie scheme and
redirected to the login page.

Server-003: DashboardAuthenticator.CreatePrincipal never issued the
'scope' claim that DashboardAuthorizationHandler checks when
Dashboard:RequireAdminScope is enabled, so enforcing the policy would
have denied every LDAP login. CreatePrincipal (reached only after the
required-group check passes) now emits the admin scope claim.

Replaces the GatewayApplicationTests case that asserted dashboard
routes allow anonymous access — it encoded the bug as expected
behavior — with tests that verify component routes require the policy
and the login/logout/denied endpoints allow anonymous.

All 309 MxGateway.Tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 16:45:29 -04:00
parent 3cc53a8c69
commit a8aafdf974
3 changed files with 53 additions and 11 deletions
@@ -2,6 +2,7 @@ using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Options;
using MxGateway.Server.Configuration;
using MxGateway.Server.Security.Authorization;
using Novell.Directory.Ldap;
namespace MxGateway.Server.Dashboard;
@@ -238,10 +239,16 @@ public sealed class DashboardAuthenticator(
string displayName,
IEnumerable<string> groups)
{
// CreatePrincipal is reached only after IsMemberOfRequiredGroup passed,
// so the authenticated user is authorized for the dashboard. Emit the
// admin scope claim that DashboardAuthorizationHandler checks when
// Dashboard:RequireAdminScope is enabled — without it, every LDAP login
// would be denied once route-level authorization is enforced.
List<Claim> claims =
[
new Claim(ClaimTypes.NameIdentifier, username),
new Claim(ClaimTypes.Name, displayName)
new Claim(ClaimTypes.Name, displayName),
new Claim(DashboardAuthenticationDefaults.ScopeClaimType, GatewayScopes.Admin)
];
claims.AddRange(groups.Select(group => new Claim(
@@ -52,8 +52,13 @@ public static class DashboardEndpointRouteBuilderExtensions
.AllowAnonymous()
.WithName("DashboardAccessDenied");
// Every dashboard Razor component requires an authorized session. The
// login/logout/denied endpoints above opt out via AllowAnonymous(); an
// unauthenticated request to a component route is challenged by the
// cookie scheme and redirected to the login page.
dashboard.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
.AddInteractiveServerRenderMode()
.RequireAuthorization(DashboardAuthenticationDefaults.AuthorizationPolicy);
return endpoints;
}