We're currently migrating a .NET Framework 4.8 MVC app to .NET (Core) 5 and I'm encountering a challenge.
In the old Framework app, we implement AuthorizeAttribute
where an override on HandleUnauthorizedRequest
will clear the content of the response and 're'-execute another controller (with the help of IControllerFactory
). This essentially creates a login landing page where the URL stays the same (same request). For each secured page with this attribute, we have a different page that we render (a 'sales pitch' as you will).
Now in .NET Core, authentication works differently than in .NET Framework. So I believe the approach described earlier will not work here. I think the correct approach is to configure cookie authentication to handle this. But we have two business requirements that make this a little challenging:
- No redirections; instead, show the login page on the first request, so basically, URL rewrite/re-execute instead of redirect
- Each page requiring authentication should render a different login page
My challenge lies in the first requirement. I need to find a way to render a different controller action on authentication. A URL re-execute will be fine, too, as we already do this in a custom UseStatusCodePages handler. The AddAuthentication().AddCookie(o => o.Events.OnRedirectToLogin = context => { ... }
looks like the place where I could handle my logic, but nothing I tried works. A reason might be that this is too far into the middleware?
So the question is: how to do a rewrite or re-execute instead of a redirect on the authentication challenge?
CodePudding user response:
With the help of a colleague and looking at the source code of the UseStatusCodePages
and UseRewriter
features we have come up with a solution to the problem.
We ended up with implementing the OnRedirectToLogin
event on the AddCookie
. When this event gets triggered we find the URI of the current login page that we need to show. But instead of setting the ``context.RedirectUriwe instead add it to the
context.HttpContext.Itemsdictionary. We then use custom middleware to look for that item and do the rewrite/re-execute there. The
return Task.CompletedTask;` makes sure nothing happens, no redirect. Without the middleware, we get a blank page.
services.AddAuthentication().AddCookie(o =>
{
o.Events.OnRedirectToLogin = context =>
{
var loginPageUri = GetLoginLandingPageUri(context);
context.HttpContext.Items[ReExecuteUriMiddleware.ReExecuteUriItemKey] = loginPageUri;
return Task.CompletedTask;
};
});
When the middleware sees that item in the Items dictionary we reset the endpoint information, so when we call the _next
for the second time, the middleware pipeline will nicely re-execute the request, but with a different path.
public class ReExecuteUriMiddleware
{
public const string ReExecuteUriItemKey = "ReExecuteUri";
private readonly RequestDelegate _next;
public ReExecuteUriMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
await _next.Invoke(httpContext);
if (httpContext.Items[ReExecuteUriItemKey] is string reExecuteUri)
{
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
// the endpoint and route values to ensure things are re-calculated.
httpContext.SetEndpoint(null);
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
httpContext.Request.Path = reExecuteUri;
await _next.Invoke(httpContext);
}
}
}
And ofcourse we added it in the Startup
class right after the app.UseStatusCodePages()
.
app.UseMiddleware<ReExecuteUriMiddleware>();