Background
I am doing a POC to find out if Angular, Razor and MVC pages work seamlessly in a web application. I started with Visual Studio template named "ASP.NET Core with Angular". I have selected "Individual Accounts" to include default authentication functionality. This creates an Angular app with a secure web API endpoint (WeatherForecast) and provides basic user registration, login, logout, user profile pages etc features built in. So far all works well, when I try to fetch data from the protected API (WeatherForecast) I get redirected to the Identiy/Account/Login razor page where I can login and then get redirected back to Angular and I can see that data is returned and grid is populated. Till this point everything works fine.
The Problem
I added a DemoController class with a basic "Hello World" HTML view. When I try to access this new page with /demo, it works as expected. However, when I apply [Authorize]
attribute to the controller, I get 401 Unauthorized. I checked on server side that User.IsAuthenticated
property is set to false
despite having successfully logged in before. Now interesting observation is that the user profile page (which is protected and works only if there an active login) works fine.
Please note that all API calls issues from Angular use JWT bearer token and work fine. When I try to access user profile page, it does NOT use JWT, it uses cookies to authenticate. The GET request to /demo page also has all these cookies in headers, still it is met with 401.
I spent a lot of time going thru articles, searching web with no success. The closing thing we found is this : ASP.NET Core 5.0 JWT authentication is throws 401 code But that didn't help either.
The project is created using Visual Studio 2022, .net core 6.0. Here is the Program.cs file for your reference:
using CoreAngular.Data;
using CoreAngular.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
builder.Services.AddAuthentication()
.AddIdentityServerJwt();
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapRazorPages();
app.MapFallbackToFile("index.html"); ;
app.Run();
This has been answered here: https://stackoverflow.com/a/62090053/3317709
It turned out that using IdentityServer extension methods add a policy scheme such that only /Identity pages have cookie authentication. The rest default to JWT.
We can customize this by adding our own policy like so:
builder.Services.AddAuthentication()
.AddIdentityServerJwt()
.AddPolicyScheme("ApplicationDefinedAuthentication", null, options =>
{
options.ForwardDefaultSelector = (context) =>
{
if (context.Request.Path.StartsWithSegments(new PathString("/Identity"), StringComparison.OrdinalIgnoreCase) ||
context.Request.Path.StartsWithSegments(new PathString("/demo"), StringComparison.OrdinalIgnoreCase))
return IdentityConstants.ApplicationScheme;
else
return IdentityServerJwtConstants.IdentityServerJwtBearerScheme;
};
});
// Use own policy scheme instead of default policy scheme that was set in method AddIdentityServerJwt
builder.Services.Configure<AuthenticationOptions>(options => options.DefaultScheme = "ApplicationDefinedAuthentication");