IP based authorization policy with Attributes - asp.net-core

I'm trying to secure a REST API based on the client IP.
Imagine a blog application with these request examples:
/post/list // Everyone should see the posts
/post/create // Only Authors should create a post
/post/update/42 // Only Authors should update a post
/post/delete/42 // Only Admins should delete a post
/comment/42/list // Everyone should see a post's comments
/comment/42/create // Everyone should create a comment
/comment/42/delete/1337 // Only Admins should delete a comment
IP whitelists defined in appsettings.json:
"IpSecurity": {
"Author": "123.456.789.43,123.456.789.44",
"Admin": "123.456.789.42"
}
Here are action examples with the according RequireRole attributes I'd like to implement:
[HttpGet("post/list")]
public List<Post> List()
// ...
[RequireRole("Author")]
[HttpGet("post/create")]
public StandardResponse Create([FromBody]Post post)
// ...
[RequireRole("Admin")]
[HttpGet("post/delete/{id}")]
public StandardResponse Delete(int id)
// ...
Defined injectable from Startup
var IpSecurity = Configuration.GetSection("IpSecurity");
services.Configure<IpSecurityConfig>(IpSecurity);
Does it sound like a good idea ?
Should I do a custom authorization policy, a middleware and/or a filter for that ?
How would I implement the RequireRole attribute ?
This gives an idea of how to implement an IP whitelist but since a middleware does not have access to the contextual action, I can't use attributes to define my requirements.

Yes, that looks good not least because it looks easy understand at a glance.
One comment I would offer is that using the term "Role" for this might confuse your successors. Call it "MachineRole" instead? (And, for the same reason, don't use the [Authorize(Roles="..."])
Implementation in AspNetCore looks to me a little more complex that it was under MVC4, something like this in the usual methods in Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
//after services.AddMvc() :
services.AddAuthorization(o => { o.AddPolicy(MachineRole.AuthorMachine, p => p.RequireClaim(nameof(MachineRole), MachineRole.AuthorMachine)); });
services.AddAuthorization(o => { o.AddPolicy(MachineRole.AdminMachine, p => p.RequireClaim(nameof(MachineRole), MachineRole.AdminMachine)); });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// ...
app.UseClaimsTransformation( AddMachineRoleClaims );
// app.UseMvc( ... );
// ...etc...
}
public Task<ClaimsPrincipal> AddMachineRoleClaims(ClaimsTransformationContext ctx)
{
var connectionRemoteIpAddress = ctx.Context.Connection.RemoteIpAddress.MapToIPv4();
if (Configuration.GetSection("IpSecurity")["Author"].Contains(connectionRemoteIpAddress.ToString()))
{
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim(nameof(MachineRole), MachineRole.AuthorMachine) }));
}
if (Configuration.GetSection("IpSecurity")["Admin"].Contains(connectionRemoteIpAddress.ToString()))
{
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim( nameof(MachineRole), MachineRole.AdminMachine) }));
}
return Task.FromResult(ctx.Principal);
}
public static class MachineRole
{
public const string AuthorMachine = "AuthorMachine";
public const string AdminMachine = "AdminMachine";
}
and then you can use
[Authorize(Policy = MachineRole.AdminMachine)]

I was sufficiently irritated by the fact that this is not simple, and in particular not a simple as it was in MVC4 that I've done https://github.com/chrisfcarroll/RequireClaimAttributeAspNetCore to make it possible to write:
[RequireClaim("ClaimType",Value = "RequiredValue")]
public IActionResult Action(){}

Assuming you've thought about the implications of IP based authorization -- such that they can be spoofed, and requests make it very deep into your stack before being rejected...
I'd suggest creating a middleware that assigns claims, or at the very least sets the identity (so the user is authenticated). And then use either claims (which you've assigned to the identity in the middleware) or authorization policies (https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies). You could then reject each request based on the IPs associated with a policy:
[Authorize(Policy="AuthorIp")]
[HttpGet("post/create")]
public StandardResponse Create([FromBody]Post post)

Related

Authorization: How to handle mutiple (dozen or more) requirements

I have a set of tables in our database with users, permissions, and a join that maps which users have what permissions.
Looking at the docs, the following is an example of how policies and the requirement(s) are set up on Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
}
And here is an example of a handler for multiple requirements:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;
public class PermissionHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is ReadPermission)
{
if (IsOwner(context.User, context.Resource) ||
IsSponsor(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
else if (requirement is EditPermission ||
requirement is DeletePermission)
{
if (IsOwner(context.User, context.Resource))
{
context.Succeed(requirement);
}
}
}
//TODO: Use the following if targeting a version of
//.NET Framework older than 4.6:
// return Task.FromResult(0);
return Task.CompletedTask;
}
private bool IsOwner(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
private bool IsSponsor(ClaimsPrincipal user, object resource)
{
// Code omitted for brevity
return true;
}
}
My intention is to check my database tables within the handler to validate that the user has a setting that corresponds to the policy. To check if a user can upload files, the policy might look like this:
services.AddAuthorization(config =>
{
config.AddPolicy("CanUploadFiles", policy => policy.Requirements.Add(new CanDoRequirement("CanUploadFiles")));
});
Using an [Authorize] attribute for a given policy, I can check that within the handler. I have that much working.
Question: Given that I might have 10-20 separate "CanDo…" permissions in our table, is there a better way to set these up rather than have separate lines in AddAuthorization()?
Well, I'm not aware of any shortcuts when configuring the 20-ish requirements and policies that would remove the separate lines in startup, but you could consider implementing a sort of custom resource based authorization rather than a policy based one, policy-based being a declarative one. Declarative meaning the policy is pre-configured. Like so: [Authorize("policy")].
By using imperative authorization, rather than declarative, you would remove the need for x amount of policies to be configured. Instead of saying "Authorize this method", you let the framework take care of the authorization itself.
Consider the following requirements
A user must be authenticated.
That user can only upload a file if they satisfy the CanUploadFiles which is a boolean on the user's record in the database.
Now consider the following example
You have created your own ICustomAuthorizationHandler, somewhat similar to the the ASP.NET Core's IAuthorizationHandler, with the exception that you won't be satisfying a policy, but instead you will feed it a 'CanDoPermission' and it will return true or false if that user has that specific 'flag'.
public class FileController : Controller
{
private ICustomAuthorizationService _authService
public FileController(ICustomAuthorizationService authService)
{
_authService = authService;
}
[Authorize]
public async Task<IActionResult> Upload(IFormFile file)
{
var authResult = await _authService.AuthorizeAsync(User, "CanDoUpload");
if (!authResult.Succeeded)
{
return new ForbidResult();
}
// Process upload
return View();
}
}
This way, there wouldn't have to be policies nor requirements configured for checking if the user can upload a file. But, you would need to take care of a lot of the stuff that you get for 'free' by simply going for policies and configuring them in AddAuthorization.

Authorise the localhost in ASP.NET Core

I am newbie in ASP.NET Core, and I have a controller I need to authorise it only on my machine, for the test purposes, however, deny on other...
I have the following config:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.DateFormatString= "yyyy-MM-ddTHH:mm:ssZ";
});
services.AddAuthentication("Cookie")
.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>("Cookie", null);
services.AddLogging(builder => { builder.AddSerilog(dispose: true); });
And on the test controlled I enabled the [Authorise] attrubute
[Authorize]
public class OrderController : Controller
Is there a way to allow my local machine to be autorised to acces the controller's actions? Something like [Authorize(Allow=localhost)]
You can create an action filter like so:
public class LocalhostAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var ip = context.HttpContext.Connection.RemoteIpAddress;
if (!IPAddress.IsLoopback(ip)) {
context.Result = new UnauthorizedResult();
return;
}
base.OnActionExecuting(context);
}
}
And then use the tag Localhost:
//[Authorize]
[Localhost]
public class OrderController : Controller
I believe this will work, restricting the access to the machine where it's executed.
This is more whitelisting than authorization. Authorization means checking whether a user has permission to do something. To do that, the user must be identified first, ie authenticated.
The article Client IP Safelist in the docs shows how you can implement IP safelists through middleware, an action filter or a Razor Pages filter.
App-wide Middleware
The middleware option applies to the entire application. The sample code retrieves the request's endpoint IP, checks it against a list of safe IDs and allows the call to proceed only if it comes from a "safe" list. Otherwise it returns a predetermined error code, in this case 401:
public async Task Invoke(HttpContext context)
{
if (context.Request.Method != "GET")
{
var remoteIp = context.Connection.RemoteIpAddress;
_logger.LogDebug("Request from Remote IP address: {RemoteIp}", remoteIp);
string[] ip = _adminSafeList.Split(';');
var bytes = remoteIp.GetAddressBytes();
var badIp = true;
foreach (var address in ip)
{
var testIp = IPAddress.Parse(address);
if(testIp.GetAddressBytes().SequenceEqual(bytes))
{
badIp = false;
break;
}
}
if(badIp)
{
_logger.LogInformation(
"Forbidden Request from Remote IP address: {RemoteIp}", remoteIp);
context.Response.StatusCode = 401;
return;
}
}
await _next.Invoke(context);
}
The article shows registering it before UseMvc() which means the request will be rejected before reaching the MVC middleware :
app.UseMiddleware<AdminSafeListMiddleware>(Configuration["AdminSafeList"]);
app.UseMvc();
This way we don't waste CPU time routing and processing a request that's going to be rejected anyway. The middleware option is a good choice for implementing a blacklist too.
Action Filter
The filtering code is essentially the same, this time defined in a class derived from ActionFilterAttribute. The filter is defined as a scoped service :
services.AddScoped<ClientIpCheckFilter>();
services.AddMvc(options =>
{
options.Filters.Add
(new ClientIpCheckPageFilter
(_loggerFactory, Configuration));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
In this case the request will reach the MVC infrastructure before it's accepted or rejected.
Razor Pages Filter
The code is once more the same, this time deriving from IPageFilter

Windows Authentication using Active Directory Groups as Authorization Roles in ASP.NET Core 2.2?

I have a dot net core 2.2 application that needs to have Windows Authentication with an Active Directory Group lookup to get a list of assigned groups for the current principal. These assigned groups will be the 'roles' that will be used in the Authorize attribute of certain methods. At least, in theory, that's what I'm hoping to accomplish.
I have completed the AD lookup and retrieval of the groups. At this point I'm not sure how to configure the Startup to persist this info within an auth token/cookie of some type or any UserManager/RoleManager setup kinda stuff.
Here are a couple of previous, somewhat similar questions, among others I've looked at. This previous post from .net 4.5 appears to be a similar issue, but it's the wrong version of .NET : windows-authentication-with-active-directory-groups. Can these AD groups be added as roles? Here's a potentially helpful post with this where they create a role for a user: how-to-create-roles-in-asp-net-core-2-2-and-assign-them-to-users. Confused about how this works. I've always found Identity, claims, tokens, etc. confusing so hopefully someone can assist with this in Core 2.2.
What do I need to do to get this to work? I've included most of my current code (AD code, some middleware parts, etc.), but then what? I'm sure there are others that would benefit from this too! Thank you!
I get the current Windows user and their AD record here:
return Task.Run(() =>
{
try
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
if (context != null)
{
//var identityName = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
var identityName = identity.Name; // when windows authentication is checked
principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, identity.Name);
}
return AdUser.CastToAdUser(principal);
}
catch (Exception ex)
{
//TODO LOGGING
throw new Exception("Error retrieving AD User", ex);
}
});
The extension method CastToAdUser to create a more useful model is here:
public static AdUser CastToAdUser(UserPrincipal user)
{
return new AdUser
{
AccountExpirationDate = user.AccountExpirationDate,
AccountLockoutTime = user.AccountLockoutTime,
BadLogonCount = user.BadLogonCount,
Description = user.Description,
DisplayName = user.DisplayName,
DistinguishedName = user.DistinguishedName,
EmailAddress = user.EmailAddress,
EmployeeId = user.EmployeeId,
Enabled = user.Enabled,
GivenName = user.GivenName,
Guid = user.Guid,
HomeDirectory = user.HomeDirectory,
HomeDrive = user.HomeDrive,
LastBadPasswordAttempt = user.LastBadPasswordAttempt,
LastLogon = user.LastLogon,
LastPasswordSet = user.LastPasswordSet,
MiddleName = user.MiddleName,
Name = user.Name,
PasswordNeverExpires = user.PasswordNeverExpires,
PasswordNotRequired = user.PasswordNotRequired,
SamAccountName = user.SamAccountName,
ScriptPath = user.ScriptPath,
Sid = user.Sid,
Surname = user.Surname,
UserCannotChangePassword = user.UserCannotChangePassword,
UserPrincipalName = user.UserPrincipalName,
VoiceTelephoneNumber = user.VoiceTelephoneNumber,
Token = string.Empty,
};
}
return Task.Run(() =>
{
PrincipalSearchResult<Principal> groups = UserPrincipal.Current.GetGroups();
IEnumerable<SecurityGroup> securityGroups = groups.Select(x => x.ToAdUserSecurityGroups());
return securityGroups;
});
With the extension method to create a useful model, ToAdUserSecurityGroups here:
public static SecurityGroup ToAdUserSecurityGroups (this Principal result)
{
var securityGroup = new SecurityGroup
{
Sid = result.Sid.Value,
Name = result.SamAccountName,
Guid = result.Guid.Value,
};
return securityGroup;
}
So now I have the AD user, and the security groups that will hopefully be used for Authorization. I wire in my AD lookup stuff using some custom middleware, called UseAdMiddleWare. In my Startup class, I have an extension in the Configure method to fire off all the above 'stuff':
app.UseAdMiddleware();
And in my ConfigureServices I have the AddAuthentication stuff, which is needed, but might not be configured correctly for what I'm trying to do:
services.AddAuthentication();
In separate classes I have the code that allows this. The IAdUserProvider is my own class that does the AD lookup, with an entry point called Create:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseAdMiddleware(this IApplicationBuilder builder) =>
builder.UseMiddleware<AdUserMiddleware>();
}
public class AdUserMiddleware
{
private readonly RequestDelegate next;
public AdUserMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context, IAdUserProvider userProvider, IConfiguration config)
{
if (!(userProvider.Initialized))
{
await userProvider.Create(context, config);
}
await next(context);
}
}
So I think I'm well on my way to getting this wired up, but how/where do I add the security group specifics into claims or whatever? Thank you very much!
I (mostly) found a solution to this using Nan's recommendation to use the IClaimsTransformer. The concrete implementation of this class fires every Authorize request, and I'm not sure if there's a possible way to persist these claims?
Here's my Startup.ConfigureServices, where I have some IIS options to automatically log in using my Windows auth, and there's the line to create the singleton of my IClaimsTransformation:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.Configure<IISServerOptions>(options =>
{
options.AutomaticAuthentication = true;
});
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
options.ForwardClientCertificate = true;
});
services.AddSingleton<IClaimsTransformation, CustomClaimsTransformation>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
}
In Startup.Configure I have this: Do I need the cookiepolicy?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Add whatever you typically need here...
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
My CustomClaimsTransformation is here, and this fires at each Authorize. Is this normal? I'm adding the Security Groups as ROLES so I can use these to Authorize the users based on the groups they are assigned. I had hoped that this would be handled once, and the claims would be permanent for the duration. Thoughts on this?
public class CustomClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
//add new claim
// Check and see if Groups are already part of the principal, and add them as claims.
// var groups = userClaimsId.Claims.Where(x => x.Type.Equals("groups")).ToList();
var ci = (ClaimsIdentity)principal.Identity;
var c = new Claim(ci.RoleClaimType, "Super_Special_User");
ci.AddClaim(c);
return Task.FromResult(principal);
}
}
Within the Controller I add the Authorize attribute (seems to be case sensitive). It might be a good ideas to create a static class of role string constants to hold all these values. Keeps you free of the magic strings all over the place.
[Authorize(Roles = "Super_Special_User")]
Please let me know if I can improve this! Thanks for your time!

Authorization policy via attribute before data binding in web api

I am struggling to find a good solution for doing custom authorization checks without having to repeat the authorization check manually over and over again.
To illustrate, suppose I have the following setup for a .net core web api, which has two endpoints, one for GET and one for POST. I would like to check (maybe against db) whether the user has the right to see the resource, or the right to create a resource.
This is what the documentation refers to as resource based authorization
and would look something like this:
[Authorize]
[ApiVersion ("1.0")]
[Route ("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ResourcesController : ControllerBase {
private readonly IAuthorizationService _authorizationService;
//..constructor DI
[HttpGet ("{resourceId}")]
public ActionResult<Resource> Get (Guid resourceId) {
var authorizationCheck = await _authorizationService.AuthorizeAsync (User, resourceId, ServiceOperations.Read);
if (!authorizationCheck.Succeeded) {
return Forbid ();
}
return Ok (ResourceRep.Get (resourceId));
}
[HttpPost]
public ActionResult<Resource> Post ([FromBody] Resource resource) {
var authorizationCheck = await _authorizationService.AuthorizeAsync (User, null, ServiceOperations.Write);
if (!authorizationCheck.Succeeded) {
return Forbid ();
}
return Ok (ResourceRep.Create (resource));
}
}
Now imagine the ServiceOperations enum has a long list of supported operations, or there are 100 different endpoints, I will have to do the same check everywhere, or even worse, might forget to add a check where I should definitely have added a check. And there is not an easy way to pick this up in unit tests.
I thought of using attributes but as the docs state:
Attribute evaluation occurs before data binding and before execution of the page handler or action that loads the document. For these reasons, declarative authorization with an [Authorize] attribute doesn't suffice. Instead, you can invoke a custom authorization method—a style known as imperative authorization.
So it seems I cannot use an authorization policy and decorate the methods with authorization attributes (which are easy to unit test that they are there) when the check itself requires a parameter that is not available (the resourceId).
So for the question itself:
How do you use imperative (resource based) authorization generically without having to repeat yourself (which is error-prone). I would love to have an attribute like the following:
[HttpGet ("{resourceId}")]
[AuthorizeOperation(Operation = ServiceOperations.Read, Resource=resourceId)]
public ActionResult<Resource> Get (Guid resourceId) {..}
[AuthorizeOperation(Operation = ServiceOperations.Write)]
[HttpPost]
public ActionResult<Resource> Post ([FromBody] Resource resource) {..}
You can achieve it using AuthorizationHandler in a policy-based authorization and combine with an injected service specifically created to determine the Operation-Resources pairing.
To do it, first setup the policy in Startup.ConfigureServices :
services.AddAuthorization(options =>
{
options.AddPolicy("OperationResource", policy => policy.Requirements.Add( new OperationResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IOperationResourceService, OperationResourceService>();
next create the OperationResourceHandler :
public class OperationResourceHandler: AuthorizationHandler<OperationResourceRequirement>
{
readonly IOperationResourceService _operationResourceService;
public OperationResourceHandler(IOperationResourceService o)
{
_operationResourceService = o;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, OperationResourceRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext filterContext)
{
var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
if (_operationResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
the OperationResourceRequirement can be an empty class:
public class OperationResourceRequirement : IAuthorizationRequirement { }
The trick is, rather than specify action's Operation in attribute, we specify it elsewhere such as in database, in appsettings.json, in some config file, or hardcoded.
Here's an example getting the Operation-Resource pair from config file:
public class OperationResourceService : IOperationResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class OpeartionResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u)
{
_config = c;
_accessor = a;
_userManager = u;
}
public bool IsAuthorize(string area, string controller, string action, string id)
{
var operationConfig = _config.GetValue<string>($"OperationSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
//all of needed data are available now, do the logic of authorization
return result;
}
}
Please note that to make IHttpContextAccessor injectable, add services.AddHttpContextAccessor() in Startup.ConfigurationServices method body.
After all is done, use the policy on an action:
[HttpGet ("{resourceId}")]
[Authorize(Policy = "OperationResource")]
public ActionResult<Resource> Get (Guid resourceId) {..}
the authorize policy can be the same for every action.

ASP.NET Core Identity 2: User.IsInRole always returns false

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