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();
}