Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent redirect to /Account/Login in asp.net core 2.2

I am trying to prevent the app to redirect to /Account/Login in asp.net core 2.2 when the user isn't logged in.

Even though i write LoginPath = new PathString("/api/contests"); any unauthorized requests are still redirected to /Account/Login

This is my Startup.cs:

using System;
using System.Reflection;
using AutoMapper;
using Contest.Models;
using Contest.Tokens;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Swashbuckle.AspNetCore.Swagger;

namespace Contest
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddAutoMapper();

            // In production, the React files will be served from this directory
            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "clientapp/build";
            });

            // ===== Add our DbContext ========
            string connection = Configuration.GetConnectionString("DBLocalConnection");
            string migrationAssemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            services.AddDbContext<ContestContext>(options =>
                    options.UseSqlServer(connection,
                    sql => sql.MigrationsAssembly(migrationAssemblyName)));

            // ===== Add Identity ========
            services.AddIdentity<User, IdentityRole>(o =>
            {
                o.User.RequireUniqueEmail = true;
                o.Tokens.EmailConfirmationTokenProvider = "EMAILCONF";
                // I want to be able to resend an `Email` confirmation email
                // o.SignIn.RequireConfirmedEmail = true; 
            }).AddRoles<IdentityRole>()
                .AddEntityFrameworkStores<ContestContext>()
                .AddTokenProvider<EmailConfirmationTokenProvider<User>>("EMAILCONF")
                .AddDefaultTokenProviders();

            services.Configure<DataProtectionTokenProviderOptions>(o =>
                o.TokenLifespan = TimeSpan.FromHours(3)
            );

            services.Configure<EmailConfirmationTokenProviderOptions>(o =>
                o.TokenLifespan = TimeSpan.FromDays(2)
            );

            // ===== Add Authentication ========

            services.AddAuthentication(o => o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.Cookie.Name = "auth_cookie";
                    options.Cookie.SameSite = SameSiteMode.None;
                    options.LoginPath = new PathString("/api/contests");
                    options.AccessDeniedPath = new PathString("/api/contests");
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnRedirectToLogin = context =>
                        {
                            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                            return Task.CompletedTask;
                        },
                    };
                });

            // ===== Add Authorization ========

            services.AddAuthorization(o =>
            {

            });

            services.AddCors();

            // ===== Add MVC ========
            services.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                    .RequireAuthenticatedUser()
                                    .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            })
                .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore)
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            // ===== Add Swagger ========
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info
                {
                    Title = "Core API",
                    Description = "Documentation",
                });

                var xmlPath = $"{System.AppDomain.CurrentDomain.BaseDirectory}Contest.xml";
                c.IncludeXmlComments(xmlPath);
            });

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseSpaStaticFiles(new StaticFileOptions()
            {

            });

            app.UseCors(policy =>
            {
                policy.AllowAnyHeader();
                policy.AllowAnyMethod();
                policy.AllowAnyOrigin();
                policy.AllowCredentials();
            });

            app.UseAuthentication();

            app.UseMvc();

            if (env.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI(c =>
                {
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Core API");
                });
            }

            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "clientapp";

                if (env.IsDevelopment())
                {
                    // spa.UseReactDevelopmentServer(npmScript: "start");
                    spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
                }
            });

        }

    }
}

I managed to bypass this by creating a controller to handle this route:

[Route("/")]
[ApiController]
public class UnauthorizedController : ControllerBase
{
    public UnauthorizedController()
    {

    }

    [HttpGet("/Account/Login")]
    [AllowAnonymous]
    public IActionResult Login()
    {
        HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return new ObjectResult(new
        {
            StatusCode = StatusCodes.Status401Unauthorized,
            Message = "Unauthorized",
        });
    }

}

What is wrong with my setup? Thanks

like image 266
Tănase Hagi Avatar asked Dec 25 '18 21:12

Tănase Hagi


3 Answers

Hey there and welcome to StackOverflow 👋

The behaviour you experience is linked to the fact that you use ASP.NET Identity. When you call services.AddIdentity, behind the scenes a cookie-based authentication scheme is registered and set as the default challenge scheme, as you can see in the code here on GitHub.

Even though you registered a cookie authentication scheme yourself and set it as the default scheme, the specific default schemes — like AuthenticateScheme, ChallengeScheme, SignInScheme, etc... — take precendence. DefaultScheme is used by the authentication system only when the specific one is not set.

To answer your question, you could apply the configuration settings to the ASP.NET Identity cookie options by using the helper method services.ConfigureApplicationCookie, like so:

// ===== Add Identity ========
services.AddIdentity<User, IdentityRole>(o =>
{
    o.User.RequireUniqueEmail = true;
    o.Tokens.EmailConfirmationTokenProvider = "EMAILCONF";
    // I want to be able to resend an `Email` confirmation email
    // o.SignIn.RequireConfirmedEmail = true; 
}).AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<ContestContext>()
    .AddTokenProvider<EmailConfirmationTokenProvider<User>>("EMAILCONF")
    .AddDefaultTokenProviders();

services.Configure<DataProtectionTokenProviderOptions>(o =>
    o.TokenLifespan = TimeSpan.FromHours(3)
);

services.Configure<EmailConfirmationTokenProviderOptions>(o =>
    o.TokenLifespan = TimeSpan.FromDays(2)
);

// ===== Configure Identity =======
service.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = "auth_cookie";
    options.Cookie.SameSite = SameSiteMode.None;
    options.LoginPath = new PathString("/api/contests");
    options.AccessDeniedPath = new PathString("/api/contests");

    // Not creating a new object since ASP.NET Identity has created
    // one already and hooked to the OnValidatePrincipal event.
    // See https://github.com/aspnet/AspNetCore/blob/5a64688d8e192cacffda9440e8725c1ed41a30cf/src/Identity/src/Identity/IdentityServiceCollectionExtensions.cs#L56
    options.Events.OnRedirectToLogin = context =>
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return Task.CompletedTask;
    };
});

It also means that you can safely remove the part where you add a cookie-based authentication scheme since this is taken care of by ASP.NET Identity itself.

Let me know how you go!

like image 91
Mickaël Derriey Avatar answered Nov 20 '22 10:11

Mickaël Derriey


If you are not using ASP.NET Identity, you can follow the same pattern that kuldeep chopra mentioned in another answer, but instead inside the AddCookie method:

public void ConfigureServices(IServiceCollection services)
{
 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie((o) =>
    {
      o.Cookie.HttpOnly = true;
      o.LoginPath = string.Empty;
      o.AccessDeniedPath = string.Empty;
      o.Events.OnRedirectToLogin = context =>
      {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return Task.CompletedTask;
      };
    });
}

It is not enough to just set the paths to empty/null.

like image 11
jnt Avatar answered Nov 20 '22 08:11

jnt


 services.ConfigureApplicationCookie(options => {
            options.AccessDeniedPath = "/Account/Login";
            options.LoginPath = "/Account/Denied";
            options.Cookie.HttpOnly = true;
            options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
            options.Events.OnRedirectToLogin = context => {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return Task.CompletedTask;
            };
        });
like image 5
kuldeep chopra Avatar answered Nov 20 '22 10:11

kuldeep chopra