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?)?
In . NET Core, the ClaimsIdentity class represents a user in your application. It helps describe who they are and helps manage the list of claims which describe what they can do.
Claim based authorization checks are declarative - the developer embeds them within their code, against a controller or an action within a controller, specifying claims which the current user must possess, and optionally the value the claim must hold to access the requested resource.
You need get valid claims when generating JWT. Here is example code:
Login logic:
[HttpPost] [AllowAnonymous] public async Task<IActionResult> 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<List<Claim>> GetValidClaims(ApplicationUser user) { IdentityOptions _options = new IdentityOptions(); var claims = new List<Claim> { 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())); } }); }
ClaimPermission:
public static class ClaimPermission { public const string CanAddNewService = "Tự thêm dịch vụ", CanCancelCustomerServices = "Hủy dịch vụ khách gọi", CanPrintReceiptAgain = "In lại hóa đơn", CanImportGoods = "Quản lý tồn kho", CanManageComputers = "Quản lý máy tính", CanManageCoffees = "Quản lý bàn cà phê", CanManageBillards = "Quản lý bàn billard"; }
Use the similar snippet to get all pre-defined permissions and insert it to asp.net permission claims table:
var staffRole = await roleManager.CreateRoleIfNotExists(UserType.Staff); foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) { await roleManager.AddClaimIfNotExists(staffRole, 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?
This is my working code! ASP.NET Core 2.0 + JWT. Adding roles to JWT token.
appsettings.json
"JwtIssuerOptions": { "JwtKey": "4gSd0AsIoPvyD3PsXYNrP2XnVpIYCLLL", "JwtIssuer": "http://yourdomain.com", "JwtExpireDays": 30 }
Startup.cs
// ===== Add Jwt Authentication ======== JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims // jwt // get options var jwtAppSettingOptions = Configuration.GetSection("JwtIssuerOptions"); services .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(cfg => { cfg.RequireHttpsMetadata = false; cfg.SaveToken = true; cfg.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = jwtAppSettingOptions["JwtIssuer"], ValidAudience = jwtAppSettingOptions["JwtIssuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"])), ClockSkew = TimeSpan.Zero // remove delay of token when expire }; });
AccountController.cs
[HttpPost] [AllowAnonymous] [Produces("application/json")] public async Task<object> GetToken([FromBody] LoginViewModel model) { var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false); if (result.Succeeded) { var appUser = _userManager.Users.SingleOrDefault(r => r.Email == model.Email); return await GenerateJwtTokenAsync(model.Email, appUser); } throw new ApplicationException("INVALID_LOGIN_ATTEMPT"); } // create token private async Task<object> GenerateJwtTokenAsync(string email, ApplicationUser user) { var claims = new List<Claim> { new Claim(JwtRegisteredClaimNames.Sub, email), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id) }; var roles = await _userManager.GetRolesAsync(user); claims.AddRange(roles.Select(role => new Claim(ClaimsIdentity.DefaultRoleClaimType, role))); // get options var jwtAppSettingOptions = _configuration.GetSection("JwtIssuerOptions"); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var expires = DateTime.Now.AddDays(Convert.ToDouble(jwtAppSettingOptions["JwtExpireDays"])); var token = new JwtSecurityToken( jwtAppSettingOptions["JwtIssuer"], jwtAppSettingOptions["JwtIssuer"], claims, expires: expires, signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); }
Fiddler test GetToken
method. Request:
POST https://localhost:44355/Account/GetToken HTTP/1.1 content-type: application/json Host: localhost:44355 Content-Length: 81 { "Email":"[email protected]", "Password":"ukj90ee", "RememberMe":"false" }
Debug response token https://jwt.io/#debugger-io
Payload data:
{ "sub": "[email protected]", "jti": "520bc1de-5265-4114-aec2-b85d8c152c51", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "8df2c15f-7142-4011-9504-e73b4681fb6a", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "exp": 1529823778, "iss": "http://yourdomain.com", "aud": "http://yourdomain.com" }
Role Admin is worked!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With