20

Following the answer on this question, I have added authorization on everything by default, using the following code:

public void ConfigureServices(IServiceCollection aServices)
{
  aServices.AddMvc(options =>
  {
     var lBuilder = new AuthorizationPolicyBuilder().RequireAuthenticatedUser();

     var lFilter = new AuthorizeFilter(lBuilder.Build());
     options.Filters.Add(lFilter);
   });

   aServices.AddMvc();
}

public void Configure(IApplicationBuilder aApp, IHostingEnvironment aEnv, ILoggerFactory aLoggerFactory)
{
  aApp.UseCookieAuthentication(options =>
  {
    options.AuthenticationScheme = "Cookies";
    options.AutomaticAuthentication = true;
  });
}

However when someone tries to access something unauthorized, it returns a (what seems a default) redirect URL (http://foo.bar/Account/Login?ReturnUrl=%2Fapi%2Ffoobar%2F).

I want it to return a HTTP 401 only, instead of a redirect.

How can I do this in ASP.NET 5 for a WebAPI?

Tratcher
  • 5,371
  • 26
  • 42
Geerten
  • 987
  • 1
  • 7
  • 22

7 Answers7

30

I had with this problem in an Angular2 + ASP.NET Core application. I managed to fix it in the following way:

services.AddIdentity<ApplicationUser, IdentityRole>(config =>   {
    // ...
    config.Cookies.ApplicationCookie.AutomaticChallenge = false;
    // ...
});

If this is not working for you, you can try with the following method instead:

services.AddIdentity<ApplicationUser, IdentityRole>(config =>   {
    // ...
    config.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents
    {
       OnRedirectToLogin = ctx =>
       {
           if (ctx.Request.Path.StartsWithSegments("/api")) 
           {
               ctx.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
               // added for .NET Core 1.0.1 and above (thanks to @Sean for the update)
               ctx.Response.WriteAsync("{\"error\": " + ctx.Response.StatusCode + "}");
           }
           else
           {
               ctx.Response.Redirect(ctx.RedirectUri);
           }
           return Task.FromResult(0);
       }
    };
    // ...
}

Update for Asp.Net Core 2.0

Cookie options are now configured in the following way:

services.ConfigureApplicationCookie(config =>
            {
                config.Events = new CookieAuthenticationEvents
                {
                    OnRedirectToLogin = ctx => {
                        if (ctx.Request.Path.StartsWithSegments("/api"))
                        {
                            ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                        }
                        else {
                            ctx.Response.Redirect(ctx.RedirectUri);
                        }
                        return Task.FromResult(0);
                    }
                };
            });
Blake Mumford
  • 15,734
  • 12
  • 45
  • 67
Darkseal
  • 8,378
  • 6
  • 68
  • 98
  • Question/clarification -- Why should we check for HTTP 200 before changing it? If the redirect is happening, shouldn't we just act regardless? Thanks in advance. – John Hargrove Jan 27 '17 at 02:23
  • 1
    Updated answer accordingly. – Darkseal Jan 30 '17 at 14:36
  • Nice answer, pointed me in the right direction. You might also want to use OnRedirectToAccessDenied for authenticated (logged in) but not authorized users with HttpStatusCode.Forbidden for the api case and a redirect without the ReturnString for View users. – A.J.Bauer Feb 25 '17 at 10:50
  • This is the correct answer for ASP.NET MVC Core, and with Angular 2 apps. Thank you!!!! Drove me bunkers. Note: this is required when using JWT token auth via cookies - especially when switching from JWT bearer auth. – Max Apr 23 '17 at 04:12
  • 1
    Doesn't seem to work for .NET core 3.0 (preview) :/ – Bernoulli IT Jul 26 '19 at 10:47
4

By the url you get redirected to I assume you're using cookie authentication.

You should get the desired results by setting the LoginPath property of the CookieAuthenticationOptions to null or empty as described by one of the users.

app.UseCookieAuthentication(options =>
        {
            options.LoginPath = "";
        });

It was probably working back then but it's not working anymore (because of this change).

I've submitted a bug on GitHub for this.

I'll update the answer once it gets fixed.

mbudnik
  • 1,974
  • 12
  • 30
  • 1
    Indeed I use cookie authentication, so I added the CookieAuthentication code. Thanks for added the bug request, it's not working over here as well! – Geerten Oct 02 '15 at 07:42
  • Thanks for filing the issue. Reading the discussion was very helpful. – Aluan Haddad Dec 01 '15 at 04:18
  • That github bug thread is a really good read I agree. Summary is really that using cookies isn't great for web apis. – Almund Feb 01 '16 at 14:53
  • No longer works. The `config.Cookies.ApplicationCookie.Events` answer is the one that works now – Max Apr 23 '17 at 04:13
4

Setting LoginPath = "" or null no longer works on Version 1.1.0.0. So here's what I did:

app.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            ExpireTimeSpan = TimeSpan.FromDays(150),
            AuthenticationScheme = options.Cookies.ApplicationCookie.AuthenticationScheme,
            Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync,
                OnRedirectToLogin = async (context) => context.Response.StatusCode = 401,
                OnRedirectToAccessDenied = async (context) => context.Response.StatusCode = 403
            },
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,
        });
mode777
  • 2,597
  • 1
  • 18
  • 31
  • Just a heads up - you should also have `await Task.FromResult(0);` so that any exceptions thrown in the async methods are observed. e.g. `OnRedirectToLogin = async (context) => { context.Response.StatusCode = 401; await Task.FromResult(0); },` – Levi Fuller Jul 19 '17 at 04:22
4

I had a similar problem. I solved this adding by manually the services.

ConfigureServices method:

    services.AddTransient<IUserStore<User>, UserStore<User, IdentityRole, ApplicationDbContext>>();   
    services.AddTransient<IPasswordHasher<User>, PasswordHasher<User>>();
    services.AddTransient<IUserValidator<User>, UserValidator<User>>();
    services.AddTransient<ILookupNormalizer, UpperInvariantLookupNormalizer>();
    services.AddTransient<IPasswordValidator<User>, PasswordValidator<User>>();
    services.AddTransient<IdentityErrorDescriber, IdentityErrorDescriber>();
    services.AddTransient<ILogger<UserManager<User>>, Logger<UserManager<User>>>();
    services.AddTransient<UserManager<User>>();

    services.AddMvcCore()
    .AddJsonFormatters()
    .AddAuthorization();


    services.AddCors(options=> {
    options.AddPolicy("AllowAllHeaders", (builder) => {
        builder.WithOrigins("*").AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("WWW-Authenticate"); ;
    });
});


    services.AddAuthentication(options=> {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;
        options.ApiName = "api1";
        options.ApiSecret = "secret";
    });

Configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseCors("AllowAllHeaders");
    app.UseAuthentication();
    app.UseMvc();

}

I am using aspnet core 2.0, IdentityServer 4 and aspnet identity.

Brandon Minnick
  • 11,396
  • 12
  • 55
  • 108
  • Thank-you, your post helped me. – Sharbel Nov 02 '18 at 05:03
  • 1
    In my case the problem was I was using `services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)`. I thought It would just override all the parameters. Apparently I was wrong. You need to override `options.DefaultChallengeScheme` with `JwtBearerDefaults.AuthenticationScheme;` then It works. Tested on *ASP.NET Core preview 3* – Ahmad Jul 21 '19 at 18:14
1

Be aware, you should not use the CookieAuthentication only if you want to use your own Authentication Mechanism for example bypassing the Identity provider which not the case for most of us.

The default Identity provider use the CookieAuthenticationOptions behind the scene, you can configure it like the below.

services.AddIdentity<ApplicationUser, IdentityRole>(o =>
 {
     o.Password.RequireDigit = false;
     o.Password.RequireUppercase = false;
     o.Password.RequireLowercase = false;
     o.Password.RequireNonAlphanumeric = false;
     o.User.RequireUniqueEmail = true;

     o.Cookies.ApplicationCookie.LoginPath = null; // <-----
 })
 .AddEntityFrameworkStores<ApplicationDbContext>()
 .AddDefaultTokenProviders(); 

Tested in version 1.0.0

amd
  • 18,048
  • 6
  • 45
  • 64
1

in case it helps, below is my answer - with dotnet 1.0.1

its based on Darkseal's answer except I had to add the line ctx.Response.WriteAsync() to stop the redirect to the default 401 URL (Account/Login)

        // Adds identity to the serviceCollection, so the applicationBuilder can UseIdentity
        services.AddIdentity<ApplicationUser, IdentityRole>(options =>
        {                
            //note: this has no effect - 401 still redirects to /Account/Login!
            //options.Cookies.ApplicationCookie.LoginPath = null;

            options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents
            {
                OnRedirectToLogin = ctx =>
                {
                    //for WebApi: prevent aspnet core redirecting to 'Account/Login' on a 401:
                    if (ctx.Request.Path.StartsWithSegments("/api"))
                    {
                        ctx.RedirectUri = null;
                        ctx.Response.WriteAsync("{\"error\": " + ctx.Response.StatusCode + "}");
                    }
                    else
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                    }
                    return Task.FromResult(0);
                }
            };
        })
            .AddDefaultTokenProviders();
    }
Sean
  • 939
  • 8
  • 16
0

Use this code in Startup :

services.ConfigureApplicationCookie(options =>
            {
                options.LoginPath = $"/Account/Login";
                options.LogoutPath = $"/Account/Logout";
                options.AccessDeniedPath = $"/Account/AccessDenied";
                options.Events = new CookieAuthenticationEvents()
                {
                    OnRedirectToLogin = (ctx) =>
                    {
                        if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
                            ctx.Response.StatusCode = 401;
                        return Task.CompletedTask;
                    },
                    OnRedirectToAccessDenied = (ctx) =>
                    {
                        if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
                            ctx.Response.StatusCode = 403;
                        return Task.CompletedTask;
                    }
                };
            });
MohsenB
  • 1,223
  • 14
  • 27