10

How can I apply a middleware to the OnRedirectToLogin event in Cookie options (so i can use dependency injection), OR how can I retrieve actionContext from RedirectContext? I have tried searching for documentation or examples, but it is hard to find, and I have not seen any examples explaining or defining how. Is this even possible?

my Startup.cs

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(o =>
                {
                    o.AccessDeniedPath = new PathString("/Error/AccessDenied");
                    o.LoginPath = new PathString("/Account/Login/");
                    o.Cookie.Path = "/";
                    o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    o.Cookie.HttpOnly = true;
                    o.LogoutPath = new PathString("/Account/Logout/");
                    o.Events.OnRedirectToLogin = (context) =>
                    {
                        var routeData = context.HttpContext.GetRouteData();
                        RouteValueDictionary routeValues = new RouteValueDictionary();
                        if (routeData != null) routeValues.Add("lang", routeData.Values["lang"]);
                        Uri uri = new Uri(context.RedirectUri);
                        string returnUrl = HttpUtility.ParseQueryString(uri.Query)[context.Options.ReturnUrlParameter];
                        string focustab = "";
                        context.Request.Query.ToList().ForEach(x =>
                        {
                            if (x.Key == "id") routeValues.Add("id", x.Value.FirstOrDefault());
                            if (x.Key == "values") routeValues.Add("values", x.Value.FirstOrDefault());

                        });

                        routeValues.Add(context.Options.ReturnUrlParameter, returnUrl + focustab);

                        //context here is a redirect context, how can i get the action context to create a new Action as what UrlHelper is expecting
                        context.RedirectUri = new UrlHelper(context).Action("login", "account", routeValues);
                        return Task.CompletedTask;
                    };
                });

Thanks in advance.

John Dover
  • 739
  • 1
  • 8
  • 19

1 Answers1

25

I was able to get it working as I wanted, so I am posting my answer in case anyone else comes across this question. With the help of this documentation: Use cookie authentication without ASP.NET Core Identity. I implemented the CustomCookieAuthenticationEvents and casted that as the type for the Cookie events.

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o =>
    {
        o.AccessDeniedPath = new PathString("/Error/AccessDenied");
        o.LoginPath = new PathString("/Account/Login/");
        o.Cookie.Path = "/";
        o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
        o.Cookie.HttpOnly = true;
        o.LogoutPath = new PathString("/Account/Logout/");
        //This line here
        o.EventsType = typeof(CustomCookieAuthenticationEvents);
    });

//register the IActionContextAccessor so that I can inject into my CustomCookieAuthenticationEvents constructor
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

Then inside that Custom class, I was able to inject IUrlHelperFactory and IActionContextAccessor which helps me create new UrlHelper for the current action.

public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
    private IUrlHelperFactory _helper;
    private IActionContextAccessor _accessor;
    public CustomCookieAuthenticationEvents(IUrlHelperFactory helper, IActionContextAccessor accessor)
    {
        _helper = helper;
        _accessor = accessor;
    }
    public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
    {
        var routeData = context.Request.HttpContext.GetRouteData();
        RouteValueDictionary routeValues = new RouteValueDictionary();
        //Create new routeValues based on routeData above
        ... code removed for brevity
        Uri uri = new Uri(context.RedirectUri);
        string returnUrl = HttpUtility.ParseQueryString(uri.Query)[context.Options.ReturnUrlParameter];

        routeValues.Add(context.Options.ReturnUrlParameter, returnUrl + focustab);
        var urlHelper = _helper.GetUrlHelper(_accessor.ActionContext);
        context.RedirectUri = UrlHelperExtensions.Action(urlHelper, "login", "account", routeValues);
        return base.RedirectToLogin(context);
    }
}

This allows me to modify the query string and returnUrl parameter on Redirect to Login requests. My requirement was to remove a specific query string value on first request to Login, and this allows me to meet that need.

It was pointed out in the comments that ActionContext is null in .net Core 3.1. I created a new .net core 3.1 project and this is the new code to make sure ActionContext is NOT null. Have not fully tested, just made sure the ActionContext was not null for inject IActionContextAccessor.

The main thing to remember here is that IActionContextAccessor works with the MVC pipeline. This means we would have to change the .net core 3.1 template to use MVC. CustomCookieAuthenticationEvents does not change.

New Startup.cs

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(m =>
        {
            m.EnableEndpointRouting = false;
        });
        //register the IActionContextAccessor so that I can inject into my CustomCookieAuthenticationEvents constructor
        services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
        services.AddScoped<CustomCookieAuthenticationEvents>();
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(o =>
        {
            o.AccessDeniedPath = new PathString("/Error/AccessDenied");
            o.LoginPath = new PathString("/Account/Login/");
            o.Cookie.Path = "/";
            o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            o.Cookie.HttpOnly = true;
            o.LogoutPath = new PathString("/Account/Logout/");
            //This line here
            o.EventsType = typeof(CustomCookieAuthenticationEvents);
        });

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        //app.UseRouting();

        app.UseAuthorization();

        app.UseMvcWithDefaultRoute();

        //app.UseEndpoints(endpoints =>
        //{
        //    endpoints.MapControllerRoute(
        //        name: "default",
        //        pattern: "{controller=Home}/{action=Index}/{id?}");
        //});
    }
John Dover
  • 739
  • 1
  • 8
  • 19
  • 6
    Thanks for the solution. In my case, I have to register `CustomCookieAuthenticationEvents` class in DI to make it work. – Janak Jan 01 '19 at 20:08
  • Thanks! Worked for me. I could not find an other way to just return a 401 status code and not server-redirecting to /Account/Login. My implementation of the CustomCookieAuthenticationEvents class is this: context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return Task.CompletedTask; My angular front-end catches the 401 and redirects to the login page. – Mcanic Aug 13 '19 at 12:11
  • Does this still work in ASP.NET Core 3.1? _accessor.ActionContext is always null for me. – Mike Brunner May 06 '20 at 16:31
  • Updated answer to reflect ASP.NET Core 3.1 – John Dover Jun 05 '20 at 15:14