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.
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:
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: