namespace PowderCoating.Web.Middleware; /// /// Intercepts authenticated requests and redirects any user that has the /// "MustChangePassword" claim to the change-password page before they can /// access any other part of the application. /// public class MustChangePasswordMiddleware { private readonly RequestDelegate _next; /// /// URL path prefixes that are reachable even before the mandatory password /// change is completed. These are the minimum set of routes required to: /// /// Display and submit the change-password form itself. /// Allow the user to log out if they do not want to proceed. /// Load static assets served by the Identity UI (CSS, JS, etc.). /// Serve logo and profile images referenced by the layout. /// Allow SignalR hub negotiation and API calls that the page may /// initiate before the redirect is completed client-side. /// /// All other paths redirect to /Account/ChangeInitialPassword. /// private static readonly string[] AllowedPrefixes = [ "/Account/ChangeInitialPassword", "/Identity/Account/Logout", "/Identity/", "/CompanyLogo", "/Profile/Photo", "/api/", "/stripe/", "/hubs/", ]; /// /// Initialises the middleware with the next request delegate in the pipeline. /// /// The next middleware component. public MustChangePasswordMiddleware(RequestDelegate next) => _next = next; /// /// Intercepts every request. If the authenticated user carries the /// MustChangePassword=true claim and is requesting a path not in /// , issues a redirect to the change-password /// page and short-circuits the rest of the pipeline. /// /// The claim is set by the admin when creating or resetting a user account. /// Using a claim (rather than a DB flag) means the check is evaluated on /// every request without an additional database query, at the cost of a /// sign-out being required to clear it — which is acceptable since the /// password change form re-signs the user in automatically. /// /// /// The current HTTP context. public async Task InvokeAsync(HttpContext context) { if (context.User.Identity?.IsAuthenticated == true && context.User.FindFirst("MustChangePassword")?.Value == "true") { var path = context.Request.Path.Value ?? string.Empty; var allowed = AllowedPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase)); if (!allowed) { context.Response.Redirect("/Account/ChangeInitialPassword"); return; } } await _next(context); } }