Core 5, PasswordSignInAsync fails to set cookie in dual Authentication scheme

Viewed 239

I am writing a frontend/backend application. The frontend is an Angular 13 application. The backend is a combination backend API and administration web site. The backend has:

  • Local Identity (including Identity scaffolding),
  • Web API (for Angular frontend using Swagger bearer tokens),
  • MVC view/controllers for side table administration.

The frontend needs to access API services. Login returns a token. The token is used to access the various services to maintain the application tables.

The backend .net 5 Core website reads and writes to a local SQL Server database. The database contains the Identity tables. The backend is also used to maintain the Identity tables using the scaffolding Razor pages. The backend maintains (basic CRUD) for a number of administrative tables. So, a user or an administrator logons via the scaffolding logon form using the same logon account that is used for the Angular frontend.

The problem is the login via PasswordSignInAsync is successful but User.Identity.IsAuthenticated is false. I use User.Identity in lots of places for name, roles and IsAuthenticated. I got a sense that User.Identity is supposed to happen automatically when using cookie authentication scheme. I added dual schemes, but that has not solved the problem. I have read through a number of questions per PasswordSignInAsync not working, but none seemed to help. The things I tried to solve the problem may have adversely affecting the outcome. I have read the source code of CheckPasswordSignInAsync and I do not see any setting of User.Identity. Not knowing what to do to get beyond this issue.

Please feel free to ask for any clarifications.

I’m showing my complete Startup.cs.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using System;
using System.Threading.Tasks;
//
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using FluentValidation.AspNetCore;
using MediatR;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using NSG.NetIncident4.Core.Domain.Entities.Authentication;
using NSG.NetIncident4.Core.Infrastructure.Authentication;
using NSG.NetIncident4.Core.Infrastructure.Common;
using NSG.NetIncident4.Core.Infrastructure.Notification;
using NSG.NetIncident4.Core.Infrastructure.Services;
//
namespace NSG.NetIncident4.Core
{
  public class Startup
  {
    public Startup(IConfiguration configuration)
    {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
      //
      services.AddControllers();
      /*
      ** Configure logging
      */
      services.AddLogging(builder => builder
        .AddConfiguration(Configuration.GetSection("Logging"))
        .AddConsole()
        .AddDebug()
      );
      /*
      ** Cookie options.
      */
      #region Cookie options
      services.Configure<CookiePolicyOptions>(options =>
      {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
      });
      services.ConfigureApplicationCookie(options =>
      {
        // Cookie settings
        options.Cookie.HttpOnly = true;
        options.Cookie.Name = "Net-Incident.Identity";
        options.LoginPath = new PathString("/Account/Login");
        options.LogoutPath = new PathString("/Account/Logout");
        options.AccessDeniedPath = new PathString("/Account/AccessDenied");
        options.SlidingExpiration = true;
        options.ExpireTimeSpan = TimeSpan.FromHours(2);
      });
      #endregion
      /*
      ** Read values from the 'appsettings.json'
      ** * Add and configure email/notification services
      ** * Services like ping/whois
      ** * Applications information line name and phone #
      ** * Various authorization information
      */
      services.Configure<MimeKit.NSG.EmailSettings>(Configuration.GetSection("EmailSettings"));
      services.Configure<ServicesSettings>(Configuration.GetSection("ServicesSettings"));
      services.Configure<ApplicationSettings>(Configuration.GetSection("ApplicationSettings"));
      AuthSettings _authSettings = Options.Create<AuthSettings>(
        Configuration.GetSection("AuthSettings").Get<AuthSettings>()).Value;
      services.Configure<AuthSettings>(Configuration.GetSection("AuthSettings"));
      // call local method below
      ConfigureDatabase(services);
      /*
      ** For Identity
      */
      services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddDefaultUI()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
      /*
      ** Add CORS
      */
      services.AddCors(options => {
        options.AddPolicy("CorsAnyOrigin", builder => {
          builder
            .WithOrigins("http://localhost:4200,http://localhost:10111")
            .AllowAnyOrigin()
            .AllowAnyHeader()
            .AllowAnyMethod();
        });
      });
      /*
      ** Add Authentication
      */
      #region Add Authentication
      services.AddAuthentication(options =>
      {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = "BearerOrCookie";
      })
        /*
        ** Add Jwt Bearer
        */
        .AddJwtBearer(options =>
        {
          options.SaveToken = true;
          options.RequireHttpsMetadata = false;
          options.TokenValidationParameters = new TokenValidationParameters()
          {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidAudience = Configuration["JWT:ValidAudience"],
            ValidIssuer = Configuration["JWT:ValidIssuer"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
          };
        })
        /*
        ** Add cookie authentication scheme
        */
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        /*
        ** Conditionally add either JWT bearer of cookie authentication scheme
        */
        .AddPolicyScheme("BearerOrCookie", "Custom JWT bearer or cookie", options =>
        {
          options.ForwardDefaultSelector = context =>
          {
            // since all my api will be starting with /api, modify this condition as per your need.
            if (context.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
              return JwtBearerDefaults.AuthenticationScheme;
            else
              return CookieAuthenticationDefaults.AuthenticationScheme;
          };
        });
      #endregion // Add Authentication
      /*
      ** Add authorization services
      */
      services.AddAuthorization(options =>
      {
        options.AddPolicy("AdminRole", policy => policy.RequireRole("Admin"));
        options.AddPolicy("CompanyAdminRole", policy => policy.RequireRole("Admin", "CompanyAdmin"));
        options.AddPolicy("AnyUserRole", policy => policy.RequireRole("User", "Admin", "CompanyAdmin"));
        Console.WriteLine(options);
      });
      /*
      ** Add Swagger services
      */
      #region Add Swagger
      services.AddSwaggerGen(swagger =>
      {
        // Generate the Default UI of Swagger Documentation
        swagger.SwaggerDoc("v1", new OpenApiInfo
        {
          Version = "v1",
          Title = "NSG Net-Incident4.Core",
          Description = "Authentication and Authorization in ASP.NET 5 with JWT and Swagger"
        });
        // To Enable authorization using Swagger (JWT)
        swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
        {
          Name = "Authorization",
          Type = SecuritySchemeType.ApiKey,
          Scheme = "Bearer",
          BearerFormat = "JWT",
          In = ParameterLocation.Header,
          Description = "Enter 'Bearer' [space] and then your valid token in the text input below.\r\n\r\nExample: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\"",
        });
        swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
          {
            new OpenApiSecurityScheme
            {
              Reference = new OpenApiReference
              {
                Type = ReferenceType.SecurityScheme,
                Id = "Bearer"
              }
            },
            new string[] {}
          }
        });
      });
      #endregion // end of AddSwagger
      /*
      ** Add email/notification services
      */
      services.AddSingleton<INotificationService, NotificationService>();
      services.AddSingleton<IEmailSender, NotificationService>();
      // Add framework services.
      services.AddTransient<IApplication, ApplicationImplementation>();
      /*
      ** Configure MVC service.
      */
      services.AddMvc(option => option.EnableEndpointRouting = false)
        .AddFeatureFolders()
        .AddAreaFeatureFolders()
        .AddApplicationPart(typeof(Startup).Assembly)
        .AddSessionStateTempDataProvider()
        .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>())
        .AddRazorPagesOptions(options =>
        {
          options.Conventions.Clear();
          options.RootDirectory = "/UI/Identity";
        });
      /*
      ** Configure locations of views
      */
      #region View locations
      services.Configure<RazorViewEngineOptions>(options =>
      {
        // https://github.com/OdeToCode/AddFeatureFolders
        // {2} is area, {1} is controller,{0} is the action
        options.ViewLocationFormats.Clear();
        options.ViewLocationFormats.Add("/UI/Views/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.ViewLocationFormats.Add("/UI/Views/Admin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.ViewLocationFormats.Add("/UI/Views/CompanyAdmin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.ViewLocationFormats.Add("/UI/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
        // now razor pages
        options.PageViewLocationFormats.Clear();
        options.PageViewLocationFormats.Add("/UI/Views/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Views/Admin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Views/CompanyAdmin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Identity/{0}" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Identity/Account/{0}" + RazorViewEngine.ViewExtension);
        options.PageViewLocationFormats.Add("/UI/Identity/Account/Manage/{0}" + RazorViewEngine.ViewExtension);
        //
        options.AreaViewLocationFormats.Clear();
        options.AreaViewLocationFormats.Add("/UI/Views/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.AreaViewLocationFormats.Add("/UI/Views/Admin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.AreaViewLocationFormats.Add("/UI/Views/CompanyAdmin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.AreaViewLocationFormats.Add("/UI/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
        // now razor areas
        options.AreaPageViewLocationFormats.Clear();
        options.AreaPageViewLocationFormats.Add("/UI/Views/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Views/Admin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Views/CompanyAdmin/{1}/{0}" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
        options.AreaPageViewLocationFormats.Add("/UI/Identity/Account/Manage/{0}" + RazorViewEngine.ViewExtension);
      });
      #endregion // View locations
      /*
      ** Add session for state for temp data provider
      */
      services.AddSession(options =>
      {
        options.Cookie.IsEssential = true;
      });
      //
    }
    //
    /// <summary>
    /// An overridable method, that allows for different configuration.
    /// </summary>
    /// <param name="services">The current collection of services, 
    /// add DB contect to the container.
    /// </param>
    public virtual void ConfigureDatabase(IServiceCollection services)
    {
      string _connetionString = Configuration.GetConnectionString("DefaultConnection");
      if (string.IsNullOrEmpty(_connetionString))
      {
        throw (new ApplicationException("No connection string found"));
      }
      services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(_connetionString));
    }
    //
    /// <summary>
    /// This method gets called by the runtime.
    /// Use this method to configure the HTTP request pipeline.
    /// </summary>
    public void Configure(
      IApplicationBuilder app,
      IWebHostEnvironment env,
      ApplicationDbContext context,
      UserManager<ApplicationUser> userManager,
      RoleManager<ApplicationRole> roleManager)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }
      else
      {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
      }
      //
      app.UseHttpsRedirection();
      app.UseStaticFiles();
      app.UseRouting();
      // routing/CORS/endpoint
      app.UseCors("CorsAnyOrigin");
      app.UseAuthentication();
      app.UseAuthorization();
      app.UseCookiePolicy();
      app.UseSession();
      app.UseSwagger();
      // app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NSG Net-Incident4.Core v1"));
      app.UseMvc(routes =>
      {
        routes.MapRoute(
          name: "default",
          template: "{controller=Home}/{action=Index}/{id?}");
      });
      //
    }
  }
}

A snippet of the login.chtml.cs:

    public async Task<IActionResult> OnPostAsync(string returnUrl = null)
    {
      returnUrl ??= Url.Content("~/");
      ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
      if (ModelState.IsValid)
      {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
          _logger.LogInformation($"User: {Input.UserName} is logged in.");
          //
          var _user = await _signInManager.UserManager.FindByNameAsync(Input.UserName);
          ClaimsPrincipal _userPrincipal = await _signInManager.CreateUserPrincipalAsync(_user);
          await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, _userPrincipal);
          //
          return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
          return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
        }
        if (result.IsLockedOut)
        {
          _logger.LogWarning("User account locked out.");
          return RedirectToPage("./Lockout");
        }
        else
        {
          ModelState.AddModelError(string.Empty, "Invalid login attempt.");
          return Page();
        }
      }
      // If we got this far, something failed, redisplay form
      return Page();
    }
1 Answers

After starting from ground zero, I feel I found the problem. I am now getting logon via Swagger API service (Angular 13) and the logon.cshtml Identity Razor scaffolding page.

When one properly adds the Identity scaffolding, one needs to change/review the IdentityHostingStartup.cs file. My updated IdentityHostingStartup is as follows:

using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NSG.NetIncident4.Core.Domain.Entities.Authentication;

[assembly: HostingStartup(typeof(NSG.NetIncident4.Core.UI.Identity.IdentityHostingStartup))]
namespace NSG.NetIncident4.Core.UI.Identity
{
    public class IdentityHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices((context, services) => {
                //
                string _connetionString = context.Configuration.GetConnectionString("DefaultConnection");
                if (string.IsNullOrEmpty(_connetionString))
                {
                    throw (new ApplicationException("No connection string found"));
                }
                services.AddDbContext<ApplicationDbContext>(options =>
                    options.UseSqlServer(_connetionString));
                services.AddDefaultIdentity<ApplicationUser>(options => {
                        options.SignIn.RequireConfirmedAccount = true;
                        options.Password.RequireDigit = true;
                        options.Password.RequiredLength = 8;
                        options.Password.RequireLowercase = true;
                        options.Password.RequireUppercase = true;
                        options.Password.RequireNonAlphanumeric = true;
                    })
                    .AddRoles<ApplicationRole>()
                    .AddEntityFrameworkStores<ApplicationDbContext>();
                //
            });
        }
    }
}

This made the Startup.cs file much simpler. The Startup adds the following services:

  • Logging,
  • Controllers with views,
  • View Locations,
  • Notification,
  • Authorization policies,
  • Session,
  • CORS,
  • Swagger.

The Configure/use method stayed the same.

Related