I have a dotnet core 2.2 api with some controllers and action methods that needs to be authorized based on a user claim and the resource being accessed. Basically, each user can have 0 or many "roles" for each resource. This is all done using ASP.NET Identity Claims.
So, my understanding is that I need to make use of Resource-based authorization. But both examples there are mostly identical and require the explicit imperative if/else logic on each action method, which is what I'm trying to avoid.
I want to be able to do something like
[Authorize("Admin")] // or something similar
public async Task<IActionResult> GetSomething(int resourceId)
{
var resource = await SomeRepository.Get(resourceId);
return Json(resource);
}
And somewhere else define the authorization logic as a policy/filter/requirement/whatever and have access to both the current user claims and the resourceId parameter received by the endpoint. So there I can see if the user has a claim that denotes that he has the "Admin" role for that specific resourceId.
Edit: Based on feedback to make it dynamic
The key thing with RBAC and claims in .NET, is to create your ClaimsIdentity and then let the framework do it's job. Below is an example middleware that will look at the query parameter "user" and then generate the ClaimsPrincipal based on a dictionary.
To avoid the need to actually wire up to an identity provider, I created a Middleware that sets up the ClaimsPrincipal:
// **THIS CLASS IS ONLY TO DEMONSTRATE HOW THE ROLES NEED TO BE SETUP **
public class CreateFakeIdentityMiddleware
{
private readonly RequestDelegate _next;
public CreateFakeIdentityMiddleware(RequestDelegate next)
{
_next = next;
}
private readonly Dictionary<string, string[]> _tenantRoles = new Dictionary<string, string[]>
{
["tenant1"] = new string[] { "Admin", "Reader" },
["tenant2"] = new string[] { "Reader" },
};
public async Task InvokeAsync(HttpContext context)
{
// Assume this is the roles
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "John"),
new Claim(ClaimTypes.Email, "john#someemail.com")
};
foreach (KeyValuePair<string, string[]> tenantRole in _tenantRoles)
{
claims.AddRange(tenantRole.Value.Select(x => new Claim(ClaimTypes.Role, $"{tenantRole.Key}:{x}".ToLower())));
}
// Note: You need these for the AuthorizeAttribute.Roles
claims.AddRange(_tenantRoles.SelectMany(x => x.Value)
.Select(x => new Claim(ClaimTypes.Role, x.ToLower())));
context.User = new System.Security.Claims.ClaimsPrincipal(new ClaimsIdentity(claims,
"Bearer"));
await _next(context);
}
}
To wire this up, just use the UseMiddleware extension method for IApplicationBuilder in your startup class.
app.UseMiddleware<RBACExampleMiddleware>();
I create an AuthorizationHandler which will look for the query parameter "tenant" and either succeed or fail based on the roles.
public class SetTenantIdentityHandler : AuthorizationHandler<TenantRoleRequirement>
{
public const string TENANT_KEY_QUERY_NAME = "tenant";
private static readonly ConcurrentDictionary<string, string[]> _methodRoles = new ConcurrentDictionary<string, string[]>();
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TenantRoleRequirement requirement)
{
if (HasRoleInTenant(context))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
private bool HasRoleInTenant(AuthorizationHandlerContext context)
{
if (context.Resource is AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.HttpContext
.Request
.Query
.TryGetValue(TENANT_KEY_QUERY_NAME, out StringValues tenant)
&& !string.IsNullOrWhiteSpace(tenant))
{
if (TryGetRoles(authorizationFilterContext, tenant.ToString().ToLower(), out string[] roles))
{
if (context.User.HasClaim(x => roles.Any(r => x.Value == r)))
{
return true;
}
}
}
}
return false;
}
private bool TryGetRoles(AuthorizationFilterContext authorizationFilterContext,
string tenantId,
out string[] roles)
{
string actionId = authorizationFilterContext.ActionDescriptor.Id;
roles = null;
if (!_methodRoles.TryGetValue(actionId, out roles))
{
roles = authorizationFilterContext.Filters
.Where(x => x.GetType() == typeof(AuthorizeFilter))
.Select(x => x as AuthorizeFilter)
.Where(x => x != null)
.Select(x => x.Policy)
.SelectMany(x => x.Requirements)
.Where(x => x.GetType() == typeof(RolesAuthorizationRequirement))
.Select(x => x as RolesAuthorizationRequirement)
.SelectMany(x => x.AllowedRoles)
.ToArray();
_methodRoles.TryAdd(actionId, roles);
}
roles = roles?.Select(x => $"{tenantId}:{x}".ToLower())
.ToArray();
return roles != null;
}
}
The TenantRoleRequirement is a very simple class:
public class TenantRoleRequirement : IAuthorizationRequirement { }
Then you wire everything up in the startup.cs file like this:
services.AddTransient<IAuthorizationHandler, SetTenantIdentityHandler>();
// Although this isn't used to generate the identity, it is needed
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://localhost:5000/identity/";
});
services.AddAuthorization(authConfig =>
{
authConfig.AddPolicy(Policies.HasRoleInTenant, policyBuilder => {
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new TenantRoleRequirement());
});
});
The method looks like this:
// TOOD: Move roles to a constants/globals
[Authorize(Policy = Policies.HasRoleInTenant, Roles = "admin")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
Below are the test scenarios:
Positive: https://localhost:44337/api/values?tenant=tenant1
Negative: https://localhost:44337/api/values?tenant=tenant2
Negative: https://localhost:44337/api/values
The key thing with this approach is that I never actually return a 403. The code setups the identity and then lets the framework handle the result. This ensures authentication is separate from authorization.
You could create your own attribute which will check the user's role. I have done this in one of my applications:
public sealed class RoleValidator : Attribute, IAuthorizationFilter
{
private readonly IEnumerable<string> _roles;
public RoleValidator(params string[] roles) => _roles = roles;
public RoleValidator(string role) => _roles = new List<string> { role };
public void OnAuthorization(AuthorizationFilterContext filterContext)
{
if (filterContext.HttpContext.User.Claims == null || filterContext.HttpContext.User.Claims?.Count() <= 0)
{
filterContext.Result = new UnauthorizedResult();
return;
}
if (CheckUserRoles(filterContext.HttpContext.User.Claims))
return;
filterContext.Result = new ForbidResult();
}
private bool CheckUserRoles(IEnumerable<Claim> claims) =>
JsonConvert.DeserializeObject<List<RoleDto>>(claims.FirstOrDefault(x => x.Type.Equals(ClaimType.Roles.ToString()))?.Value)
.Any(x => _roles.Contains(x.Name));
}
It gets user role from claims and check is user have proper role to get this resouce.
You can use it like this:
[RoleValidator("Admin")]
or better approach with enum:
[RoleValidator(RoleType.Admin)]
or you can pass a multiple roles:
[RoleValidator(RoleType.User, RoleType.Admin)]
With this solution you must also use the standard Authorize attribute.
Edited based on comments
According to my understanding, you want to access current user (all information related to it), the role(s) you want to specify for a controller (or action) and parameters received by endpoint. Haven't tried for web api, but for asp.net core MVC, You can achieve this by using AuthorizationHandler in a policy-based authorization and combine with an injected service specifically created to determine the Roles-Resources access.
To do it, first setup the policy in Startup.ConfigureServices :
services.AddAuthorization(options =>
{
options.AddPolicy("UserResource", policy => policy.Requirements.Add( new UserResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IRoleResourceService, RoleResourceService>();
next create the UserResourceHandler :
public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
readonly IRoleResourceService _roleResourceService;
public UserResourceHandler (IRoleResourceService r)
{
_roleResourceService = r;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement 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 (_roleResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
Accessing the parameters received by endpoint is achieved by casting context.Resource to AuthorizationFilterContext, so that we could access RouteData from it. As for UserResourceRequirement, we can leave it empty.
public class UserResourceRequirement : IAuthorizationRequirement { }
As for the IRoleResourceService, it's a plain service class so that we can inject anything to it. This service is the substitute of pairing a Role to an action in code so that we don't need to specify it in action's attribute. That way, we can have a freedom to choose the implementation, ex: from database, from config file, or hard-coded.
Accessing user in RoleResourceService is achieved by injecting IHttpContextAccessor. Please note that to make IHttpContextAccessor injectable, add services.AddHttpContextAccessor() in Startup.ConfigurationServices method body.
Here's an example getting the info from config file:
public class RoleResourceService : IRoleResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class RoleResourceService(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 roleConfig = _config.GetValue<string>($"RoleSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
var userRoles = await _userManager.GetRolesAsync(appUser);
// all of needed data are available now, do the logic of authorization
return result;
}
}
Get the setting from database surely is a bit more complex, but it can be done since we can inject AppDbContext. For the hardcoded approach, exist plenty ways to do it.
After all is done, use the policy on an action:
[Authorize(Policy = "UserResource")] //dont need Role name because of the RoleResourceService
public ActionResult<IActionResult> GetSomething(int resourceId)
{
//existing code
}
In fact, we can use "UserResource" policy for any action that we want to apply.
You can use Roles if you are using identity. Simply call authorize and provide it with the Role name on a resource or a whole controller or even add more roles to the authorize like below:
[Authorize(Roles ="Clerk")]
I am authorizing a User Role with a name of Clerk on a certain resource. To add more roles simply add a comma after the clerk and add the other role name
I've implemented a custom User Store for ASP.NET Identity by following the example set here. That all works fine, except for this:
I need access to data about the currently logged in user in my user store. Normally, you'd access that by accessing
HttpContext.Current.User
Now, once auser has logged in, if he user then goes to the Manage controller (e.g. to try and change his/her password), when ASP.NET identity looks up the user again by calling
CustomUserManager.FindByIdAsync(string userId)
HttpContext.Current is empty altogether (that's prior to rendering the page). So, how do I get information about the HttpContext in this scenario? The user is properly logged in, so how do I figure out which user has been logged in?
#edit.. the problem is in CustomUserStore.. here's a bit of it
public class CustomUserStore<TUser> : IUserStore<TUser>, IUserLoginStore<TUser>, IUserClaimStore<TUser>, IUserPasswordStore<TUser>, IUserSecurityStampStore<TUser>, IUserEmailStore<TUser>, IUserPhoneNumberStore<TUser>,
IUserLockoutStore<TUser, string>, IUserTwoFactorStore<TUser, string>//, IQueryableUserStore<TUser>
where TUser: CustomUser<string>, IUser<string>
{
string storageFile = #"c:\temp\aspnetusers.json";
List<TUser> users;
public CustomUserStore()
{
if (File.Exists(storageFile))
{
string contents = File.ReadAllText(storageFile);
users = JsonConvert.DeserializeObject<List<TUser>>(contents);
if (users == null)
users = new List<TUser>();
}
else
users = new List<TUser>();
}
#region IUserStore implementation
public Task<TUser> FindByIdAsync(string userId)
{
string sessionId = HttpContext.Current?.Session?.SessionID;
return Task.FromResult<TUser>(users.FirstOrDefault(u => u.Id == userId));
}
public Task<TUser> FindByNameAsync(string userName)
{
string sessionId = HttpContext.Current?.Session?.SessionID;
return Task.FromResult<TUser>(users.FirstOrDefault(u => string.Compare(u.UserName, userName, true) == 0));
}
#endregion
}
and it's in the FindByAsync method where HttpContext.Current can be empty.
It happens in the Index method of the AccountController when the model is created
var model = new IndexViewModel
{
HasPassword = HasPassword(),
PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
Logins = await UserManager.GetLoginsAsync(userId),
BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId)
};
And it's the FindById request in the HasPassword method that causes the problem
private bool HasPassword()
{
var user = UserManager.FindById(User.Identity.GetUserId());
if (user != null)
{
return user.PasswordHash != null;
}
return false;
}
The other 4 requests to the user manager all have a filled out HttpContext.Current. So it appears that it's calls to UserManager that cause the issue.
Having identified the exact source of the problem, it's easy enough to fix.
Add this async emthod to check if the user has a password:
private async Task<bool> HasPasswordAsync()
{
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user != null)
{
return user.PasswordHash != null;
}
return false;
}
And in the Index method, use the new async methode
var model = new IndexViewModel
{
HasPassword = await HasPasswordAsync(),
PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
Logins = await UserManager.GetLoginsAsync(userId),
BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId)
};
But, why does the synchronous method call break things? You'd imagine the sync call would run into the standard context where HttpContext.Current should be available.
I have a more custom User Store in my real project where I run into this problem a lot more frequently.. guess I need to check if contains a lot more synchronous access to UserManager methods.
I'm working on an MVC 6 application that does not use Entity or Identity. Instead, I'm using Dapper. I have a controller that accepts a POST request and uses Dapper to check the database to see if the users name / password match. All I'd like to do is store the users name and whether they're logged in or not so I can make that check on subsequent pages.
I looked around and it seems like using Cookie based authentication should allow me to do what I want. Here's the relevant code in my Startup.cs file:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/account/login",
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
Here's what the relevant code in my controllers login action looks like:
var user = _repo.FindByLogin(model.VendorId, model.Password);
if (user != null) {
var claims = new List < Claim > {
new Claim("VendorId", user.VendorId),
new Claim("Name", "john")
};
var id = new ClaimsIdentity(claims, "local", "name", "role");
await HttpContext.Authentication.SignInAsync("Cookies", new ClaimsPrincipal(id));
var l = ClaimsIdentity.DefaultNameClaimType;
return RedirectToAction("Index", "PurchaseOrders");
}
The above code seems to work in that a cookie is being saved, but I'm not sure how I would go about getting the user information back out of the cookie (or even how to retrieve the cookie on subsequent requests in the first place).
In my mind I'm imagining being able to do something like: var user = (User)HttpContext.Request.Cookies.Get(????), but I'm not sure if that's practical or not.
You can get the user data back by using the ClaimsPrincipal.FindFirstValue(xxx)
here is my example class which can be used in Controller/Views to get the current user information
public class GlobalSettings : IGlobalSettings
{
private IHttpContextAccessor _accessor;
public GlobalSettings(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public string RefNo
{
get
{
return GetValue(_accessor.HttpContext.User, "employeeid");
}
}
public string SAMAccount
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.WindowsAccountName);
}
}
public string UserName
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.Name);
}
}
public string Role
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.Role);
}
}
private string GetValue(ClaimsPrincipal principal, string key)
{
if (principal == null)
return string.Empty;
return principal.FindFirstValue(key);
}
}
Example Usage in controller after DI:
var currentUser = GlobalSettings.SAMAccount;
Please note that you need to inject HttpContextAccessor in ConfigureServices method in Startup.cs
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
I'm using OWIN authentication for my MVC5 project.
This is my SignInAsync
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
var AccountNo = "101";
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
identity.AddClaim(new Claim(ClaimTypes.UserData, AccountNo));
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent, RedirectUri="Account/Index"}, identity);
}
As you can see, i added AccountNo into the Claims list.
Now, how can I update this Claim at some point in my application? So far, i have this:
public string AccountNo
{
get
{
var CP = ClaimsPrincipal.Current.Identities.First();
var Account= CP.Claims.FirstOrDefault(p => p.Type == ClaimTypes.UserData);
return Account.Value;
}
set
{
var CP = ClaimsPrincipal.Current.Identities.First();
var AccountNo= CP.Claims.FirstOrDefault(p => p.Type == ClaimTypes.UserData).Value;
CP.RemoveClaim(new Claim(ClaimTypes.UserData,AccountNo));
CP.AddClaim(new Claim(ClaimTypes.UserData, value));
}
}
when i try to remove the claim, I get this exception:
The Claim
'http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata:
101' was not able to be removed. It is either not part of this
Identity or it is a claim that is owned by the Principal that contains
this Identity. For example, the Principal will own the claim when
creating a GenericPrincipal with roles. The roles will be exposed
through the Identity that is passed in the constructor, but not
actually owned by the Identity. Similar logic exists for a
RolePrincipal.
How does one remove and update the Claim?
I created a Extension method to Add/Update/Read Claims based on a given ClaimsIdentity
namespace Foobar.Common.Extensions
{
public static class Extensions
{
public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return;
// check for existing claim and remove it
var existingClaim = identity.FindFirst(key);
if (existingClaim != null)
identity.RemoveClaim(existingClaim);
// add new claim
identity.AddClaim(new Claim(key, value));
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
}
public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return null;
var claim = identity.Claims.FirstOrDefault(c => c.Type == key);
// ?. prevents a exception if claim is null.
return claim?.Value;
}
}
}
and then to use it
using Foobar.Common.Extensions;
namespace Foobar.Web.Main.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
// add/updating claims
User.AddUpdateClaim("key1", "value1");
User.AddUpdateClaim("key2", "value2");
User.AddUpdateClaim("key3", "value3");
}
public ActionResult Details()
{
// reading a claim
var key2 = User.GetClaimValue("key2");
}
}
}
You can create a new ClaimsIdentity and then do the claims update with such.
set {
// get context of the authentication manager
var authenticationManager = HttpContext.GetOwinContext().Authentication;
// create a new identity from the old one
var identity = new ClaimsIdentity(User.Identity);
// update claim value
identity.RemoveClaim(identity.FindFirst("AccountNo"));
identity.AddClaim(new Claim("AccountNo", value));
// tell the authentication manager to use this new identity
authenticationManager.AuthenticationResponseGrant =
new AuthenticationResponseGrant(
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = true }
);
}
Another (async) approach, using Identity's UserManager and SigninManager to reflect the change in the Identity cookie (and to optionally remove claims from db table AspNetUserClaims):
// Get User and a claims-based identity
ApplicationUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
var Identity = new ClaimsIdentity(User.Identity);
// Remove existing claim and replace with a new value
await UserManager.RemoveClaimAsync(user.Id, Identity.FindFirst("AccountNo"));
await UserManager.AddClaimAsync(user.Id, new Claim("AccountNo", value));
// Re-Signin User to reflect the change in the Identity cookie
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
// [optional] remove claims from claims table dbo.AspNetUserClaims, if not needed
var userClaims = UserManager.GetClaims(user.Id);
if (userClaims.Any())
{
foreach (var item in userClaims)
{
UserManager.RemoveClaim(user.Id, item);
}
}
Using latest Asp.Net Identity with .net core 2.1, I'm being able to update user claims with the following logic.
Register a UserClaimsPrincipalFactory so that every time SignInManager sings user in, the claims are created.
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimService>();
Implement a custom UserClaimsPrincipalFactory<TUser, TRole> like below
public class UserClaimService : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
{
private readonly ApplicationDbContext _dbContext;
public UserClaimService(ApplicationDbContext dbContext, UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{
_dbContext = dbContext;
}
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
// Get user claims from DB using dbContext
// Add claims
((ClaimsIdentity)principal.Identity).AddClaim(new Claim("claimType", "some important claim value"));
return principal;
}
}
Later in your application when you change something in the DB and would like to reflect this to your authenticated and signed in user, following lines achieves this:
var user = await _userManager.GetUserAsync(User);
await _signInManager.RefreshSignInAsync(user);
This makes sure user can see up to date information without requiring login again. I put this just before returning the result in the controller so that when the operation finishes, everything securely refreshed.
Instead of editing existing claims and creating race conditions for secure cookie etc, you just sign user in silently and refresh the state :)
I get that exception too and cleared things up like this
var identity = User.Identity as ClaimsIdentity;
var newIdentity = new ClaimsIdentity(identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
newIdentity.AddClaims(identity.Claims.Where(c => false == (c.Type == claim.Type && c.Value == claim.Value)));
// the claim has been removed, you can add it with a new value now if desired
AuthenticationManager.SignOut(identity.AuthenticationType);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, newIdentity);
Compiled some answers from here into re-usable ClaimsManager class with my additions.
Claims got persisted, user cookie updated, sign in refreshed.
Please note that ApplicationUser can be substituted with IdentityUser if you didn't customize former. Also in my case it needs to have slightly different logic in Development environment, so you might want to remove IWebHostEnvironment dependency.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using YourMvcCoreProject.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Hosting;
namespace YourMvcCoreProject.Identity
{
public class ClaimsManager
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IWebHostEnvironment _env;
private readonly ClaimsPrincipalAccessor _currentPrincipalAccessor;
public ClaimsManager(
ClaimsPrincipalAccessor currentPrincipalAccessor,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IWebHostEnvironment env)
{
_currentPrincipalAccessor = currentPrincipalAccessor;
_userManager = userManager;
_signInManager = signInManager;
_env = env;
}
/// <param name="refreshSignin">Sometimes (e.g. when adding multiple claims at once) it is desirable to refresh cookie only once, for the last one </param>
public async Task AddUpdateClaim(string claimType, string claimValue, bool refreshSignin = true)
{
await AddClaim(
_currentPrincipalAccessor.ClaimsPrincipal,
claimType,
claimValue,
async user =>
{
await RemoveClaim(_currentPrincipalAccessor.ClaimsPrincipal, user, claimType);
},
refreshSignin);
}
public async Task AddClaim(string claimType, string claimValue, bool refreshSignin = true)
{
await AddClaim(_currentPrincipalAccessor.ClaimsPrincipal, claimType, claimValue, refreshSignin);
}
/// <summary>
/// At certain stages of user auth there is no user yet in context but there is one to work with in client code (e.g. calling from ClaimsTransformer)
/// that's why we have principal as param
/// </summary>
public async Task AddClaim(ClaimsPrincipal principal, string claimType, string claimValue, bool refreshSignin = true)
{
await AddClaim(
principal,
claimType,
claimValue,
async user =>
{
// allow reassignment in dev
if (_env.IsDevelopment())
await RemoveClaim(principal, user, claimType);
if (GetClaim(principal, claimType) != null)
throw new ClaimCantBeReassignedException(claimType);
},
refreshSignin);
}
public async Task RemoveClaims(IEnumerable<string> claimTypes, bool refreshSignin = true)
{
await RemoveClaims(_currentPrincipalAccessor.ClaimsPrincipal, claimTypes, refreshSignin);
}
public async Task RemoveClaims(ClaimsPrincipal principal, IEnumerable<string> claimTypes, bool refreshSignin = true)
{
AssertAuthenticated(principal);
foreach (var claimType in claimTypes)
{
await RemoveClaim(principal, claimType);
}
// reflect the change in the Identity cookie
if (refreshSignin)
await _signInManager.RefreshSignInAsync(await _userManager.GetUserAsync(principal));
}
public async Task RemoveClaim(string claimType, bool refreshSignin = true)
{
await RemoveClaim(_currentPrincipalAccessor.ClaimsPrincipal, claimType, refreshSignin);
}
public async Task RemoveClaim(ClaimsPrincipal principal, string claimType, bool refreshSignin = true)
{
AssertAuthenticated(principal);
var user = await _userManager.GetUserAsync(principal);
await RemoveClaim(principal, user, claimType);
// reflect the change in the Identity cookie
if (refreshSignin)
await _signInManager.RefreshSignInAsync(user);
}
private async Task AddClaim(ClaimsPrincipal principal, string claimType, string claimValue, Func<ApplicationUser, Task> processExistingClaims, bool refreshSignin)
{
AssertAuthenticated(principal);
var user = await _userManager.GetUserAsync(principal);
await processExistingClaims(user);
var claim = new Claim(claimType, claimValue);
ClaimsIdentity(principal).AddClaim(claim);
await _userManager.AddClaimAsync(user, claim);
// reflect the change in the Identity cookie
if (refreshSignin)
await _signInManager.RefreshSignInAsync(user);
}
/// <summary>
/// Due to bugs or as result of debug it can be more than one identity of the same type.
/// The method removes all the claims of a given type.
/// </summary>
private async Task RemoveClaim(ClaimsPrincipal principal, ApplicationUser user, string claimType)
{
AssertAuthenticated(principal);
var identity = ClaimsIdentity(principal);
var claims = identity.FindAll(claimType).ToArray();
if (claims.Length > 0)
{
await _userManager.RemoveClaimsAsync(user, claims);
foreach (var c in claims)
{
identity.RemoveClaim(c);
}
}
}
private static Claim GetClaim(ClaimsPrincipal principal, string claimType)
{
return ClaimsIdentity(principal).FindFirst(claimType);
}
/// <summary>
/// This kind of bugs has to be found during testing phase
/// </summary>
private static void AssertAuthenticated(ClaimsPrincipal principal)
{
if (!principal.Identity.IsAuthenticated)
throw new InvalidOperationException("User should be authenticated in order to update claims");
}
private static ClaimsIdentity ClaimsIdentity(ClaimsPrincipal principal)
{
return (ClaimsIdentity) principal.Identity;
}
}
public class ClaimCantBeReassignedException : Exception
{
public ClaimCantBeReassignedException(string claimType) : base($"{claimType} can not be reassigned")
{
}
}
public class ClaimsPrincipalAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ClaimsPrincipalAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public ClaimsPrincipal ClaimsPrincipal => _httpContextAccessor.HttpContext.User;
}
// to register dependency put this into your Startup.cs and inject ClaimsManager into Controller constructor (or other class) the in same way as you do for other dependencies
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddTransient<ClaimsPrincipalAccessor>();
services.AddTransient<ClaimsManager>();
}
}
}
when I use MVC5, and add the claim here.
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(PATAUserManager manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
userIdentity.AddClaim(new Claim(ClaimTypes.Role, this.Role));
return userIdentity;
}
when I'm check the claim result in the SignInAsync function,i can't get the role value use anyway. But...
after this request finished, I can access Role in other action(anther request).
var userWithClaims = (ClaimsPrincipal)User;
Claim CRole = userWithClaims.Claims.First(c => c.Type == ClaimTypes.Role);
so, i think maybe asynchronous cause the IEnumerable updated behind the process.
You can update claims for the current user by implementing a CookieAuthenticationEvents class and overriding ValidatePrincipal. There you can remove the old claim, add the new one, and then replace the principal using CookieValidatePrincipalContext.ReplacePrincipal. This does not affect any claims stored in the database. This is using ASP.NET Core Identity 2.2.
public class MyCookieAuthenticationEvents : CookieAuthenticationEvents
{
string newAccountNo = "102";
public override Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// first remove the old claim
var claim = context.Principal.FindFirst(ClaimTypes.UserData);
if (claim != null)
{
((ClaimsIdentity)context.Principal.Identity).RemoveClaim(claim);
}
// add the new claim
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(ClaimTypes.UserData, newAccountNo));
// replace the claims
context.ReplacePrincipal(context.Principal);
context.ShouldRenew = true;
return Task.CompletedTask;
}
}
You need to register the events class in Startup.cs:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddScoped<MyCookieAuthenticationEvents>();
services.ConfigureApplicationCookie(o =>
{
o.EventsType = typeof(MyCookieAuthenticationEvents);
});
}
You can inject services into the events class to access the new AccountNo value but as per the warning on this page you should avoid doing anything too expensive:
Warning
The approach described here is triggered on every request. Validating
authentication cookies for all users on every request can result in a
large performance penalty for the app.
I am using a .net core 2.2 app and used the following solution:
in my statup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
...
})
.AddEntityFrameworkStores<AdminDbContext>()
.AddDefaultTokenProviders()
.AddSignInManager();
usage
private readonly SignInManager<IdentityUser> _signInManager;
public YourController(
...,
SignInManager<IdentityUser> signInManager)
{
...
_signInManager = signInManager;
}
public async Task<IActionResult> YourMethod() // <-NOTE IT IS ASYNC
{
var user = _userManager.FindByNameAsync(User.Identity.Name).Result;
var claimToUse = ClaimsHelpers.CreateClaim(ClaimTypes.ActiveCompany, JsonConvert.SerializeObject(cc));
var claimToRemove = _userManager.GetClaimsAsync(user).Result
.FirstOrDefault(x => x.Type == ClaimTypes.ActiveCompany.ToString());
if (claimToRemove != null)
{
var result = _userManager.ReplaceClaimAsync(user, claimToRemove, claimToUse).Result;
await _signInManager.RefreshSignInAsync(user); //<--- THIS
}
else ...
The simplest solution for updating existing claims for me at the moment was:
//updating user data
await signInManager.SignOutAsync();
await signInManager.SignInAsync(user, false);
Appreciate this question was about .NET 4/ OWIN, but to aid searchers looking for the .NET 5 or later equivalent, here's some sample code.
I'm sure you can improve it, but it's a working starter using the UserManager and SignInManager in Microsoft.AspNetCore.Identity.
// Get the user first first.
var claims = await _userManager.GetClaimsAsync(user);
var givenNameClaim = claims.FirstOrDefault(r => r.Type == JwtClaimTypes.GivenName);
IdentityResult result = null;
if (givenNameClaim != null)
{
result = await _userManager.ReplaceClaimAsync(user, givenNameClaim, new Claim(JwtClaimTypes.GivenName, "<newvalue>"));
}
else
{
result = await _userManager.AddClaimAsync(user, new Claim(JwtClaimTypes.GivenName, "<newvalue>"));
}
if (result.Errors.Any())
{
// TODO: List errors here;
}
else
{
await _signInManager.RefreshSignInAsync(user); // refresh the login, so it takes effect immediately.
}
To remove claim details from database we can use below code. Also, we need to sign in again to update the cookie values
// create a new identity
var identity = new ClaimsIdentity(User.Identity);
// Remove the existing claim value of current user from database
if(identity.FindFirst("NameOfUser")!=null)
await UserManager.RemoveClaimAsync(applicationUser.Id, identity.FindFirst("NameOfUser"));
// Update customized claim
await UserManager.AddClaimAsync(applicationUser.Id, new Claim("NameOfUser", applicationUser.Name));
// the claim has been updates, We need to change the cookie value for getting the updated claim
AuthenticationManager.SignOut(identity.AuthenticationType);
await SignInManager.SignInAsync(Userprofile, isPersistent: false, rememberBrowser: false);
return RedirectToAction("Index", "Home");
Multiple Cookies,Multiple Claims
public class ClaimsCookie
{
private readonly ClaimsPrincipal _user;
private readonly HttpContext _httpContext;
public ClaimsCookie(ClaimsPrincipal user, HttpContext httpContext = null)
{
_user = user;
_httpContext = httpContext;
}
public string GetValue(CookieName cookieName, KeyName keyName)
{
var principal = _user as ClaimsPrincipal;
var cp = principal.Identities.First(i => i.AuthenticationType == ((CookieName)cookieName).ToString());
return cp.FindFirst(((KeyName)keyName).ToString()).Value;
}
public async void SetValue(CookieName cookieName, KeyName[] keyName, string[] value)
{
if (keyName.Length != value.Length)
{
return;
}
var principal = _user as ClaimsPrincipal;
var cp = principal.Identities.First(i => i.AuthenticationType == ((CookieName)cookieName).ToString());
for (int i = 0; i < keyName.Length; i++)
{
if (cp.FindFirst(((KeyName)keyName[i]).ToString()) != null)
{
cp.RemoveClaim(cp.FindFirst(((KeyName)keyName[i]).ToString()));
cp.AddClaim(new Claim(((KeyName)keyName[i]).ToString(), value[i]));
}
}
await _httpContext.SignOutAsync(CookieName.UserProfilCookie.ToString());
await _httpContext.SignInAsync(CookieName.UserProfilCookie.ToString(), new ClaimsPrincipal(cp),
new AuthenticationProperties
{
IsPersistent = bool.Parse(cp.FindFirst(KeyName.IsPersistent.ToString()).Value),
AllowRefresh = true
});
}
public enum CookieName
{
CompanyUserProfilCookie = 0, UserProfilCookie = 1, AdminPanelCookie = 2
}
public enum KeyName
{
Id, Name, Surname, Image, IsPersistent
}
}
if (HttpContext.User.Identity is ClaimsIdentity identity)
{
identity.RemoveClaim(identity.FindFirst("userId"));
identity.AddClaim(new Claim("userId", userInfo?.id.ToString()));
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(HttpContext.User.Identity));
}
Old thread i know, but the requirements seem to have changed.
The below is working for me:
var user = await _userManager.FindByEmailAsync(input.Email);
var userClaim = await _userManager.GetClaimsAsync(user);
var userNameClaims = userClaim.Where(x => x.Type == ClaimTypes.GivenName).ToList();
await _userManager.RemoveClaimsAsync(user, userNameClaims);
await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.GivenName, user.Forename));
await _signInManager.SignOutAsync();
await _signInManager.SignInAsync(user, new AuthenticationProperties() { IsPersistent = input.RememberMe });
The sign out and in methods are essential, without the claims don't reflect the changes
The extension method worked great for me with one exception that if the user logs out there old claim sets still existed so with a tiny modification as in passing usermanager through everything works great and you dont need to logout and login.
I cant answer directly as my reputation has been dissed :(
public static class ClaimExtensions
{
public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value, ApplicationUserManager userManager)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return;
// check for existing claim and remove it
var existingClaim = identity.FindFirst(key);
if (existingClaim != null)
{
RemoveClaim(currentPrincipal, key, userManager);
}
// add new claim
var claim = new Claim(key, value);
identity.AddClaim(claim);
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
//Persist to store
userManager.AddClaim(identity.GetUserId(),claim);
}
public static void RemoveClaim(this IPrincipal currentPrincipal, string key, ApplicationUserManager userManager)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return ;
// check for existing claim and remove it
var existingClaims = identity.FindAll(key);
existingClaims.ForEach(c=> identity.RemoveClaim(c));
//remove old claims from store
var user = userManager.FindById(identity.GetUserId());
var claims = userManager.GetClaims(user.Id);
claims.Where(x => x.Type == key).ToList().ForEach(c => userManager.RemoveClaim(user.Id, c));
}
public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return null;
var claim = identity.Claims.First(c => c.Type == key);
return claim.Value;
}
public static string GetAllClaims(this IPrincipal currentPrincipal, ApplicationUserManager userManager)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return null;
var claims = userManager.GetClaims(identity.GetUserId());
var userClaims = new StringBuilder();
claims.ForEach(c => userClaims.AppendLine($"<li>{c.Type}, {c.Value}</li>"));
return userClaims.ToString();
}
}
Here you go:
var user = User as ClaimsPrincipal;
var identity = user.Identity as ClaimsIdentity;
var claim = (from c in user.Claims
where c.Type == ClaimTypes.UserData
select c).Single();
identity.RemoveClaim(claim);
taken from here.