Search code examples
c#asp.net-web-apijwtauthorizationasp.net-identity

.NET 7 Controller Authorization with Identity and JWT


This is a follow up to this post:

.NET 7 Identity JwtBearer - Authorize Attribute doesn't affect API

I have a .NET web api communicating with React.ts and am attempting to add role based authorization via JWTs. Adding a default scheme in Program.cs and then adding [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] as mentioned in the comments of the above post worked great until it came time for roles. I can bypass Identity by adding tokens with roles via the cli or in appsettings, and if that's required I'll do that but I get the feeling that there is a way to pass Identity Roles in JWTs and read them from the backend.

At this time, [Authorize(Role = "User")] is responding with a 200 rather than a 401. Any suggestions or pointers to noob friendly documentation would be greatly appreciated.

AccountController token generation (commented out sections were replaced by claims):

    private async Task<string> CreateToken(ApplicationUser user)
    {
        //IList<string> roles = await _userManager.GetRolesAsync(user);

        List<Claim> claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, user.UserName),
            new Claim(ClaimTypes.Role, "User")
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
            _configuration.GetSection("AppSettings:Token").Value!));
            
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

        var token = new JwtSecurityToken(
                claims: claims,
                expires: DateTime.Now.AddDays(1),
                signingCredentials: creds
            );

        //token.Payload["roles"] = roles;
        //token.Payload["userId"] = user.Id;

        var jwt = new JwtSecurityTokenHandler().WriteToken(token);
        return jwt;  
}

TopicController to be blocked without role

namespace SdeResearch.Api.Controllers
{
    [ApiController]
    [Route("[controller]")]
    [Authorize(Roles = "User")]
    public class TopicController : ControllerBase
    {
        private readonly SdeResearchDbContext _db;

        public TopicController(SdeResearchDbContext db)
        {
            _db = db;
        }

        [HttpGet]
        [Route("/topic/get-all-topics")]
        public async Task<List<Topic>> GetTopicsAsync()
        {
            return await _db.Topics.ToListAsync();
        }
    }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("CORSPolicy", builder =>
    {
        builder
        .AllowAnyMethod()
        .AllowAnyHeader()
        .WithOrigins("http://localhost:3000");
    });
});
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey
    });

    options.OperationFilter<SecurityRequirementsOperationFilter>();
});

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateAudience = false,
            ValidateIssuer = false,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
                builder.Configuration.GetSection("AppSettings:Token").Value!)),
        };
    });

builder.Services.AddDbContext<SdeResearchDbContext>();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<SdeResearchDbContext>()
    .AddDefaultTokenProviders();

builder.Services.Configure<IdentityOptions>(options =>
{
    // Password Settings.
    options.Password.RequireDigit= true;
    options.Password.RequireLowercase= true;
    options.Password.RequireNonAlphanumeric= true;
    options.Password.RequireUppercase= true;
    options.Password.RequiredLength= 8;
    options.Password.RequireLowercase= true;
    options.Password.RequiredUniqueChars = 6;

    // User settings
    options.User.AllowedUserNameCharacters =
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+!";
    options.User.RequireUniqueEmail= true;
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.UseCors("CORSPolicy");

app.MapControllerRoute(
    name: "Topic",
    pattern: "topic/*{action}"
    );

app.Run();

DbContext

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace SdeResearch.Api.Models
{
    public class SdeResearchDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<Topic> Topics { get; set; }
        public DbSet<Researcher> Researchers { get; set; }
        public DbSet<Article> Articles { get; set; }
        public DbSet<ResearcherArticle> ResearcherArticles { get; set; }

        public SdeResearchDbContext(DbContextOptions<SdeResearchDbContext> options) : base(options) { }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseSqlServer("Server=MINE;Database=SdeResearch;Trusted_Connection=true;TrustServerCertificate=true");
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<IdentityRole>().HasData
            (
                new IdentityRole 
                {
                    Id = "DevelopmentAdministratorId",
                    Name = "Admin",
                    NormalizedName = "ADMIN".ToUpper()
                },
                new IdentityRole
                {
                    Id= "DevelopmentUserId",
                    Name= "User",
                    NormalizedName = "USER".ToUpper(),
                }
            );
        }
    }
}

EDIT:

After adding the following policy, and changing the authorize attribute to [Authorize(Policy = "edittopic")] it appears that authorize isn't doing anything. Am I missing a step with setting this up?

Program.cs below AddAuthentication

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("edittopic", policy =>
        policy.RequireRole("User")
    );
});

Solution

  • Some problems with your code is that you lack UseAuthentication before Authoriztation. This means that the user is never created (you are always anonymous). Ie, AddJwtBearer will never be used.

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

    I would also add

    builder.Services.AddAuthorization();