Files
PowderCoatingLogix/src/PowderCoating.Web/Middleware/MustChangePasswordMiddleware.cs
T
2026-04-23 21:38:24 -04:00

75 lines
3.0 KiB
C#

namespace PowderCoating.Web.Middleware;
/// <summary>
/// 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.
/// </summary>
public class MustChangePasswordMiddleware
{
private readonly RequestDelegate _next;
/// <summary>
/// URL path prefixes that are reachable even before the mandatory password
/// change is completed. These are the minimum set of routes required to:
/// <list type="bullet">
/// <item>Display and submit the change-password form itself.</item>
/// <item>Allow the user to log out if they do not want to proceed.</item>
/// <item>Load static assets served by the Identity UI (CSS, JS, etc.).</item>
/// <item>Serve logo and profile images referenced by the layout.</item>
/// <item>Allow SignalR hub negotiation and API calls that the page may
/// initiate before the redirect is completed client-side.</item>
/// </list>
/// All other paths redirect to <c>/Account/ChangeInitialPassword</c>.
/// </summary>
private static readonly string[] AllowedPrefixes =
[
"/Account/ChangeInitialPassword",
"/Identity/Account/Logout",
"/Identity/",
"/CompanyLogo",
"/Profile/Photo",
"/api/",
"/stripe/",
"/hubs/",
];
/// <summary>
/// Initialises the middleware with the next request delegate in the pipeline.
/// </summary>
/// <param name="next">The next middleware component.</param>
public MustChangePasswordMiddleware(RequestDelegate next) => _next = next;
/// <summary>
/// Intercepts every request. If the authenticated user carries the
/// <c>MustChangePassword=true</c> claim and is requesting a path not in
/// <see cref="AllowedPrefixes"/>, issues a redirect to the change-password
/// page and short-circuits the rest of the pipeline.
/// <para>
/// 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.
/// </para>
/// </summary>
/// <param name="context">The current HTTP context.</param>
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);
}
}