28

After upgrading my ASP.NET Core project to 2.0, attempts to access protected endpoints no longer returns 401, but redirects to an (non-existing) endpoint in an attempt to let the user authenticate.

The desired behaviour is for the application simply to return a 401. Previously I would set AutomaticChallenge = false when configuring authentication, but according to this article the setting is no longer relevant (in fact it doesn't exist anymore).

My authentication is configured like this:

Startup.cs.ConfigureServices():

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(o =>
                {
                    o.Cookie.Name = options.CookieName;
                    o.Cookie.Domain = options.CookieDomain;
                    o.SlidingExpiration = true;
                    o.ExpireTimeSpan = options.CookieLifetime;
                    o.TicketDataFormat = ticketFormat;
                    o.CookieManager = new CustomChunkingCookieManager();
                });

Configure():

app.UseAuthentication();

How can I disable automatic challenge, so that the application returns 401 when the user is not authenticated?

severin
  • 5,203
  • 9
  • 35
  • 48
  • How were you getting your user to log in after the 401? – Tratcher Aug 25 '17 at 14:07
  • What I did for (temporarly) solving this problem was using my custom `[Auth]` attribute, and I was returning 400 instead of 401. – Ozgur Aug 28 '17 at 07:10
  • Could you mention what this property does? – Mohammed Noureldin Nov 12 '17 at 23:16
  • Reference: [Setting default authentication schemes](https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x?view=aspnetcore-2.2#setting-default-authentication-schemes) which discuss the changes to `AutomaticChallenge` and `AutomaticAuthenticate` – spottedmahn May 22 '19 at 14:31

8 Answers8

33

As pointed out by some of the other answers, there is no longer a setting to turn off automatic challenge with cookie authentication. The solution is to override OnRedirectToLogin:

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

This may change in the future: https://github.com/aspnet/Security/issues/1394

severin
  • 5,203
  • 9
  • 35
  • 48
26

After some research, I found we can deal with this problem though the bellow approach:

We can add two Authentication scheme both Identity and JWT; and use Identity scheme for authentication and use JWT schema for challenge, JWT will not redirect to any login route while challenge.

services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();

services.AddAuthentication((cfg =>
{
    cfg.DefaultScheme = IdentityConstants.ApplicationScheme;
    cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})).AddJwtBearer();
Zheng Xing
  • 277
  • 2
  • 7
  • 2
    This is a working answer. Previously in ASP.NET Core 1.1 I was using the exact same approach, after upgrading to 2.0 I had some troubles configuring JwtBearer authentication but following your code solved the issue. Now everything works like a charm. – James L. Oct 03 '17 at 17:33
  • 1
    Keep in mind that the order is important. Didn't work for me while AddAuthentication was before AddIdentity. The order in this answer is correct. – Jovica Zaric Feb 08 '19 at 14:50
  • Additional details here in the ASP.NET Core doc pages: [Setting default authentication schemes](https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x?view=aspnetcore-2.2#setting-default-authentication-schemes) – spottedmahn May 22 '19 at 14:33
  • I had to log in just to give this answer an upvote, I've been stuck trying to achieve this for months! This works perfectly in 2.x! – skidoodle3336 Apr 04 '20 at 18:09
  • I ran into something very similar. For my particular case, I additionally had to set the _DefaultSignInScheme_ and _DefaultAuthenticateScheme_ to use the JwtBearerDefaults.AuthenticationScheme as well. Thank you for pointing me in the right direction. – Josh Stella Aug 07 '20 at 14:36
11

Similiar to @Serverin, setting the OnRedirectToLogin of the Application Cookie worked, but must be done in statement following services.AddIdentity in Startup.cs:ConfigureServices:

services.ConfigureApplicationCookie(options => {
  options.Events.OnRedirectToLogin = context => {
    context.Response.Headers["Location"] = context.RedirectUri;
    context.Response.StatusCode = 401;
    return Task.CompletedTask;
  };
});
sammarcow
  • 2,736
  • 2
  • 28
  • 56
  • 1
    My Man! Finally! I tried so many variations on this subject and this did the trick! Configure it after the call to `services.AddIdentity`. How bizarre that this order of declarations makes a difference :/ Microsoft?! – Youp Bernoulli Aug 01 '19 at 11:42
  • 1
    This should be the accepted answer (at least for .NET Core 3.x (preview)) for which I could get it to work. – Youp Bernoulli Aug 01 '19 at 11:42
  • There's no need to set the "location" header - that is only useful for a 3xx status code – Andy Feb 27 '20 at 11:16
  • @Andy The location header can help js/angular code redirect to login. since an automatic login (302) from an ajax call when isp in a different domain will result with a CORS error. Hence the need for 401 response. – mamashare Dec 16 '20 at 08:45
4

According to this article:

In 1.x, the AutomaticAuthenticate and AutomaticChallenge properties were intended to be set on a single authentication scheme. There was no good way to enforce this.

In 2.0, these two properties have been removed as flags on the individual AuthenticationOptions instance and have moved into the base AuthenticationOptions class. The properties can be configured in the AddAuthentication method call within the ConfigureServices method of Startup.cs

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);

Alternatively, use an overloaded version of the AddAuthentication method to set more than one property. In the following overloaded method example, the default scheme is set to CookieAuthenticationDefaults.AuthenticationScheme. The authentication scheme may alternatively be specified within your individual [Authorize] attributes or authorization policies.

services.AddAuthentication(options => {
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});

Define a default scheme in 2.0 if one of the following conditions is true:

  • You want the user to be automatically signed in
  • You use the [Authorize] attribute or authorization policies without specifying schemes

An exception to this rule is the AddIdentity method. This method adds cookies for you and sets the default authenticate and challenge schemes to the application cookie IdentityConstants.ApplicationScheme. Additionally, it sets the default sign-in scheme to the external cookie IdentityConstants.ExternalScheme.

Hope this help you.

Sergey
  • 287
  • 1
  • 4
  • 1
    Thank you for your reply, but I'm afraid it doesn't solve my problem. If I do not specify a DefaultChallengeScheme the application throws an error: "InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found." – severin Aug 28 '17 at 12:47
3

This is the source code of CookieAuthenticationEvents.OnRedirectToLogin :

public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
    if (IsAjaxRequest(context.Request))
    {
        context.Response.Headers["Location"] = context.RedirectUri;
        context.Response.StatusCode = 401;
    }
    else
    {
        context.Response.Redirect(context.RedirectUri);
    }
    return Task.CompletedTask;
};

You can add "X-Requested-With: XMLHttpRequest" Header to the request while making API calls from your client.

Zack
  • 3,799
  • 2
  • 11
  • 12
  • Hmm I wonder why it's setting the "location" header. according to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location that has no meaning for a 4xx response code – Andy Feb 27 '20 at 12:51
3

I found that in most cases the solution is to override

OnRedirectToLogin

But in my app I was using multiple authentication policies and overriding of the OnRedirectToLogin did not work for me. The solution in my case it was to add a simple middleware to redirect the incoming request.

app.Use(async (HttpContext context, Func<Task> next) => {
    await next.Invoke(); //execute the request pipeline

    if (context.Response.StatusCode == StatusCodes.Status302Found && context.Response.Headers.TryGetValue("Location", out var redirect)) {
        var v = redirect.ToString();
        if (v.StartsWith($"{context.Request.Scheme}://{context.Request.Host}/Account/Login")) {
            context.Response.Headers["Location"] = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
            context.Response.StatusCode = 401;
        }
    }
});

1

Another way to do this which is more DI/testing-friendly is to use AuthenticationSchemeOptions.EventsType (another answer points at it here). This will allow you to pull other components into the resolution process.

Here's an example including registration and resolution which stops the default redirect to login on an unauthenticated request, and instead just returns with a hard 401. It also has a slot for any other dependencies which may need to know about unauthenticated requests.

In Startup.cs:

services
    .AddAuthentication("MyAuthScheme")
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
    {
        options.EventsType = typeof(MyEventsWrapper);
    };

...

services.AddTransient<MyEventsWrapper>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

Then, in MyEventsWrapper.cs:

public class MyEventsWrapper : CookieAuthenticationEvents
{
    private readonly IHttpContextAccessor _accessor;
    private readonly IDependency _otherDependency;

    public MyEventsWrapper(IHttpContextAccessor accessor,
                           IDependency otherDependency)
    {
        _accessor = accessor;
        _otherDependency = otherDependency;
    }

    public override async Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
    {
        context.Response.Headers.Remove("Location");
        context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
        await _otherDependency.Cleanup(_accessor.HttpContext);
    }
}
eouw0o83hf
  • 9,438
  • 5
  • 53
  • 75
0

I'm not sure how to generate the 401 error, however if you use the:

o.AccessDeniedPath = "{path to invalid}";

This will allow you to redirect somewhere when the challenge has failed.

Gary Holland
  • 2,565
  • 1
  • 16
  • 17