Role based authorization not working in .net 5 - asp.net-core

Problem Statement : I had this issue, role base Authorization ([Authorize(Roles = "Admin")]) wasn't working as expected for me in asp .net 5, role claims where not added to identity.
I googled and tried many solutions and finally I was able to figure out. So I am sharing few scenario so that it might help someone.

If you have a custom User that inherits from IdentityUser
For example:
public class ApplicationUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
And you have your services configured properly, like so:
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>() /* Added role manager*/
.AddDefaultUI()
.AddEntityFrameworkStores<ApplicationDbContext>();
That should work fine if you are not using a custom claim principal for UserPrincipal.
For example:
public class AppUserClaimsIdentityFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
public AppUserClaimsIdentityFactory(
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor)
{
}
/* code implementation */
}
and injected it using DI like below:
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, AppUserClaimsIdentityFactory>();
In that case it doesn't work, you'll need to use a different constructor overload for UserClaimsPrincipalFactory and add role manager to it.
Update your custom UserClaimsIdentityFactory:
public class AppUserClaimsIdentityFactory :
UserClaimsPrincipalFactory<ApplicationUser,
IdentityRole>
/* Note: you can use your custom Role class or identity default */
{
public AppUserClaimsIdentityFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager, /* Add role manager */
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager /* send it to base constructor */, optionsAccessor)
{
}
/* code implementation */
}
You don't have to change anything in Dependency Injection container for your custom UserClaimsPrincipalFactory.
Everything thing should work fine as expected.

Related

When dealing with Razor Pages and a Page Model class is the constructor called on each request?

Something I have struggled with is understanding the lifecycle of the Page Model class for my Razor Pages usages. I'm trying to think about how and when to deal with common data I pass to my business logic like the userId that is running the request. So many times I need to save this information with the results of the action.
So where is the best place to handle something over and over like geting User details that might be in the persistence model and not in the context of the page model's User from the HTTPContext?
I should mention I am using authorize tags with cookie backed authenication to a webservice.
For accessing Reuqest from other layers except the Razor Page, you could try IHttpContextAccessor.
For general way to handling user details from request, you could create a service like below:
public interface IUserService
{
IdentityUser GetUser();
}
public class UserService:IUserService
{
private readonly ApplicationDbContext _context;
private readonly HttpContext _httpContext;
public UserService(ApplicationDbContext context
, IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContext = httpContextAccessor.HttpContext;
}
public IdentityUser GetUser()
{
StringValues userId = "";
if (_httpContext.Request.Headers.TryGetValue("userId", out userId))
{
var user = _context.Users.FirstOrDefault(u => u.Id == userId);
return user;
}
else
{
return null;
}
}
}
And then register like
services.AddScoped<IUserService, UserService>();
Then, you could resolve IUserService from DI to use them when you want to access user info.
public class IndexModel : PageModel
{
private readonly IUserService _userService;
public IndexModel(IUserService userService)
{
_userService = userService;
}
public void OnGet()
{
var user = _userService.GetUser();
}
}

How to add new colum into Identity RoleClaims table (asp net core)

I'm trying to add a column to the identity (asp net core) RoleClaims table but I find content just to extend the roles and users classes and not to RoleClaims.
Could someone help with examples or point out content.
You would need to create a new class to extend the RoleClaim. Here is an example of how to do it if your key type is string:
public class ApplicationRoleClaim : IdentityRoleClaim<string>
{
public virtual ApplicationRole Role { get; set; }
}
You can add whatever new properties you want to this class then create a migration to add them as table columns.
You would also need to tell your IdentityDbContext to use this new class as well. Here is an example from the docs:
public class ApplicationDbContext
: IdentityDbContext<
ApplicationUser, ApplicationRole, string,
ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
ApplicationRoleClaim, ApplicationUserToken>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
EDIT:
With your custom ApplicationRoleClaim class, you could override OnModelCreating as well. This is an example from the docs:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
⋮
modelBuilder.Entity<IdentityRoleClaim<string>>(b =>
{
b.ToTable("MyRoleClaims");
});
⋮
}
Reference: Identity model customization in ASP.NET Core
I made a demo with asp.net core 2.2 and it worked well ,try the following code , customize ApplicationRoleClaim to add other propertyies.
public class ApplicationRoleClaim: IdentityRoleClaim<string>
{
public string Description { get; set; }
}
Then use DbSet<TEntity> class which represents a collection for a given entity within the model and is the gateway to database operations against an entity to add the new column to table
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<ApplicationRoleClaim> ApplicationRoleClaim { get; set; }
}
Finally add-migration and update-database.

CreateAsync doesn't save IdentityUser to database with derived IdentityUser

When Posting the following code works fine until it reaches the _userManager.CreateAsync method. No data is saved to the database.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly ApplicationDbContext _appDbContext;
private readonly UserManager<IdentityUser> _userManager;
public ValuesController(UserManager<IdentityUser> userManager, ApplicationDbContext appDbContext)
{
_userManager = userManager;
_appDbContext = appDbContext;
}
[HttpPost]
public async Task<IActionResult> Post()
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
AppUser user = new AppUser();
user.Email = "email#mail.co.uk";
user.FirstName = "mark";
user.LastName = "Macneill";
user.UserName = "Saints22";
await _userManager.CreateAsync(user, "P4$$word");
return new OkObjectResult("Account created");
}
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
You don't seem to be using the IdentityUser directly but instead a AppUser class. This might be a class extending IdentityUser<> or IdentityUser. I am not sure if you have the rest of your setup right, so here is the process:
If you have created a custom AppUser class, let's say you created it as follows:
class AppUser : IdentityUser<int>
This would mean you have assigned the primary key as in int. However, if you extended IdentityUser instead, note that IdentityUser extends IdentityUser<string>, meaning your key is a string. Going ahead I am going to assume your key is int. If not, you can make the changes accordingly (change all ints to your key type.
In your startup class, you need to be adding the following to register this as your user class used for Identity
services.AddIdentity<AppUser, IdentityRole<int>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Your ApplicationDbContext needs to be defined as follows:
public class ApplicationDbContext : IdentityDbContext<AppUser, IdentityRole<int>, int>
If you don't want to use IdentityRole<int> in the above two pieces of code, you can define a custom role class as follows:
public AppUserRole : IdentityRole<int>
Once you have these setup, you need to inject UserManager<AppUser> not UserManager<IdentityUser>.
Thank you neville-nazerane
I had IdentityUser in dependency injection and all I had to do was change IdentityUser to AppUser.
Startup.cs
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

How to retrieve additional info from a logged in user without accessing DB every time

I'm working with ASP.NET Core 2.0 and Identity Framework. My application has several subscriptions. Today, subscription Id is stored on the ApplicationUser Object and persisted to database context.
Every web call I'm looking at the database in order to get the subscriptionId property of the logged in user.
How can I avoid this database call for the logged in user?
as #Kirk said in the comments you can use claims for this.
You need to add subscriptionId to ClaimsIdentity when user is logging-in and retrieve the value in your controller.
I assume you have a subscriptionId property in ApplicationUser
public class ApplicationUser : IdentityUser
{
public int SubscriptionId { get; set; }
}
For adding custom claims you need to implement IUserClaimsPrincipalFactory
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
public CustomUserClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor)
{
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
ClaimsIdentity claimsIdentity = await base.GenerateClaimsAsync(user);
claimsIdentity.AddClaim(new Claim("subscription_id", user.SubscriptionId));
return claimsIdentity;
}
}
user.SubscriptionId must be a string. in other case you can create a property to cast the value to an string.
public string SubscriptionIdStting => SubscriptionId.ToString();
Register it to IServiceCollection
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory>();
You can access SubscriptionId in Controller or Page like this :
User.FindFirst("subscription_id").Value

AspNet Identity MVC6 Violate the type constraint

I am trying to customize the ASP.NET identity to use integer based keys instead of string. The code compiles but when I run the app, it throws an error for violation of type constraint. Here's how my identity classes look:
public class AppUser: IdentityUser<int, AppUserClaim, AppUserRole, AppUserLogin>
{
public DateTime FirstTrip { get; set; }
}
public class AppRole : IdentityRole<int, AppUserRole, AppRoleClaim>
{
}
public class AppUserClaim : IdentityUserClaim<int>
{
}
public class AppRoleClaim : IdentityRoleClaim<int>
{
}
public class AppUserLogin : IdentityUserLogin<int>
{
}
public class AppUserRole : IdentityUserRole<int>
{
}
public class AppUserToken : IdentityUserToken<int>
{
}
My AppDbContext class:
public class AppDbContext: IdentityDbContext<AppUser,AppRole, int, AppUserClaim, AppUserRole, AppUserLogin, AppRoleClaim, AppUserToken>
{
//other code
}
And here's how I am setting up identity in my startup.cs class:
services.AddIdentity<AppUser, AppRole>(config => //register ASPNET Identity
{
config.User.RequireUniqueEmail = true;
config.Password.RequiredLength = 8;
config.Cookies.ApplicationCookie.LoginPath = "/Auth/Login";
})
.AddEntityFrameworkStores<AppDbContext, int>();
When I run the app, I get following error:
This setup is in line with some suggestions like:
Why does this violate the type constraint?
ASP.NET identity configuration exception and that breaks dotnet.exe run process
What am I missing?
For anyone running into similar problems, here's what I had to do make it work:
1) Implement custom UserStore and RoleStore to account for changed TKey.
2) Register your custom UserStore and RoleStore in the ConfigureServices method in startup.cs. So Instead of the suggested,
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext, int>();
use the following,
services.AddIdentity<AppUser, IdentityRole>()
.AddUserStore<AppUserStore<AppDbContext>>()
.AddRoleStore<AppRoleStore<AppRole, AppDbContext>>();
If you are looking for actual implementation of the custom User and Role store, you can check the following link out(at the very bottom look for PR1):
https://github.com/aspnet/Identity/issues/970