Search code examples
c#asp.net-coreauthenticationrolesclaims-based-identity

.net custom claims identity authentication: User role is always null


I'm working in a .Net api which uses jwt token auth, and everything's working fine regarding token generation/validation, but my problem arises when getting user role from token to do role validation in some endpoints.

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

#region swagger
//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(option =>
{
  option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
  option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
  {
    In = ParameterLocation.Header,
    Description = "Please enter a valid token",
    Name = "Authorization",
    Type = SecuritySchemeType.Http,
    BearerFormat = "JWT",
    Scheme = "Bearer"
  });
  option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});
#endregion

#region jwt data
var validIssuer = builder.Configuration["JWT:ValidIssuer"];
var validAudience = builder.Configuration["JWT:ValidAudience"];
var issuerSigningKey = builder.Configuration["JWT:IssuerSigningKey"];
#endregion

if (validIssuer != null && validAudience != null && issuerSigningKey != null)
{
  #region jwt
  builder.Services
  .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  //.AddAuthentication(options =>
  //{
  //    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
  //    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  //    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  //})
  .AddJwtBearer(options =>
  {
    options.TokenValidationParameters = new TokenValidationParameters()
    {
      ClockSkew = TimeSpan.FromDays(1),
      ValidateIssuer = true,
      ValidateAudience = true,
      ValidateLifetime = true,
      ValidateIssuerSigningKey = true,
      ValidIssuer = validIssuer,
      ValidAudience = validAudience,
      IssuerSigningKey = new SymmetricSecurityKey(
              Encoding.UTF8.GetBytes(issuerSigningKey)
          ),
    };
  });
  #endregion

  #region DbContext
  var connectionString = Util.getAppSettingsValue("ConnectionStrings:SurveyConnection");
  if (connectionString == null)
  {
    throw new Exception("SQL connection string is not set in appsettings.json!");
  }
  builder.Services.AddDbContext<UsersDbContext>(options =>
  {
    options.UseSqlServer(connectionString);
  });
  #endregion

  #region neo4j data
  var neo4jServer = builder.Configuration["Neo4J:server"];
  var neo4jUser = builder.Configuration["Neo4J:user"];
  var neo4jPwd = builder.Configuration["Neo4J:pwd"];
  #endregion

  if (neo4jServer != null && neo4jUser != null && neo4jPwd != null)
  {
    #region neo4j
    var client = new BoltGraphClient(new Uri(neo4jServer), neo4jUser, neo4jPwd);
    client.ConnectAsync();
    #endregion

    #region services

    #region repository
    builder.Services.AddSingleton<ISurveysRepository, SurveysRepository>();
    builder.Services.AddSingleton<IBlocksRepository, BlocksRepository>();
    builder.Services.AddSingleton<IQuestionsRepository, QuestionsRepository>();
    builder.Services.AddSingleton<IOptionsRepository, OptionsRepository>();
    builder.Services.AddSingleton<IAnswersRepository, AnswersRepository>();
    #endregion

    #region domain
    builder.Services.AddSingleton<ISurveysApplication, SurveysApplication>();
    builder.Services.AddSingleton<IBlocksApplication, BlocksApplication>();
    builder.Services.AddSingleton<IQuestionsApplication, QuestionsApplication>();
    builder.Services.AddSingleton<IOptionsApplication, OptionsApplication>();
    builder.Services.AddSingleton<IAnswersApplication, AnswersApplication>();
    #endregion

    builder.Services.AddSingleton<AuthService>();

    builder.Services.AddSingleton<IGraphClient>(client);

    builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
    builder.Services.AddScoped<TokenService, TokenService>();
    builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
    #endregion

    #region users
    builder.Services
            .AddIdentityCore<ApplicationUser>(options =>
            {
              options.SignIn.RequireConfirmedAccount = false;
              options.User.RequireUniqueEmail = true;
              options.Password.RequireDigit = false;
              options.Password.RequiredLength = 6;
              options.Password.RequireNonAlphanumeric = false;
              options.Password.RequireUppercase = false;
              options.Password.RequireLowercase = false;
            }).AddRoles<ApplicationRole>().AddEntityFrameworkStores<UsersDbContext>();
    #endregion

    var app = builder.Build();

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

    #region swagger default page in release mode
    if (!app.Environment.IsDevelopment())
    {
      app.UseSwaggerUI(options =>
      {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "Surveys Web API");
        options.RoutePrefix = string.Empty;
      });
      app.UseHsts();
    }
    #endregion

    app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();

    app.Run();
  }
  else
  {
    throw new Exception("Neo4J is not configured!");
  }
}
else
{
  throw new Exception("JWT is not configured!");
}

CreateUser:

[HttpPost]
[Route("users")]
public async Task<IActionResult> CreateUserAsync(UserRequest user)
{
  if (!ModelState.IsValid)
  {
    return BadRequest(ModelState);
  }

  //TODO: Validate required fields

  var identityUser = new ApplicationUser
  {
    UserName = user.Username,
    Email = user.Email,
    FirstName = user.FirstName,
    LastName = user.LastName
  };
  var result = await _userManager.CreateAsync(
      identityUser,
      user.Passwd
  );
  if (result.Succeeded)
  {
    string[] roles = { "Administrator" };
    await AddRolesAsync(identityUser, roles);
    return Created($"{Request.GetHost()}/ftq/user/{identityUser.Id}", null);
  }
  foreach (var error in result.Errors)
  {
    ModelState.AddModelError(error.Code, error.Description);
  }
  return BadRequest(ModelState);
}

AddRoles:

private async Task<IdentityResult> AddRolesAsync(ApplicationUser user, string[] rolesToAdd)
{
  //actualizo el rol
  var roles = await _userManager.GetRolesAsync(user);
  var removeRoleResult = await _userManager.RemoveFromRolesAsync(user, roles);
  if (!removeRoleResult.Succeeded)
    return removeRoleResult;

  //agrego los nuevos roles
  var addedRol = new IdentityResult();
  foreach (var rol in rolesToAdd)
  {
    addedRol = await _userManager.AddToRoleAsync(user, rol);
    if (!addedRol.Succeeded)
      return removeRoleResult;
  }
  return addedRol;
}

ApplicationRole:

public class ApplicationRole : IdentityRole<Guid>
{
  public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

ApplicationUser:

public class ApplicationUser : IdentityUser<Guid>
{
  [Required]
  [MaxLength(256)]
  [Display(Name = "First Name")]
  public string? FirstName { get; set; }

  [Required]
  [MaxLength(256)]
  [Display(Name = "Last Name")]
  public string? LastName { get; set; }

  [Required]
  [Display(Name = "Status")]
  public bool Status { get; set; } = true;

  //propiedad con relacion a tabla de roles de usuarios
  public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

ApplicationUserRole:

public class ApplicationUserRole : IdentityUserRole<Guid>
{
  //propiedad con relacion a tabla de usuarios
  public virtual ApplicationUser User { get; set; }

  //propiedad con relacion a tabla de roles
  public virtual ApplicationRole Role { get; set; }
}

UsersDbContext:

public class UsersDbContext : IdentityDbContext<ApplicationUser
                                    , ApplicationRole
                                    , Guid
                                    , IdentityUserClaim<Guid>
                                    , ApplicationUserRole
                                    , IdentityUserLogin<Guid>
                                    , IdentityRoleClaim<Guid>
                                    , IdentityUserToken<Guid>>
{
  public UsersDbContext(DbContextOptions<UsersDbContext> context) : base(context)
  {
  }

  public DbSet<ApplicationUser> AppUsers { get; set; }
  public DbSet<ApplicationRole> AppRoles { get; set; }
  public DbSet<ApplicationUserRole> AppUserRoles { get; set; }

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

    builder.Entity<ApplicationUser>(entity =>
    {
      entity.ToTable(name: "User");
      //configuro relacion entre usuario y rol
      entity.HasMany(u => u.UserRoles)
            .WithOne()
            .HasForeignKey(ur => ur.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    });
    builder.Entity<ApplicationRole>(entity =>
    {
      entity.ToTable(name: "Role");
    });
    builder.Entity<ApplicationUserRole>(entity =>
    {
      entity.ToTable("UserRoles");
      entity.HasKey(ur => new { ur.UserId, ur.RoleId });

      entity.HasOne(ur => ur.Role)
          .WithMany(r => r.UserRoles)
          .HasForeignKey(ur => ur.RoleId)
          .IsRequired();

      entity.HasOne(ur => ur.User)
          .WithMany(r => r.UserRoles)
          .HasForeignKey(ur => ur.UserId)
          .IsRequired();
    });
    builder.Entity<IdentityUserClaim<Guid>>(entity =>
    {
      entity.ToTable("UserClaims")
            .HasKey(x => x.Id);
    });
    builder.Entity<IdentityUserLogin<Guid>>(entity =>
    {
      entity.ToTable("UserLogins")
            .HasKey(x => x.UserId);
    });
    builder.Entity<IdentityRoleClaim<Guid>>(entity =>
    {
      entity.ToTable("RoleClaims");
    });
    builder.Entity<IdentityUserToken<Guid>>(entity =>
    {
      entity.ToTable("UserTokens")
            .HasKey(x => x.UserId);
    });
  }
}

GetCurrentUser:

public User? GetCurrentUser(HttpContext httpContext)
{
  var identity = httpContext.User.Identity as ClaimsIdentity;
  if (identity != null)
  {
    var userClaims = identity.Claims;
    if (userClaims != null)
    {
      var username = userClaims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value;
      var email = userClaims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
      var role = userClaims.FirstOrDefault(x => x.Type == ClaimTypes.Role)?.Value;

      if (username != null && email != null && role != null) {
        return new User
        {
          Username = username,
          Email = email,
          Role = role
        };
      }
      return null;
    }
    return null;
  }
  return null;
}

Here in GetCurrentUser role is always null.


Solution

  • I use the template to create the application and it uses the AddDefaultIdentity function to add the identity role. Here is the code of the Model:

    namespace WebApplication1.Models
    {
        public class ApplicationUser : IdentityUser
        {
        }
    
     
    
        public class ApplicationRole : IdentityRole
        {
        }
    
     
    
        public class ApplicationUserRole : IdentityUserRole<Guid>
        {
        }
    }
    

    Here is the code of DbContext:

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
        {
            public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
                : base(options)
            {
            }
        }
    

    I directly operated the database table and added the role of user, you can see from the screenshot: enter image description here

    And I can see in the your given code, you are using the AddIdentityCore function. But if I tried to use your code, there was an error message:

    InvalidOperationException: No service for type 'Microsoft.AspNetCore.Identity.SignInManager`1[WebApplication1.Models.ApplicationUser]' has been registered.

    So I checked some information and here is my changed codes:

    builder.Services.AddIdentityCore<ApplicationUser>();
    builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddRoles<ApplicationRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();
    

    Besides that, I change the code in _LoginPartial.cshtml to

    @inject SignInManager<ApplicationUser> SignInManager
    @inject UserManager<ApplicationUser> UserManager
    

    I find out that if I use AddIdentityCore and AddDefaultIdentity at the same time, the error message will not show up and here is my test result which you can get the current user's role via httpcontext: enter image description here