JWT token role claim translated wrong by DOTNET
Asp.net core can automatic mapping some claims, You can add this configuration in Program.cs(Net 6) to avoid this
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
There are more details in this document, You can refer to it.
JWT authorization with roles in Identity Core
You don't need to use IdentityUser and identity database in your case, you are using JWT. Create your User
model with defined Roles
property and simple persist it in the database. Like:
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Role Role { get; set; }
}
public enum Role
{
Viewer,
Developer,
Manager
}
the token:
var user = // ...
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(your_seccret_key);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.FirstName),
new Claim(ClaimTypes.Name, user.LastName),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
controller method:
[Authorize(Roles = Role.Developer)]
[HttpGet("GetSomethingForAuthorizedOnly")]
public async Task<object> GetSomething()
{
// .... todo
}
Asp.Net Core jwt token is transformed after authentication
It has in fact been understood as ClaimTypes.Email
, however the string returned by this property is http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
(source).
The token is not transformed, unless specifically done so, rather parsed and understood as ClaimTypes.Email
with the actual token not modified.
How to populate JWT claims dynamically so policies can be used in controllers?
I have done something similar, I check the user has the required permission on each request. Mine is done based on Features, but you can change it a bit to be based on Roles.
I have a User
with 1 Role
, and then Role has N
Features.
For this I use created my own AuthorizationHandler
as follows. AuthorizationHandlers are at the start of the pipeline, so it will go through all AuthorizationHandlers before hitting the controller endpoint. Offical Docs here
'IFeaturesProvider': is just my Business layer to retrieve features from DB.
ICustomUserContext
: Is a wrapper around HttpContextAccessor.HttpContext.User
(I will post it at the end of answer for clarity.)
public class FeatureRequirement : IAuthorizationRequirement
{
public string FeatureName { get; set; }
public FeatureRequirement(string featureName)
{
FeatureName = featureName;
}
}
/// <summary>
/// Authorisation based on Feature.
/// A user is assigned a Role, a role can have N Features.
/// If the user is not assigned the Feature required to access the Controller Action this will throw an Authorisation Error -
/// </summary>
public class FeatureAuthorizationHandler : AuthorizationHandler<FeatureRequirement>
{
private readonly IFeaturesProvider _featuresProvider;
private readonly ICustomUserContext _userContext;
public FeatureAuthorizationHandler([FromServices] IFeaturesProvider featuresProvider,
[FromServices] ICustomUserContext userContext)
{
_featuresProvider = featuresProvider;
_userContext = userContext;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FeatureRequirement requirement)
{
// Authentication of user failed - Means Token expired or Token incorrect BUT Token exists in Header
if (!context.User.Identity.IsAuthenticated)
{
return Task.FromResult(0);
}
// get the logged in user id and user type
var userId = _userContext.UserId;
var userType = _userContext.UserType;
// get features assigned to currently logged in user
var userFeatures = _featuresProvider.GetFeaturesByUserId(userId, userType);
// check if user has the required feature assigned to it's Role
if (userFeatures.Select(s => s.Name).Contains(requirement.FeatureName))
context.Succeed(requirement);
return Task.FromResult(0);
}
}
Then in Startup.cs
you want to regiseter all your feature/roles as a permission.
services.AddAuthorization(options =>
{
// load all features from DB
var features = rolesProvider.GetAllFeatures(validationContainer);
// Add policy foreach feature in DB
foreach (var feature in features)
{
options.AddPolicy(feature.Name, policy => policy.Requirements.Add(new FeatureRequirement(feature.Name)));
}
});
Also in Startup
, register your Depency Injection for the AuthorizationHandler
services.AddScoped<IAuthorizationHandler, FeatureAuthorizationHandler>();
Then in controller you can just do:
[Authorize(Policy = "View Asset Driver")]
public class AssetDriversController : BaseController
{
where "View Asset Driver" is the name of one of my features.
OR
[Authorize(Policy = "View Asset Driver")]
public async Task<IActionResult> GetAssetDrivers(int companyId, int assetId)
Here is the ICustomUserContext
:
public interface ICustomUserContext
{
ClaimsPrincipal CurrentUser { get; }
int UserId { get; }
UserTypeEnum UserType { get; }
int ResellerId { get; }
int CompanyId { get; }
int DriverId { get; }
}
public class CustomUserContextAdapter : ICustomUserContext
{
private readonly IHttpContextAccessor _accessor;
public CustomUserContextAdapter(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public ClaimsPrincipal CurrentUser => _accessor.HttpContext.User;
public int UserId => CurrentUser != null ? CurrentUser.GetUserIdInternal() : 0;
public UserTypeEnum UserType => CurrentUser != null ? CurrentUser.GetUserTypeInternal() : UserTypeEnum.User;
public int ResellerId => CurrentUser != null ? CurrentUser.GetResellerIdInternal() : 0;
public int CompanyId => CurrentUser != null ? CurrentUser.GetCompanyIdInternal() : 0;
public int DriverId => CurrentUser != null ? CurrentUser.GetDriverIdInternal() : 0;
}
.NET [Authorize] - Use JWT Body Path Other than Role
First solution that comes to mind
You could play it via policies and use roles in the background. Here is an example. In your controller:
[Authorize(Policy = "Foo")]
public Whatever() {}
And then you register your policy like this:
services.AddAuthorization(options =>
{
options.AddPolicy("Foo", policy => policy.RequireClaim("www.bar.com/role", new[] { "Foo" }));
}
I.e. your "Foo" policy requires a user to have the value "Foo" for the claim "www.bar.com/role".
Digging deeper into the source
In fact if you add the Authorize
attribute with a role, not much more happens in the background as in the Policy example above. .net creates an authorization policy with a RolesAuthorizationRequirement
, and this requirement checks the roles by calling context.User.IsInRole
, which, in turn calls HasClaim(_identities[i].RoleClaimType, role)
on the ClaimsIdentity
. RoleClaimType
is set in .net ClaimsIdentity
to "roles/" by default.
So another way would probably be intercepting the creation of the ClaimsIdentity and calling it with another constructor which has a "roleType" parameter, which allows overriding of the default role claim name.
A probably better solution
So I've looked for the ClaimsIdentity and found this docu. It seems that you can get to it via token validation parameters:
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "www.bar.com/role"
}
})
Related Topics
How to Clear Event Subscriptions in C#
Xamarin Project Not Running, Assembly Not Found
When to Use a Sortedlist<Tkey, Tvalue> Over a Sorteddictionary<Tkey, Tvalue>
How to Sort Depended Objects by Dependency
How to Make a Wizard with ASP.NET MVC
Casting Object to Int Throws Invalidcastexception in C#
Algorithm for Intersection of 2 Lines
How to Get a Type's Alias Through Reflection
How to Solve Circular Reference
Why Is There Huge Performance Hit in 2048X2048 Versus 2047X2047 Array Multiplication
Return Content with Ihttpactionresult for Non-Ok Response
Multi Threading C# Application with SQL Server Database Calls
Why Use a Using Statement with a SQLtransaction
Is There a Serializable Generic Key/Value Pair Class in .Net