ASP.NET Core JWT mapping role claims to ClaimsIdentity

匿名 (未验证) 提交于 2019-12-03 01:39:01

问题:

I want to protect ASP.NET Core Web API using JWT. Additionally, I would like to have an option of using roles from tokens payload directly in controller actions attributes.

Now, while I did find it out how to use it with Policies:

Authorize(Policy="CheckIfUserIsOfRoleX") ControllerAction()... 

I would like better to have an option to use something usual like:

Authorize(Role="RoleX") 

where Role would be automatically mapped from JWT payload.

{     name: "somename",     roles: ["RoleX", "RoleY", "RoleZ"] } 

So, what is the easiest way to accomplish this in ASP.NET Core? Is there a way to get this working automatically through some settings/mappings (if so, where to set it?) or should I, after token is validated, intercept generation of ClaimsIdentity and add roles claims manually (if so, where/how to do that?)?

回答1:

Sample - ASP.NET Core JWT

Consider this is the payload.

{ name:"somename", roles:["RoleX", "RoleY", "RoleZ"] } 

JWT MiddleWare

public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env,     ILoggerFactory loggerFactory) {     var keyAsBytes = Encoding.ASCII.GetBytes("mysuperdupersecret");      var options = new JwtBearerOptions     {         TokenValidationParameters =         {            IssuerSigningKey = new SymmetricSecurityKey(keyAsBytes)         }     };     app.UseJwtBearerAuthentication(options);      app.UseMvc();    }   } 

When I make a request to my API with the JWT created above, the array of roles in the roles claim in the JWT will automatically be added as claims with the type http://schemas.microsoft.com/ws/2008/06/identity/claims/role to my ClaimsIdentity.

You can test this by creating the following simple API method that returns the user’s claims:

public class ValuesController : Controller { [Authorize] [HttpGet("claims")] public object Claims() {     return User.Claims.Select(c =>     new     {         Type = c.Type,         Value = c.Value     }); } } 

So when I make a call to the /claimsendpoint above, and pass the JWT generated before, I will get the following JSON returned:

[ { "type": "name", "value": "someone" }, { "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "value": "RoleX" }, { "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "value": "RoleY"  }, { "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "value": "RoleZ"  }  ] 

Where this gets really interesting is when you consider that passing Roles to the [Authorize] will actually look whether there is a claim of type http://schemas.microsoft.com/ws/2008/06/identity/claims/role with the value of the role(s) you are authorizing.

This means that I can simply add [Authorize(Roles = "Admin")] to any API method, and that will ensure that only JWTs where the payload contains the claim roles containing the value of Admin in the array of roles will be authorized for that API method.

public class ValuesController : Controller { [Authorize(Roles = "Admin")] [HttpGet("ping/admin")] public string PingAdmin() {     return "Pong"; } } 

Now simply decorate the MVC controllers with [Authorize(Roles = "Admin")] and only users whose ID Token contains those claims will be authorized.

Ensure the roles claim of your JWT contains an array of roles assigned to the user, and you can use [Authorize(Roles = "???")] in your controllers. It all works seamlessly.



回答2:

You need get valid claims when generating JWT. Here is example code:

Login logic:

[HttpPost] [AllowAnonymous] public async Task Login([FromBody] ApplicationUser applicationUser) {     var result = await _signInManager.PasswordSignInAsync(applicationUser.UserName, applicationUser.Password, true, false);     if(result.Succeeded) {         var user = await _userManager.FindByNameAsync(applicationUser.UserName);          // Get valid claims and pass them into JWT         var claims = await GetValidClaims(user);          // Create the JWT security token and encode it.         var jwt = new JwtSecurityToken(             issuer: _jwtOptions.Issuer,             audience: _jwtOptions.Audience,             claims: claims,             notBefore: _jwtOptions.NotBefore,             expires: _jwtOptions.Expiration,             signingCredentials: _jwtOptions.SigningCredentials);         //...     } else {         throw new ApiException('Wrong username or password', 403);     } } 

Get user claims based UserRoles, RoleClaims and UserClaims tables (ASP.NET Identity):

private async Task> GetValidClaims(ApplicationUser user) {     IdentityOptions _options = new IdentityOptions();     var claims = new List         {             new Claim(JwtRegisteredClaimNames.Sub, user.UserName),             new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),             new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),             new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),             new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName)         };     var userClaims = await _userManager.GetClaimsAsync(user);     var userRoles = await _userManager.GetRolesAsync(user);     claims.AddRange(userClaims);     foreach (var userRole in userRoles)     {         claims.Add(new Claim(ClaimTypes.Role, userRole));         var role = await _roleManager.FindByNameAsync(userRole);         if(role != null)         {             var roleClaims = await _roleManager.GetClaimsAsync(role);             foreach(Claim roleClaim in roleClaims)             {                 claims.Add(roleClaim);             }         }     }     return claims; } 

In Startup.cs please add needed policies into authorization:

void ConfigureServices(IServiceCollection service) {    services.AddAuthorization(options =>     {         // Here I stored necessary permissions/roles in a constant         foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))         {             options.AddPolicy(prop.GetValue(null).ToString(), policy => policy.RequireClaim(ClaimType.Permission, prop.GetValue(null).ToString()));         }     }); } 

I am a beginner in ASP.NET, so please let me know if you have better solutions.

And, I don't know how worst when I put all claims/permissions into JWT. Too long? Performance ? Should I store generated JWT in database and check it later for getting valid user's roles/claims?



回答3:

For generating JWT Tokens we'll need AuthJwtTokenOptions helper class

public static class AuthJwtTokenOptions {     public const string Issuer = "SomeIssuesName";      public const string Audience = "https://awesome-website.com/";      private const string Key = "supersecret_secretkey!12345";      public static SecurityKey GetSecurityKey() =>         new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Key)); } 

Account controller code:

[HttpPost] public IActionResult GetToken([FromBody]Credentials credentials) {     // TODO: Add here some input values validations      User user = _userRepository.GetUser(credentials.Email, credentials.Password);     if (user == null)         return BadRequest();      ClaimsIdentity identity = GetClaimsIdentity(user);      return Ok(new AuthenticatedUserInfoJsonModel     {         UserId = user.Id,         Email = user.Email,         FullName = user.FullName,         Token = GetJwtToken(identity)     }); }  private ClaimsIdentity GetClaimsIdentity(User user) {     // Here we can save some values to token.     // For example we are storing here user id and email     Claim[] claims = new[]     {         new Claim(ClaimTypes.Name, user.Id.ToString()),         new Claim(ClaimTypes.Email, user.Email)     };     ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token");      // Adding roles code     // Roles property is string collection but you can modify Select code if it it's not     claimsIdentity.AddClaims(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));     return claimsIdentity; }  private string GetJwtToken(ClaimsIdentity identity) {     JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(         issuer: AuthJwtTokenOptions.Issuer,         audience: AuthJwtTokenOptions.Audience,         notBefore: DateTime.UtcNow,         claims: identity.Claims,         // our token will live 1 hour, but you can change you token lifetime here         expires: DateTime.UtcNow.Add(TimeSpan.FromHours(1)),         signingCredentials: new SigningCredentials(AuthJwtTokenOptions.GetSecurityKey(), SecurityAlgorithms.HmacSha256));     return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); } 

In Startup.cs add following code to ConfigureServices(IServiceCollection services) method before services.AddMvc call:

public void ConfigureServices(IServiceCollection services) {     // Other code here…      services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)         .AddJwtBearer(options =>         {             options.TokenValidationParameters = new TokenValidationParameters             {                 ValidateIssuer = true,                 ValidIssuer = AuthJwtTokenOptions.Issuer,                  ValidateAudience = true,                 ValidAudience = AuthJwtTokenOptions.Audience,                 ValidateLifetime = true,                  IssuerSigningKey = AuthJwtTokenOptions.GetSecurityKey(),                 ValidateIssuerSigningKey = true             };         });      // Other code here…      services.AddMvc(); } 

Also add app.UseAuthentication() call to ConfigureMethod of Startup.cs before app.UseMvc call.

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {     // Other code here…      app.UseAuthentication();     app.UseMvc(); } 

Now you can use [Authorize(Roles = "Some_role")] attributes.

To get user id and email in any controller you should do it like this

int userId = int.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);  string email = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value; 

Also userId can be retrived this way (this is due to claim type name ClaimTypes.Name)

int userId = int.Parse(HttpContext.User.Identity.Name); 

It's better to move such code to some controller extension helpers:

public static class ControllerExtensions {     public static int GetUserId(this Controller controller) =>         int.Parse(controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);      public static string GetCurrentUserEmail(this Controller controller) =>         controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value; } 

The same is true for any other Claim you've added. You should just specify valid key.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!