Search code examples
c#visual-studiogoogle-chromeasp.net-coreiis-express

Cookie Authentication Fails ASP.NET Core 3.1


Rather than migrating an existing ASP.NET Core 2.1 project to 3.1 I've created a new project. In the new project the SignIn(...) controller method executes without error but then it fails to redirect to the specified action. Instead it is redirecting to AccessDeniedPath.

My environment is Win10 VS 2019 IIS Express using Chrome.

Can anyone advise what could be causing this? Is there something I'm missing or doing wrong in 3.1?

PROGRAM.CS

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            //webBuilder.UseIISIntegration();
            //webBuilder.UseKestrel();
            //webBuilder.CaptureStartupErrors(true);
            //webBuilder.UseEnvironment(Environments.Development);
        });

STARTUP.CS

    public void ConfigureServices(IServiceCollection services)
    {
        try
        {
            services.AddRazorPages()        
                .AddRazorRuntimeCompilation();

            services.AddControllersWithViews();

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                {
                    options.ExpireTimeSpan = new TimeSpan(30, 0, 0, 0);
                    options.LoginPath = new PathString("/Home/Index/");
                    options.AccessDeniedPath = new PathString("/Home/Index/");
                    options.LogoutPath = new PathString("/Home/Index/");
                    options.Validate();
                });

            services.Configure<Microsoft.AspNetCore.Identity.IdentityOptions>(options =>
            {
                options.Password.RequireDigit = true;
                options.Password.RequireLowercase = true;
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireUppercase = true;
                options.Password.RequiredLength = 8;
                options.Password.RequiredUniqueChars = 1;
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                options.Lockout.MaxFailedAccessAttempts = 5;
                options.Lockout.AllowedForNewUsers = true;
                options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
            });

            services.AddDetectionCore()
                .AddDevice();

            services.AddMvc();
            services.AddAntiforgery();
            services.Configure<MvcOptions>(options =>
            {
                options.Filters.Add(new RequireHttpsAttribute());
            });
        }
        catch (Exception ex)
        {
            gFunc.ProcessError(ex);
        }
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        try
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            app.UseStaticFiles();
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseAuthentication();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
        catch (Exception ex)
        {
            gFunc.ProcessError(ex);
        }
    }

CONTROLLER.CS

    [HttpPost()]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> SignIn(SignIn sin)
    {
        bool is_err = false;
        try
        {
            // check data
            if (string.IsNullOrEmpty(sin.EmailAddress))
            {
                is_err = true;
                ModelState.AddModelError("Samadhi", "Missing email address.");
            }
            if (string.IsNullOrEmpty(sin.Password))
            {
                is_err = true;
                ModelState.AddModelError("Samadhi", "Missing password.");
            }

            // check authorisation
            if (ModelState.IsValid && !is_err)
            {
                sin = await RepoSamadhi.ShopSignIn(sin);
                if (sin.ShopID == 0 || sin.IsValidationFail || string.IsNullOrEmpty(sin.ShopToken))
                {
                    is_err = true;
                    ModelState.AddModelError("Samadhi", "Account not found. Check your credentials.");
                }
            }

            // check model state
            if (!ModelState.IsValid || is_err)
            {
                sin.IsSignInFailed = true;
                return View("SignIn", sin);
            }

            // create claims
            var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Sid, sin.ShopToken),
            new Claim(ClaimTypes.NameIdentifier, sin.ShopID.ToString()),
            new Claim(ClaimTypes.Email, sin.EmailAddress.ToLower()),
            new Claim(ClaimTypes.Role, "SamadhiShop")
        };

            // create identity
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); 

            // create principal
            ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));

            var authProperties = new AuthenticationProperties
            {
                IsPersistent = true // sin.RememberMe
            };

            // sign-in
            await HttpContext.SignInAsync(scheme: CookieAuthenticationDefaults.AuthenticationScheme, principal: principal, properties: authProperties);
        }
        catch (Exception ex)
        {
            gFunc.ProcessError(ex);
        }
        return RedirectToAction("Console", new { date = DateTime.Today.ToString("d MMM yyyy"), timer = false });
    }

    [HttpGet]
    [Authorize]
    public async Task<ActionResult> Console(string date, bool timer)
    {
        int shop_id = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
        string token = User.FindFirst(ClaimTypes.Sid).Value;

        DateTime dt = DateTime.Parse(date);
        Shop ss = await RepoSamadhi.GetShop(shop_id, token, dt);
        ss.IsTimerOn = timer;
        return View((_device.Type == DeviceType.Mobile) ? "ConsoleM" : "ConsoleM", ss);
    }

Solution

  • The order of your middleware is wrong, the correct order should be

    app.UseAuthentication(); 
    app.UseAuthorization();