Initial commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user