I have setup cookie authentication in my asp.net core application. I have a login page where the credentials matchup against the active directory. All this works fine.
Now next I want to implement authorization in my application. I have a table of users together with permission against them. For example permission like Reading & Write. When the user is successfully authenticated I want to check for these permissions and show them certain functionality while restricting others. For example, show certain dropdowns for write permission while hiding for reading permission.
What is the best approach to handle this in the .NET Core.
I have read about adding policy like:
services.AddAuthorization(options => {
options.AddPolicy("Read", policy =>
policy.RequireClaim("Read", "MyCLaim"));
});
Then in my controller:
[Authorize(Policy = "Read")]
public class HomeController : Controller
{
}
Where do I get the permissions for logged in user from my database and how to verify if the user has those permissions or not.
Would appreciate inputs.
Where do I get the permissions for logged in user from my database and
how to verify if the user has those permissons or not.
Right after a user is authenticated, you collect user's claims, and store them in Authentication Cookie.
For example, SignInAsync method.
public async Task SignInAsync(User user, IList<string> roleNames)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName)
};
foreach (string roleName in roleNames)
{
claims.Add(new Claim(ClaimTypes.Role, roleName));
}
var identity = new ClaimsIdentity(claims, "local", "name", "role");
var principal = new ClaimsPrincipal(identity);
await _httpContextAccessor.HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme, principal);
}
FYI: It happens to be that I store them as Role claims. You do not have to follow that route, if you don't want.
You can then verify the policy inside Startup.cs.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
// Set up policies from claims
// https://leastprivilege.com/2016/08/21/why-does-my-authorize-attribute-not-work/
services.AddAuthorization(options =>
{
options.AddPolicy(Constants.RoleNames.Administrator, policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser()
.RequireAssertion(context => context.User.HasClaim(
ClaimTypes.Role, Constants.RoleNames.Administrator))
.Build();
});
});
...
}
}
Usage is same as what you have described.
[Authorize(Policy = "Read")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
Related
So I'm using the ASP.NET Core React Template with built-in authorization. In that template, everything is working and I'm able to login and register an account via this
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
When I view the token via application localstorage, I get the following data. Without the role.
I also viewed the access token via jwt.io
My question is, how can I add the role there or the role in the jwt token?
Thank you!
You need to create a ProfileService which implements IProfileService interface
I share you code from my project
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> UserManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
UserManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
ApplicationUser user = await UserManager.GetUserAsync(context.Subject);
IList<string> roles = await UserManager.GetRolesAsync(user);
var claims = new List<Claim> {
// here you can include other properties such as id, email, address, etc. as part of the jwt claim types
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.Name, $"{user.Firstname} {user.Lastname}")
};
foreach (string role in roles)
{
// include the roles
claims.Add(new Claim(JwtClaimTypes.Role, role));
}
context.IssuedClaims.AddRange(claims);
}
public Task IsActiveAsync(IsActiveContext context)
{
return Task.CompletedTask;
}
}
Add DI registration to Startup
services.AddTransient<IProfileService, ProfileService>();
Details in IdentityServer4 documentation
I have an ASP.NET Core application where I want to add role-based authentication. I'm using Windows Authentication because it's an intranet app. I already have a custom database that contains the users/roles that frankly doesn't map to the fields in the IdentityFramework. I can easily get the logged-in user's name via the Context.User.Identity.Name. I then want to look up the user in the custom user/roles table in order to get the available roles for that user. Then I want to use an annotation-based authentication filter decorated at the Controller or Action method level. For example, [Authorize(roles="admin")].
I was able to get this working by turning off Windows Authentication and using Forms Authentication with Cookies. In the AccountController I ran code like this:
using(LDAPConnection connection = new LDAPConnection(loginModel.UserName,loginModel.Password))
{
List<Claim> claims = new List<Claim> {
new Claim(ClaimTypes.Name, loginModel.UserName),
new Claim(ClaimTypes.Role, "admin")
};
ClaimsIdentity userIdentity = new ClaimsIdentity(claims,"login");
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(principal),
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.Now.AddDays(200)
});
return Redirect("/");
}
I would then store the claims in a cookie. Then when I decorate the Controller with [Authorize(roles="admin")], I'm able to retrieve the View without issues. The authorization works. I would like to replicate this same functionality for WindowsAuthentication without logging the user in. I have tried using a ClaimsTransformer and implementing Policy-based authorization, which works. But if I decorate it with [Authorize(roles="admin")] it bombs when I navigate to the action method. Here is the ClaimsTransformer:
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = (ClaimsIdentity)principal.Identity;
List<Claim> claims = new List<Claim> {
new Claim(ClaimTypes.Name, identity.Name),
new Claim(ClaimTypes.Role, "admin")
};
identity.AddClaims(claims);
return Task.FromResult(principal);
}
What piece am I missing in order to use the [Authorize(Roles="admin")] working? BTW, I'm currently using ASP.NET Core 2.2.
You could write a custom Policy Authorization handlers in which you get all User's Roles and check if they contains your desired role name.
Refer to following steps:
1.Create CheckUserRoleRequirement(accept a parameter)
public class CheckUserRoleRequirement: IAuthorizationRequirement
{
public string RoleName { get; private set; }
public CheckUserRoleRequirement(string roleName)
{
RoleName = roleName;
}
}
2.Create CheckUserRoleHandler
public class CheckUserRoleHandler : AuthorizationHandler<CheckUserRoleRequirement>
{
private readonly IServiceProvider _serviceProvider;
public CheckUserRoleHandler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
CheckUserRoleRequirement requirement)
{
var name = context.User.Identity.Name;
using (var scope = _serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<YourDbContext>();
//your logic to look up the user in the custom user/roles table in order to get the available roles for that user
List<string> roles = dbContext.UserRoles.Where(...;
if (roles != null && roles.Contains(requirement.RoleName))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
3.Register Handler in ConfigureServices
services.AddAuthorization(options =>
{
options.AddPolicy("AdminRole", policy =>
policy.Requirements.Add(new CheckUserRoleRequirement("Admin")));
});
services.AddSingleton<IAuthorizationHandler, CheckUserRoleHandler>();
4.Usage
[Authorize(Policy = "AdminRole")]
I know this is a bit of a late answer, but I've been troubleshooting the same issue today and none of the answers I've seen on similar posts have fixed my issue.
Here are the steps I took to be able to use [Authorize(Roles = "Admin")] on my controller with Windows authentication.
Double check that UseAuthentication() comes before UseAuthorization() in the Configure() method of Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // <--- this needs to be before
app.UseAuthorization(); // <----this
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Hccc}/{action=Index}/");
});
}
Have a claims transformer to handle the necessary roles. For example,
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var ci = (ClaimsIdentity)principal.Identity;
var user = UserAuth.GetUserRole(ci.Name); // gets my user from db with role
// handle your roles however you need.
foreach(var role in user.Roles)
{
var roleClaim = new Claim(ci.RoleClaimType, role.RoleName);
ci.AddClaim(roleClaim);
}
return Task.FromResult(principal);
}
Set up the ConfigureServices() method in Startup.cs to handle authorization
services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();
// Implement a policy called "AdminOnly" that uses "Windows" authentication
// The policy requires Role "Admin"
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
{
policy.AddAuthenticationSchemes("Windows");
policy.RequireRole("Admin");
});
});
services.AddMvc();
services.AddControllersWithViews();
Use the [Authorize] tag to implement the policy. For my case, I wanted to block access to a controller unless the user was an "Admin".
[Authorize(Policy = "AdminOnly")]
public class UsersController : Controller
{
}
I have a SPA that has an ASP.NET Core web API together with the inbuilt identity server switched on using AddIdentityServer and then AddIdentityServerJwt:
services.AddIdentityServer()
.AddApiAuthorization<User, UserDataContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
I also have an authorization policy setup that requires an "Admin" role claim:
services.AddAuthorization(options =>
{
options.AddPolicy("IsAdmin", policy => policy.RequireClaim(ClaimTypes.Role, "Admin"));
});
I have a controller action that uses this policy
[Authorize(Policy = "IsAdmin")]
[HttpDelete("{id}")]
public IActionResult Deleten(int id)
{
...
}
The authenticated user does have the "Admin" role claim:
The access token for this authentication user doesn't appear to contain the admin claim:
I get a 403 back when trying to request this resource with the admin user:
So, if I'm understanding this correctly, IdentityServer isn't including the admin role claim and so the user isn't authorized to access the resource.
Is it possible to configure the claims that IdentityServer uses using AddIdentityServerJwt? or am I misunderstanding why this is not working.
One of the other answers is really close to the specific use case in question but misses the point about it being SPA.
Firstly you must add your IProfileService implementation like suggested already:
public class MyProfileService : IProfileService
{
public MyProfileService()
{ }
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//get role claims from ClaimsPrincipal
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
//add your role claims
context.IssuedClaims.AddRange(roleClaims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
// await base.IsActiveAsync(context);
return Task.CompletedTask;
}
}
But then go ahead and do this:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddProfileService<MyProfileService>();
And your claim will be exposed on the JWT. Replace the ClaimTypes.Role constant with any string corresponding to the claim type you want to expose.
On Identity Server side , you can create Profile Service to make IDS4 include role claim when issuing tokens .
You can get role claims from ClaimsPrincipal or get the roles from database and create profile service like :
public class MyProfileService : IProfileService
{
public MyProfileService()
{ }
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//get role claims from ClaimsPrincipal
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
//add your role claims
context.IssuedClaims.AddRange(roleClaims);
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
// await base.IsActiveAsync(context);
return Task.CompletedTask;
}
}
And register in Startup.cs:
services.AddTransient<IProfileService, MyProfileService>();
On client side , you should map the role claim from your JWT Token and try below config in AddOpenIdConnect middleware :
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters.RoleClaimType = "role";
Then your api could validate the access token and authorize with role policy .
I did this without using roles but with using a special claim added to the users token. I have created a CustomUserClaimsPrincipalFactory this allows me to add additional claims to the user.
register
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsPrincipalFactory>();
the code.
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole<long>>
{
public CustomUserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole<long>> roleManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity("Identity.Application",
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, user.Name));
id.AddClaim(new Claim("preferred_username", userName));
id.AddClaim(new Claim("culture", user.Culture ?? "da-DK"));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
if(user.IsXenaSupporter)
id.AddClaim(new Claim("supporter", user.Id.ToString()));
return id;
}
}
policy
services.AddAuthorization(options =>
{
options.AddPolicy("Supporter", policy => policy.RequireClaim("supporter"));
});
usage
[Authorize(AuthenticationSchemes = "Bearer", Policy = "Supporter")]
[HttpPost("supporter")]
public async Task<ActionResult> ChangeToSpecificUser([FromBody] ChangeUserRequest request)
{
// ..................
}
The question: I call RoleManager.CreateAsync() and RoleManager.AddClaimAsync() to create roles and associated role claims. Then I call UserManager.AddToRoleAsync() to add users to those roles. But when the user logs in, neither the roles nor the associated claims show up in the ClaimsPrincipal (i.e. the Controller's User object). The upshot of this is that User.IsInRole() always returns false, and the collection of Claims returned by User.Claims doesn't contain the role claims, and the [Authorize(policy: xxx)] annotations don't work.
I should also add that one solution is to revert from using the new services.AddDefaultIdentity() (which is provided by the templated code) back to calling services.AddIdentity().AddSomething().AddSomethingElse(). I don't want to go there, because I've seen too many conflicting stories online about what I need to do to configure AddIdentity for various use cases. AddDefaultIdentity seems to do most things correctly without a lot of added fluent configuration.
BTW, I'm asking this question with the intention of answering it... unless someone else gives me a better answer than the one I'm prepared to post. I'm also asking this question because after several weeks of searching I have yet to find a good end-to-end example of creating and using Roles and Claims in ASP.NET Core Identity 2. Hopefully, the code example in this question might help someone else who stumbles upon it...
The setup:
I created a new ASP.NET Core Web Application, select Web Application (Model-View-Controller), and change the Authentication to Individual User Accounts. In the resultant project, I do the following:
In Package Manager Console, update the database to match the scaffolded migration:
update-database
Add an ApplicationUser class that extends IdentityUser. This involves adding the class, adding a line of code to the ApplicationDbContext and replacing every instance of <IdentityUser> with <ApplicationUser> everywhere in the project.
The new ApplicationUser class:
public class ApplicationUser : IdentityUser
{
public string FullName { get; set; }
}
The updated ApplicationDbContext class:
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{ }
// Add this line of code
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
}
In Package Manager Console, create a new migration and update the database to incorporate the ApplicationUsers entity.
add-migration m_001
update-database
Add the following line of code in Startup.cs to enable RoleManager
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
Add some code to seed roles, claims, and users. The basic concept for this sample code is that I have two claims: can_report allows the holder to create reports, and can_test allows the holder to run tests. I have two Roles, Admin and Tester. The Tester role can run tests, but can't create reports. The Admin role can do both. So, I add the claims to the roles, and create one Admin test user and one Tester test user.
First, I add a class whose sole purpose in life is to contain constants used elsewhere in this example:
// Contains constant strings used throughout this example
public class MyApp
{
// Claims
public const string CanTestClaim = "can_test";
public const string CanReportClaim = "can_report";
// Role names
public const string AdminRole = "admin";
public const string TesterRole = "tester";
// Authorization policy names
public const string CanTestPolicy = "can_test";
public const string CanReportPolicy = "can_report";
}
Next, I seed my roles, claims, and users. I put this code in the main landing page controller just for expedience; it really belongs in the "startup" Configure method, but that's an extra half-dozen lines of code...
public class HomeController : Controller
{
const string Password = "QwertyA1?";
const string AdminEmail = "admin#example.com";
const string TesterEmail = "tester#example.com";
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
// Constructor (DI claptrap)
public HomeController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task<IActionResult> Index()
{
// Initialize roles
if (!await _roleManager.RoleExistsAsync(MyApp.AdminRole)) {
var role = new IdentityRole(MyApp.AdminRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanReportClaim, ""));
}
if (!await _roleManager.RoleExistsAsync(MyApp.TesterRole)) {
var role = new IdentityRole(MyApp.TesterRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
}
// Initialize users
var qry = _userManager.Users;
IdentityResult result;
if (await qry.Where(x => x.UserName == AdminEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = AdminEmail,
Email = AdminEmail,
FullName = "Administrator"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.AdminRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
if (await qry.Where(x => x.UserName == TesterEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = TesterEmail,
Email = TesterEmail,
FullName = "Tester"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.TesterRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
// Roles and Claims are in a cookie. Don't expect to see them in
// the same request that creates them (i.e., the request that
// executes the above code to create them). You need to refresh
// the page to create a round-trip that includes the cookie.
var admin = User.IsInRole(MyApp.AdminRole);
var claims = User.Claims.ToList();
return View();
}
[Authorize(policy: MyApp.CanTestPolicy)]
public IActionResult Test()
{
return View();
}
[Authorize(policy: MyApp.CanReportPolicy)]
public IActionResult Report()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
and I register my authentication policies in the "Startup" ConfigureServices routine, just after the call to services.AddMvc
// Register authorization policies
services.AddAuthorization(options => {
options.AddPolicy(MyApp.CanTestPolicy, policy => policy.RequireClaim(MyApp.CanTestClaim));
options.AddPolicy(MyApp.CanReportPolicy, policy => policy.RequireClaim(MyApp.CanReportClaim));
});
Whew. Now, (assuming I've noted all of the applicable code I've added to the project, above), when I run the app, I notice that neither of my "built-in" test users can access either the /home/Test or /home/Report page. Moreover, if I set a breakpoint in the Index method, I see that my roles and claims do not exist in the User object. But I can look at the database and see all of the roles and claims are there.
So, to recap, the question asks why the code provided by the ASP.NET Core Web Application template doesn't load roles or role claims into the cookie when a user logs in.
After much Googling and experimenting, there appear to be two modifications that must be made to the templated code in order to get Roles and Role Claims to work:
First, you must add the following line of code in Startup.cs to enable RoleManager. (This bit of magic was mentioned in the OP.)
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
But wait, there's more! According to this discussion on GitHub, getting the roles and claims to show up in the cookie involves either reverting to the service.AddIdentity initialization code, or sticking with service.AddDefaultIdentity and adding this line of code to ConfigureServices:
// Add Role claims to the User object
// See: https://github.com/aspnet/Identity/issues/1813#issuecomment-420066501
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
If you read the discussion referenced above, you'll see that Roles and Role Claims are apparently kind-of-deprecated, or at least not eagerly supported. Personally, I find it really useful to assign claims to roles, assign roles to users, and then make authorization decisions based on the claims (which are granted to the users based on their roles). This gives me an easy, declarative way to allow, for example, one function to be accessed by multiple roles (i.e. all of the roles that contain the claim used to enable that function).
But you DO want to pay attention to the amount of role and claim data being carried in the auth cookie. More data means more bytes sent to the server with each request, and I have no clue what happens when you bump up against some sort of limit to the cookie size.
Ahh, there are some changes from ASP.NET Core version 2.0 to 2.1. AddDefaultIdentity is the one.
I don't know where to start from your code, so, I will provide an example to create and get user role(s).
Let's create UserRoles first:
public enum UserRoles
{
[Display(Name = "Quản trị viên")]
Administrator = 0,
[Display(Name = "Kiểm soát viên")]
Moderator = 1,
[Display(Name = "Thành viên")]
Member = 2
}
Note: You can remove the attribute Display.
Then, we create RolesExtensions class:
public static class RolesExtensions
{
public static async Task InitializeAsync(RoleManager<IdentityRole> roleManager)
{
foreach (string roleName in Enum.GetNames(typeof(UserRoles)))
{
if (!await roleManager.RoleExistsAsync(roleName))
{
await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
}
}
Next, in the Startup.cs class, we run it:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
RoleManager<IdentityRole> roleManager)
{
// other settings...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
var task = RolesExtensions.InitializeAsync(roleManager);
task.Wait();
}
Note: Configure requires a returned type void, so we need to create a task to initialize the user roles and we call Wait method.
Do not change the returned type like this:
public async void Configure(...)
{
await RolesExtensions.InitializeAsync(roleManager);
}
Source: Async/Await - Best Practices in Asynchronous Programming
In the ConfigureServices method, these configurations would NOT work (we cannot use User.IsInRole correctly):
services.AddDefaultIdentity<ApplicationUser>()
//.AddRoles<IdentityRole>()
//.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
I don't know why but AddRoles and AddRoleManager don't support to check role for a user (User.IsInRole).
In this case, we need to register service like this:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
By using this way, we create 3 user roles in the databse:
When register new user, we just need to call:
await _userManager.AddToRoleAsync(user, nameof(UserRoles.Administrator));
Finally, we can use [Authorize(Roles = "Administrator")] and:
if (User.IsInRole("Administrator"))
{
// authorized
}
// or
if (User.IsInRole(nameof(UserRoles.Administrator)))
{
// authorized
}
// but
if (User.IsInRole("ADMINISTRATOR"))
{
// authorized
}
P/S: There are a lot things which need to be implement to achieve this goal. So maybe I missed something in this example.
Also you can try to fix Authentication like this
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
If I use “Roles” instead of ClaimTypes.Role in .net6 blazor wasm , #attribute [Authorize(Roles = "admin")] not work and get this error in browser console :
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (admin)”
By using of ClaimTypes.Role the problem resolved :
private async Task<List<Claim>> GetClaimsAsync(User user)
{
var claims = new List<Claim>()
{
new Claim("UserName", user.Email),
new Claim("FullName", user.FirstName+" "+user.LastName),
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
claims.Add(new Claim(ClaimTypes.Role, role)); // this line
return claims;
}
https://github.com/mammadkoma/Attendance/blob/master/Attendance/Server/Controllers/AccountsController.cs
I'm trying to create a fairly simple intranet application that will use Active Directory for authentication, and will use the AspNetRoles table to check if the user is in a certain application role. This app is just an in-house lottery where some users can create events/contests that other users can then submit an entry to the contest. I'm thinking of starting out with 2 basic roles:
Administrator - Can perform CRUD operations on "Event" or
"Contest" entities
Contestant - Can perform GET operations on
"Contest" entities, and can create new "Entry" entities.
Here's where I'm stuck: I've got Windows Authentication working in the sense that from a controller, I can do a User.Identity.Name and see my domain login name. Furthermore, I can verify that an account belongs to a domain group by doing User.IsInRole("Domain Users"). If I want to avoid creating new AD groups for each role in my application (let's say design changes down the road require additional roles), how can I use Authorization on controllers to check against Application Roles?
Here's an example controller I want to use:
[Route("api/[controller]")]
[Authorize(Roles = "Contestant")]
public class EventTypesController : Controller
{
private IRaffleRepository _repository;
private ILogger<EventTypesController> _logger;
public EventTypesController(IRaffleRepository repository, ILogger<EventTypesController> logger)
{
_repository = repository;
_logger = logger;
}
[HttpGet("")]
public IActionResult Get()
{
try
{
var results = _repository.GetAllEventTypes();
return Ok(Mapper.Map<IEnumerable<EventTypeViewModel>>(results));
}
catch (Exception ex)
{
_logger.LogError($"Failed to get all event types: {ex}");
return BadRequest("Error occurred");
}
}
}
In my Startup.cs, in ConfigureServices, I'm wiring up Identity as follows:
services.AddIdentity<RaffleUser, ApplicationRole>()
.AddEntityFrameworkStores<RaffleContext>();
My RaffleUser class is really just the default implementation of IdentityUser:
public class RaffleUser : IdentityUser
{
}
My ApplicationRole class is also just the default implementation of IdentityRole. I also tried seeding some data in a seed class:
if (!await _roleManager.RoleExistsAsync("Administrator"))
{
var adminRole = new ApplicationRole()
{
Name = "Administrator"
};
await _roleManager.CreateAsync(adminRole);
await _context.SaveChangesAsync();
}
if (await _userManager.FindByNameAsync("jmoor") == null)
{
using (var context = new PrincipalContext(ContextType.Domain))
{
var principal = UserPrincipal.FindByIdentity(context, "DOMAIN\\jmoor");
if (principal != null)
{
var user = new RaffleUser()
{
Email = principal.EmailAddress,
UserName = principal.SamAccountName
};
await _userManager.CreateAsync(user);
await _context.SaveChangesAsync();
var adminRole = await _roleManager.FindByNameAsync("Administrator");
if (adminRole != null)
{
await _userManager.AddToRoleAsync(user, adminRole.Name);
await _context.SaveChangesAsync();
}
}
}
}
The data makes it to the tables, but it just seems like at the controller level, I need to convert the authenticated user to an IdentityUser. Do I need some middleware class to do this for me? Would that be the best way to make authorization reusable on all controllers?
First, I ended up creating a custom ClaimsTransformer that returns a ClaimsPrincipal populated with UserClaims and RoleClaims (after refactoring my app, I decided to go with policy-based authorization, and the access claim can be added at either the role or user level):
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var identity = (ClaimsIdentity)context.Principal.Identity;
var userName = identity.Name;
if (userName != null)
{
var user = await _userManager.FindByLoginAsync("ActiveDirectory", userName);
if (user != null)
{
identity.AddClaims(await _userManager.GetClaimsAsync(user));
var roles = await _userManager.GetRolesAsync(user);
identity.AddClaims(await GetRoleClaims(roles));
}
}
return context.Principal;
}
private async Task<List<Claim>> GetRoleClaims(IList<string> roles)
{
List<Claim> allRoleClaims = new List<Claim>();
foreach (var role in roles)
{
var rmRole = await _roleManager.FindByNameAsync(role);
var claimsToAdd = await _roleManager.GetClaimsAsync(rmRole);
allRoleClaims.AddRange(claimsToAdd);
}
return allRoleClaims;
}
I wired that up in the Startup.cs:
services.AddScoped<IClaimsTransformer, Services.ClaimsTransformer>();
I also went with Policy-based authorization:
services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", policy => policy.RequireClaim("AccessLevel", "Administrator"));
options.AddPolicy("Project Manager", policy => policy.RequireClaim("AccessLevel", "Project Manager"));
});
So, users or roles can have a claim set with a name of "AccessLevel" and a value specified. To finish everything off, I also created a custom UserManager that just populates the User object with additional details from ActiveDirectory during a CreateAsync.
You need to add a DefaultChallangeScheme to use Windows authentication. This is how i do, but if someone has a better solution i am all ears :)
I use the following setup in my current application.
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<SecurityDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
});
Then i put in my application claims in a transformer.
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
I hope this will get you in the right direction.