75 lines
3.0 KiB
C#
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);
|
|
}
|
|
}
|