33

I'm trying to get ASP.NET Core Identity to return 401 when a user isn't logged in. I've added an [Authorize] attribute to my method and instead of returning 401 it returns 302. I've tried a ton of suggestions but nothing seems to work, including services.Configure and app.UseCookieAuthentication setting LoginPath to null or PathString.Empty.

Vadim Ovchinnikov
  • 10,848
  • 4
  • 43
  • 73
Eric B
  • 4,077
  • 5
  • 29
  • 39
  • For anyone else following this same trail, I got here from https://devblog.dymel.pl/2016/07/07/return-401-unauthorized-from-asp-net-core-api/ – Brett Rossier Jan 21 '19 at 17:58

8 Answers8

58

As of ASP.NET Core 2.x:

services.ConfigureApplicationCookie(options =>
{
    options.Events.OnRedirectToLogin = context =>
    {
        context.Response.StatusCode = 401;    
        return Task.CompletedTask;
    };
});
Matthew Steven Monkan
  • 5,780
  • 3
  • 46
  • 66
  • 2
    also, your call to services.AddIdentity must come before this one – Eric B Aug 24 '17 at 04:25
  • 4
    I believe kroatti's answer should be the accepted one. The above approach will cause issues if you want ajax requests to get 401 but non-ajax requests to get redirected. You can add logic to handle that, per Mark Perry's answer, but this logic is already built in to the framework for requests with X-Requested-With: XMLHttpRequest header. – Daniel May 01 '18 at 15:27
  • I wrote three different custom policies for my project. So, will this return 401 instead of 302 for all of my policies? – KaraKaplanKhan Jul 30 '18 at 08:00
  • 3
    This worked. What an annoying feature in the framework. If we wanted a redirect, we would specify it... – kovac Feb 09 '19 at 16:41
  • For me it was the OnRedirectToAccessDenied that needed to be set. – Steven Pena Apr 25 '19 at 21:31
  • Steven Pena is actually correct. You're not to 401 the login redirect, else your login page will not work. OnRedirectToAccessDenied is the correct answer in my opinion – Jeff Aug 18 '20 at 11:24
20

If the request header contains X-Requested-With: XMLHttpRequest the status code will be 401 instead of 302

private static bool IsAjaxRequest(HttpRequest request)
    {
        return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
            string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
    }

See on gitHub: https://github.com/aspnet/Security/blob/5de25bb11cfb2bf60d05ea2be36e80d86b38d18b/src/Microsoft.AspNetCore.Authentication.Cookies/Events/CookieAuthenticationEvents.cs#L40-L52

kroatti
  • 199
  • 1
  • 4
  • 2
    this should be the most pretty straightforward answer without hurting your code base. seesh, why i havent found you earlier. – Jeff Jun 28 '18 at 10:02
  • 5
    This is not a very helpful answer, since it requires the caller to inject a proprietary header value. How about fixing the handler code to actually honor the Accept: header instead? My client code (angular) is sending "Accept: application/json" but getting back crap HTML as a response which it can't parse. The server side code IS the problem. – theta-fish Jan 22 '19 at 01:09
15

For asp.net mvc core USE THIS INSTEAD

 services.ConfigureApplicationCookie(options =>
        {
            options.LoginPath = new PathString("/Account/Login");
            options.LogoutPath = new PathString("/Account/Logout");

            options.Events.OnRedirectToLogin = context =>
            {
                if (context.Request.Path.StartsWithSegments("/api")
                    && context.Response.StatusCode == StatusCodes.Status200OK)
                {
                    context.Response.Clear();
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return Task.CompletedTask;
                }
                context.Response.Redirect(context.RedirectUri);
                return Task.CompletedTask;
            };
        });
Francis Ofosu
  • 151
  • 1
  • 3
12
services.Configure<IdentityOptions>(options =>
{
   options.Cookies.ApplicationCookie.LoginPath = new PathString("/");
   options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents()
   {
      OnRedirectToLogin = context =>
      {
         if (context.Request.Path.Value.StartsWith("/api"))
         {
            context.Response.Clear();
            context.Response.StatusCode = 401;
            return Task.FromResult(0);
         }
         context.Response.Redirect(context.RedirectUri);
         return Task.FromResult(0);
      }
   };
});

Source:

https://www.illucit.com/blog/2016/04/asp-net-5-identity-302-redirect-vs-401-unauthorized-for-api-ajax-requests/

Mark Perry
  • 1,645
  • 10
  • 12
  • This is exactly what I needed. Thanks! – Jeff Yates Jun 19 '17 at 07:35
  • Works great on .net core 2.1 - though I did have to change StartsWith("/api") to contains("api") but worked great after that. Guess I need to do something within my app now to detect 401's and redirect accordingly after any http request. Cheers! – DanAbdn Nov 04 '18 at 09:46
  • For me it was the OnRedirectToAccessDenied that needed to be set. – Steven Pena Apr 25 '19 at 21:31
5

Okay after digging around in the asp.net core unit tests I finally found a working solution. You have to add the following to your call to services.AddIdentity

services.AddIdentity<ApplicationUser, IdentityRole>(o => {
    o.Cookies.ApplicationCookie.AutomaticChallenge = false;
});
Eric B
  • 4,077
  • 5
  • 29
  • 39
  • Does this still re-direct the user to the login page? I'm getting ALOT of traffic to pages that, when clicked, will re-direct to the login page because of the 302. I still want that to work, but I want all of the bots to fail that call so they never call it again (a 401/403). A 302 will still tell them to keep calling it. (This is for the ones that don't honor the robots.txt file where I'm specifaclly "disallow"ing that url pattern) – ganders Dec 27 '16 at 15:40
  • 1
    @ganders No. If you want your app to behave a certain way on 401, you'll have to handle it yourself. Typically a 401 should include a WWW-Authenticate header that describes how to authenticate. – Eric B Dec 27 '16 at 15:52
  • `WWW-Authenticate` header MUST be provided on 401. `If the protected resource request does not include authentication credentials or does not contain an access token that enables access to the protected resource, the resource server MUST include the HTTP "WWW-Authenticate" response header field` – urbanhusky May 17 '17 at 09:01
  • This doesn't seem to be present (anymore) in .NET Core 3.x (preview) :/ – Bernoulli IT Aug 01 '19 at 10:48
3

For ASP.NET Core 3.x (preview) using Identity with Cookie authentication this is what did the trick:

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<IdentityContext>()
    .AddDefaultTokenProviders()
    .AddRoles<IdentityRole>();

services.ConfigureApplicationCookie(options =>
{
    options.Events.OnRedirectToLogin = context =>
    {
        context.Response.Headers["Location"] = context.RedirectUri;
        context.Response.StatusCode = 401;
        return Task.CompletedTask;
    };
});

This is what we see around everywhere in different variations. BUT, the essential point here is that ConfigureApplicationCookie must be specified AFTER AddIdentity. It's "sad" but true. This SO answer finally brought light in the darkness.

I have been scratching my head for over a day and tried many different variations:

  • Override the Authorize attribute (not so much to override in 3.x anymore)
  • Specifying options.Cookie.EventType with a Cookie (runtime error)
  • options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme (It was said that JWT bearer would not redirect to a login page)
  • And of course configuring the ApplicationCookie (but before the call to AddIdentity which doesn't work.

That all didn't work. But with the answer above I finally got the 401 Unauthorized returned (which should be Unauthenticated by the way)

Alan Savage
  • 842
  • 1
  • 15
  • 24
Bernoulli IT
  • 4,268
  • 2
  • 31
  • 50
  • 1
    There's no need to set the "location" header - that is only useful for a 3xx status code – Andy Feb 27 '20 at 11:14
  • Hi @Bernoulli, I am using asp.net core 3.1, have tried to put similar code in client application, but it did not work. – MayankGaur May 12 '20 at 05:00
  • What did not work? Did it not compile? Did it throw exceptions? What kind of exception / message? This answer's main point is the order in which "things" are declared which solved the OP's (and my) issue . – Bernoulli IT May 12 '20 at 09:00
  • In my case it also didn't work. If I put a breakpoint in OnRedirectToLogin or OnRedirectToAccessDenied function, it never breaks. I can't figure out how to get this to work on .NET Core 3.1. My objective is to be able to redirect to login page or return status 401, based on whether the called method was an API method or web page. – killswitch Mar 29 '21 at 20:53
0

For me on ASP.NET Core 2.2.0 only this worked:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(
        options =>
        {
            options.LoginPath = new PathString("/Account/Login");
            options.LogoutPath = new PathString("/Account/Logout");

            options.Events.OnRedirectToLogin = context =>
            {
                if (context.Request.Path.StartsWithSegments("/api")
                    && context.Response.StatusCode == StatusCodes.Status200OK)
                {
                    context.Response.Clear();
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return Task.CompletedTask;
                }
                context.Response.Redirect(context.RedirectUri);
                return Task.CompletedTask;
            };
        }
    );
SirGordon
  • 23
  • 9
0

In continuation, I merged the previous answers into the following:

1. Startup.cs

services.ConfigureApplicationCookie(options =>
        {
            options.LoginPath = new PathString("/Account/Login");
            options.LogoutPath = new PathString("/Account/Logout");

            options.Events.OnRedirectToAccessDenied = context =>
            {
                if (wlt_AjaxHelpers.IsAjaxRequest(context.Request))
                {
                    context.Response.Clear();
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return Task.CompletedTask;
                }
                context.Response.Redirect(context.RedirectUri);
                return Task.CompletedTask;
            };
        });

2. Helper custom class

public static class wlt_AjaxHelpers
     {

        public static bool IsAjaxRequest( HttpRequest request )
        {

            return string.Equals(request.Query["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal) ||
                string.Equals(request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.Ordinal);
        }

    }