using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using ScadaLink.CentralUI.Auth; namespace ScadaLink.CentralUI.Tests.Auth; /// /// Regression tests for CentralUI-017. POST /auth/logout called /// .DisableAntiforgery() and a plain GET /logout route also /// signed the user out — either could be triggered cross-site to forcibly log /// a user out. Logout is a state-changing authenticated action and must be /// CSRF-protected: the POST keeps antiforgery enabled and the state-changing /// GET route is removed. /// public class AuthEndpointsCsrfTests { private static IReadOnlyList BuildEndpoints() { var builder = WebApplication.CreateBuilder(); builder.Services.AddRouting(); builder.Services.AddAntiforgery(); // Dispose the host: an undisposed WebApplication leaks its config // PhysicalFileProvider (appsettings reload-watch FileSystemWatcher — a // process-wide macOS run-loop thread) and a ConsoleLoggerProcessor // thread, which keep the test host process alive after the run. using var app = builder.Build(); app.MapAuthEndpoints(); return ((IEndpointRouteBuilder)app).DataSources .SelectMany(ds => ds.Endpoints) .OfType() .ToList(); } private static RouteEndpoint? Find(IReadOnlyList endpoints, string pattern, string method) => endpoints.FirstOrDefault(e => e.RoutePattern.RawText == pattern && (e.Metadata.GetMetadata()?.HttpMethods.Contains(method) ?? false)); [Fact] public void PostAuthLogout_DoesNotDisableAntiforgery() { var endpoints = BuildEndpoints(); var logout = Find(endpoints, "/auth/logout", "POST"); Assert.NotNull(logout); // DisableAntiforgery() leaves an IAntiforgeryMetadata with // RequiresValidation == false. A CSRF-protected POST has either no such // metadata, or metadata that still requires validation. var antiforgery = logout!.Metadata.GetMetadata(); Assert.True(antiforgery is null || antiforgery.RequiresValidation, "POST /auth/logout must keep antiforgery validation enabled."); } [Fact] public void GetLogout_StateChangingRoute_IsRemoved() { var endpoints = BuildEndpoints(); var getLogout = Find(endpoints, "/logout", "GET"); Assert.Null(getLogout); } [Fact] public void PostAuthLogin_StillDisablesAntiforgery_PreAuthIsAcceptable() { // Login is a pre-auth endpoint; disabling antiforgery there is acceptable // and intentional. This pins that the fix did not over-correct. var endpoints = BuildEndpoints(); var login = Find(endpoints, "/auth/login", "POST"); Assert.NotNull(login); var antiforgery = login!.Metadata.GetMetadata(); Assert.NotNull(antiforgery); Assert.False(antiforgery!.RequiresValidation); } }