How to mock "aud" claim in c# Moq - c#

I am trying to write unit tests for authentication logic implemented using Azure AD client credentials flow using MOQ.
The first test case is to check that if the "Audience" is valid. I am trying to mock claim to set up the "aud" or "appId" claims using ClaimTypes but not able to find anything like ClaimTypes.Aud
var identity = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.Name, "Sahil")
});
var mockPrincipal = new Mock<ClaimsPrincipal>(identity);
mockPrincipal.Setup(x => x.Identity).Returns(identity);
mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);
How can I set up the "aud" and "appId" claims in C#
OR
Just setup mockPrincipal so that when it tries to check if "aud" is valid it returs false.
I am trying to write unit tests for the below code.
public void Authenticate(JwtBearerOptions options)
{
_configuration.Bind("AzureAD", options);
options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidateIssuerSigningKey = true;
options.TokenValidationParameters.ValidateIssuer = true;
options.Events ??= new JwtBearerEvents();
var existingHandlers = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
string appId = GetAppIdFromToken(context);
bool isAllowed = await CheckAppIdIsAllowedAsync(context, appId);
if (isAllowed)
{
_logger.LogInformation($"[{nameof(Authenticate)}] AppId in allow list");
}
else
{
_logger.LogError($"[{nameof(Authenticate)}] AppId {appId} not in allowed list");
}
await Task.CompletedTask.ConfigureAwait(false);
};
options.Events.OnTokenValidated += existingHandlers;
}
private string GetAppIdFromToken(TokenValidatedContext context)
{
string appId = context.Principal.Claims.FirstOrDefault(x => x.Type == "appid" || x.Type == "azp")?.Value;
return appId;
}
private async Task<bool> CheckAppIdIsAllowedAsync(TokenValidatedContext context, string appId)
{
IEnumerable<string> AllowedApps = _configuration.GetSection("AllowedAppPrincipals").Get<string[]>();
var FoundAppId = AllowedApps.FirstOrDefault(a => a == appId);
if (FoundAppId == null)
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
context.Response.ContentType = "application/json";
const string message = "{\"error\" : \"Unacceptable app principal\"}";
byte[] arr = Encoding.ASCII.GetBytes(message);
await context.Response.BodyWriter.WriteAsync(arr);
context.Fail(message);
return false;
}
return true;
}
How to mock aud and appId claims with Moq?

I tried to reproduce how to get audience invalid in my environment.
It usually happens when the issuer endpoint is different or scope is not given correctly or different scope than what is intended is mentioned.
https://login.microsoftonline.com/xx/oauth2/v2.0/token
Here i gave scope for api other than microsoft graph.
But next tep i am calling graph endpoint and so i am getting invalid audience error .
https://graph.microsoft.com/v1.0/users/xxx
To check for mocking test , you can consider below point as direct claims for "aud" audience in c# couldn't be obtained AFAIK.
Aud claim is Application ID URI or GUID Identifies the intended
audience of the token. In v2.0 tokens, audience must be client ID
of the API whereas in v1.0 tokens, it can be the client ID or the
resource URI used in the request.
One way to validate it is to check following way with issuer/audience
You can give custom values according to the authorization endpoint
Code:
string AUDIENCE = "<GUID of your Audience according to the app>";
string TENANT = "<GUID of your Tenant>";
private static async Task<SecurityToken> validateJwtTokenAsync(string token)
{
// URL based on your AAD-TenantId
var stsDiscoveryEndpoint = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", TENANT);
//To Get tenant information
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint)
// Get Config from AAD:
var config = await configManager.GetConfigurationAsync();
// Validate token:
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidAudience = AUDIENCE,
ValidIssuer = config.Issuer,
IssuerSigningTokens = config.SigningTokens,
CertificateValidator = X509CertificateValidator.ChainTrust,
};
var validatedToken = (SecurityToken)new JwtSecurityToken();
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return validatedToken;
}
Or
var TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,
....
ValidIssuer = configuration["JwtAuthentication:Issuer"],
ValidAudience = configuration["JwtAuthentication:Audience"]
};
So from validation parameters, you can get if that we that audience was valid or not, it majorly occurs when issuer is different from what we expected or when scopes are not correct.
You can try get those validate as claims to check for mocking
Snippent below taken from TokenValidationParameters.AudienceValidator, System.IdentityModel.Tokens C# (CSharp) Code Examples - HotExamples
public virtual ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken
validatedToken)
{
if (string.IsNullOrWhiteSpace(securityToken))
{
throw new ArgumentNullException("securityToken");
}
if (validationParameters == null)
{
throw new ArgumentNullException("validationParameters");
}
if (validationParameters.ValidateAudience)
{
if (validationParameters.AudienceValidator != null)
{
if
(!validationParameters.AudienceValidator(jwt.Audiences, jwt,
validationParameters))
{
throw new SecurityTokenInvalidAudienceException(string.Format(CultureInfo.InvariantCulture,
ErrorMessages.IDX10231, jwt.ToString()));
}
}
else
{
this.ValidateAudience(jwt.Audiences, jwt,
validationParameters);
}
}
ClaimsIdentity identity = this.CreateClaimsIdentity(jwt, issuer,
validationParameters);
if (validationParameters.SaveSigninToken)
{
identity.BootstrapContext = new
BootstrapContext(securityToken);
}
validatedToken = jwt;
return new ClaimsPrincipal(identity);
}
Also check Reference :c# - How to mock ConfigurationManager.AppSettings with moq - Stack Overflow

Related

JWT Refresh Token not working properly when trying refresh after long time?

I am create a JWT access token and refresh token on login of valid user, access token is short lived and refresh token is with expiration time of 7 days, When I am trying to generate new access token after expiry using refresh token it is working fine and response with new access token and refresh token but after long time such as after 3 or 4 hours when I am trying it is not working. I am also comment in Refresh token method code where I am getting error.
Please see my code:
Controller:
public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(request.RefreshToken))
{
return Unauthorized();
}
var jwtResult = _jwtAuthManager.Refresh(request.RefreshToken, request.AccessToken, DateTime.Now);
var userName = jwtResult.RefreshToken.UserName;
var role = _userService.GetUserRole(userName);
var claims = new[]
{
new Claim(ClaimTypes.Role, role)
};
_logger.LogInformation($"User [{userName}] has refreshed JWT Token");
if (jwtResult == null)
{
return BadRequest();
}
return Ok(new
{
UserName = userName,
Role= role,
AccessToken = jwtResult.AccessToken,
RefreshToken = jwtResult.RefreshToken.TokenString,
Status = "Success",
Message = "New access token generated successfully"
});
}
catch (SecurityTokenException e)
{
return Unauthorized(e.Message); // return 401 so that the client side can redirect the user to login page
}
}
Generate token method:
public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now)
{
var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims?.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value);
var jwtToken = new JwtSecurityToken(
_jwtTokenConfig.Issuer,
shouldAddAudienceClaim ? _jwtTokenConfig.Audience : string.Empty,
claims,
expires: now.AddMinutes(_jwtTokenConfig.AccessTokenExpiration),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature));
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken);
var refreshToken = new RefreshToken
{
UserName = username,
TokenString = GenerateRefreshTokenString(),
ExpireAt = now.AddMinutes(_jwtTokenConfig.RefreshTokenExpiration),
};
_usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (s, t) => refreshToken);
return new JwtAuthResult
{
AccessToken = accessToken,
RefreshToken = refreshToken
};
}
Refresh Token Method:
public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now)
{
var (principal, jwtToken) = DecodeJwtToken(accessToken);
if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature))
{
throw new SecurityTokenException("Invalid token");
}
var userName = principal.Identity?.Name;
if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken))
{
throw new SecurityTokenException("Invalid token not found");
}
var result = existingRefreshToken;
if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt <= now) //After 3 or 4 hours I am getting error in this condition.
{
throw new SecurityTokenException("Invalid UserName or refresh token expired");
}
return GenerateTokens(userName, principal.Claims.ToArray(), now); // need to recover the original claims
}
Claim Principal Method:
public (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new SecurityTokenException("Invalid token");
}
var principal = new JwtSecurityTokenHandler()
.ValidateToken(token,
new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _jwtTokenConfig.Issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(_secret),
ValidAudience = _jwtTokenConfig.Audience,
ValidateAudience = true,
ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(1)
},
out var validatedToken);
return (principal, validatedToken as JwtSecurityToken);
}

403 Error on JWT Role Based Authorization using .NET Core Web API 3.1

I am getting 403 Forbidden while trying to implement Role based JWT Authorization using .NET Core Web API 3.1 version. Below is how my code looks like:
// API
[HttpGet, Route("GetAll")]
[Authorize(Roles = "Admin")]
public IEnumerable<Users> GetAllUsers(string environment) {}
// JWT Token Generation
public UserDetail GenerateToken(string userName, string password)
{
string key = Configuration["Jwt:Key"];
var issuer = Configuration["Jwt:Issuer"];
var audience = Configuration["Jwt:Audience"];
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
IdentityModelEventSource.ShowPII = true;
//Create a List of Claims, Keep claims name short
var roleClaims = GetRoleClaimsFor(user);
//Create Security Token object by giving required parameters
var token = new JwtSecurityToken(issuer,
audience: audience,
claims: roleClaims,
expires: DateTime.Now.AddMinutes(15),
signingCredentials: credentials);
//Generate JWT Token
var userToken = new JwtSecurityTokenHandler().WriteToken(token);
}
// StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = Configuration["Jwt:Policy"];
}).AddJwtBearer(Configuration["Jwt:Policy"], options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
// JWT Authorization configuration
services.AddAuthorization(auth =>
{
auth.AddPolicy(Configuration["Jwt:Policy"], new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireClaim(ClaimTypes.Name, Configuration["Jwt:RequiredClaim"]).Build());
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
}
private IList<Claim> GetRoleClaimsFor(UserPrincipal user)
{
var roleClaims = new List<Claim>();
UserRoles = new List<string>();
// Pull only groups where user belongs to
PrincipalSearchResult<Principal> groups = user.GetGroups();
// Filter user groups related to GBITs application alone
var gbitsGroups = (from grp in groups
select grp);
// Loop through gbitsGroups and assign the related role
foreach (GroupPrincipal gbitsGrp in gbitsGroups)
{
try
{
// Pull UserGroup related Permissions
var roles = _userGroupPermissionRepository.GetUserGroupPermissionFor(gbitsGrp.Name);
// Loop through UserGroupPermissions to create Claims
foreach (var role in roles)
{
var claim = new Claim(role.UserGroup.GroupName, role.Permission.PermissionName);
roleClaims.Add(claim);
}
// Load Roles collection
IList<string> usrRoles = (from role in roles
select role.Permission.PermissionName).ToList();
UserRoles.AddRange(usrRoles);
}
catch (Exception ex)
{
throw ex;
}
}
return roleClaims;
}
I am able to run and see all the roles as claims from Principal.Identity after validating the token. But the Roles are not working as expected.
Creating the claims as ClaimsType.Role instead of custom string solved the issue. Below is the refactored GetClaimsFor method
private IList<Claim> GetRoleClaimsFor(UserPrincipal user)
{
var roleClaims = new List<Claim>();
UserRoles = new List<string>();
// Pull only groups where user belongs to
PrincipalSearchResult<Principal> groups = user.GetGroups();
// Filter user groups related to GBITs application alone
var gbitsGroups = (from grp in groups
select grp);
// Loop through gbitsGroups and assign the related role
foreach (GroupPrincipal gbitsGrp in gbitsGroups)
{
try
{
// Pull UserGroup related Permissions
var roles = _userGroupPermissionRepository.GetUserGroupPermissionFor(gbitsGrp.Name);
// Loop through UserGroupPermissions to create Claims
foreach (var role in roles)
{
var claim = new Claim(ClaimsType.Role, role.Permission.PermissionName);
roleClaims.Add(claim);
}
// Load Roles collection
IList<string> usrRoles = (from role in roles
select role.Permission.PermissionName).ToList();
UserRoles.AddRange(usrRoles);
}
catch (Exception ex)
{
throw ex;
}
}
return roleClaims;
}

Validation of PS256 encoded JWT using C#

I've been tasked to validate a JWT token that's been encoded using the PS256 algorithm and for the last two days I've been having trouble with it. I lack knowledge on this subject and I've been chipping away slowly at the problem trying different solutions.
// Encoded
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpZDEyMzQifQ.eyJpc3MiOiJmb28uYmFyLnRlc3Rpc3N1ZXIiLCJleHAiOjE1NTEyMDEwNjgsImF0X2hhc2giOiJqaFl3c1pyTnZ0dFNYQnR6QVMtWlNnIn0.yJePyxdJWyydG4HM97oQag6ulGKa5Afw-LHYYEXz7lVy8v0IJD0mSO9WtowlWJIeD2Vvthuj71XUfHsgz0LD9rK0VBucJbd_OiIXpbwPUqBcdj82DNLFXDJfCJnUC-Rv8QP7OUVBvLjvBQ6WYMrx1Qnq8xP6qeL_ohKwRmo6EDhZRkYBz9gFhfha1ZlKcnyR73nXdShwy7OmmyiRvVWPBf_GgSsfz8FNNoKySW1MA4tRa7cl3zPlyCnWyLaZ3kcQsmTqarHG--YXSDF5ozZ_Sx6TkunCxrOYzOFNcPyeIWqI84cemM6TgMBw9jhzMCk7Y4Fzxe5KEYJH4GlGA4s4zg
// Header
{
"alg": "PS256",
"typ": "JWT",
"kid": "kid1234"
}
// Payload
{
"iss": "foo.bar.testissuer",
"exp": 1551201068,
"at_hash": "jhYwsZrNvttSXBtzAS-ZSg"
}
I have a working implementation for RS256 encoded JWT which is using the JWTSecurityTokenHandler provided in Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt. For the RS256 implementation I have a IssuerSigningKeyResolver that is making custom checks for the kid and supplying the public key
var tokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = issuer,
ValidateLifetime = true,
RequireSignedTokens = true,
RequireExpirationTime = true,
ValidateAudience = false,
ValidateIssuer = true,
IssuerSigningKeyResolver = (string token, SecurityToken securityToken, string kid, TokenValidationParameters validationParameters) =>
{
// Custom kid checks
var rsa = RSA.Create();
rsa.ImportParameters(new RSAParameters
{
Exponent = Base64UrlEncoder.DecodeBytes(matchingKid.E),
Modulus = Base64UrlEncoder.DecodeBytes(matchingKid.N),
});
latestSecurityKeys.Add(matchingKid.Kid, new RsaSecurityKey(rsa));
var securityKeys = new SecurityKey[1]
{
new RsaSecurityKey(rsa)
};
return securityKeys;
}
};
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
return true;
}
catch (SecurityTokenException ex)
{
// Do something with ex
return false;
}
This implementation is not working for PS256 encoded JWT. I debugged the JwtSecurityTokenHandler inside System.IdentityModel.Tokens.Jwt, but it seems that even though PS256 is in the supported algorithms list the verification fails.
I must state again that my knowledge on this subject is limited. From what I understand RSA256 and PS256 are in the same family of algorithms? Would I be better off to just create a custom validation of the PS256 JWT using another library like jose-jwt?
After raising the issue with Microsoft it seems that right now such validation is not supported by Microsoft.IdentityModel.Tokens and System.IdentityModel.Tokens.Jwt . Details can be found here - https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1117
In the end I validated my token using jose-jwt and some custom checks.
private bool IsValid(string token, string issuer, string configId)
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = jwtSecurityTokenHandler.ReadToken(token) as JwtSecurityToken;
// Extract the kid from token header
var kidHeader = jwtSecurityToken.Header.Where(k => k.Key.ToLower() == "kid")?.FirstOrDefault();
if (kidHeader?.Value == null) ThrowInvalidOperation($"Failed to find matching kid for Issuer: {issuer.ToLower() }");
var kid = kidHeader?.Value as string;
// Extract the expiration time from token payload
var expirationTime = jwtSecurityToken.Payload?.Exp;
if (expirationTime == null) ThrowInvalidOperation($"Failed to find matching expiration time for Issuer: {issuer.ToLower() }");
// Decode to verify signature
var verifiedToken = JWT.Decode(token, GetPublicKey(kid, issuer, providerId));
if (verifiedToken != null)
{
var json = JsonConvert.DeserializeObject<dynamic>(verifiedToken);
return IsValidIssuer(json, issuer) && IsValidExpirationTime(json, expirationTime);
}
else
{
return false;
}
void ThrowInvalidOperation(string msg) => throw new InvalidOperationException(msg);
}
private bool IsValidIssuer(dynamic json, string issuer)
{
if (json != null && issuer != null)
{
if (json["iss"] == issuer)
{
return true;
}
else
{
return false;
}
}
return false;
}
private bool IsValidExpirationTime(dynamic json, int? expTime)
{
if (json != null && expTime != null)
{
if (json["exp"] == expTime)
{
return true;
}
else
{
return false;
}
}
return false;
}
private RSA GetPublicKey(string kid, string validIssuer, string configId)
{
var openIdConfig = openIdConfigurationProvider.GetOpenIdConfiguration(configId);
var matchingKid = openIdConfig?.JsonWebKeySet?.Keys?.FirstOrDefault(x => x.Kid == kid);
if (matchingKid == null)
{
throw new InvalidOperationException($"kid is null");
}
var rsa = RSA.Create();
rsa.ImportParameters(new RSAParameters
{
Exponent = Base64UrlEncoder.DecodeBytes(matchingKid.E),
Modulus = Base64UrlEncoder.DecodeBytes(matchingKid.N),
});
return rsa;
}

ASP.NET Core JWT mapping role claims to ClaimsIdentity

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?)?
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":"admin#admin.site.com",
"Password":"ukj90ee",
"RememberMe":"false"
}
Debug response token https://jwt.io/#debugger-io
Payload data:
{
"sub": "admin#admin.site.com",
"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!
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 async Task<IActionResult> GetToken([FromBody]Credentials credentials)
{
// TODO: Add here some input values validations
User user = await _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.

WebApi calling Async method hangs on the inner Async call

I am calling Async method in a non-async method in the below way in WebApi authentication handler. And it hangs when it executes the inner external Async method. I need to set the claims before the subsequent code executes. So I am setting the return value to the Thread.CurrentPrincipal. Please advise.
I have tried the below and none of them worked.
Task.Run(() => Thread.CurrentPrincipal = this.ValidateTokenAsync(accessToken).GetAwaiter().GetResult()); - this works but the subsequent code execution does not wait on this and so the claims are not utilized there.
Thread.CurrentPrincipal = this.ValidateTokenAsync(accessToken).Result;
Task<ClaimsPrincipal> claimsPrincipalTask = this.ValidateTokenAsync(accessToken);
Task.WaitAll(claimsPrincipalTask);
Thread.CurrentPrincipal = Thread.CurrentPrincipal.GetAwaiter().GetResult();
Thread.CurrentPrincipal = this.ValidateTokenAsync(accessToken).GetAwaiter().GetResult();
private async Task<ClaimsPrincipal> ValidateTokenAsync(string accessToken)
{
LoggingUtilities.Logger.TraceInformation("Validating JWT.");
ClaimsPrincipal principal = ClaimsPrincipal.Current;
if (principal == null || principal.Identity == null || !principal.Identity.IsAuthenticated)
{
principal = await JwtValidator.ValidateTokenAsync(accessToken).ConfigureAwait(false);
}
return principal;
}
The JwtValidator.ValidateTokenAsync method:
public static async Task<ClaimsPrincipal> ValidateTokenAsync(string accessToken)
{
string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
string issuer = null;
string stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "{0}/.well-known/openid-configuration", authority);
List<SecurityToken> signingTokens = null;
try
{
// The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.
if (DateTime.UtcNow.Subtract(stsMetadataRetrievalTime).TotalHours > 24
|| string.IsNullOrEmpty(globalIssuer)
|| globalSigningTokens == null)
{
// Get tenant information that's used to validate incoming jwt tokens
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync().ConfigureAwait(false);
globalIssuer = config.Issuer;
globalSigningTokens = config.SigningTokens.ToList();
stsMetadataRetrievalTime = DateTime.UtcNow;
}
issuer = globalIssuer;
signingTokens = globalSigningTokens;
}
catch (Exception)
{
LoggingUtilities.Logger.TraceWarning("Failed to get signing tokens.");
throw;
}
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters
{
ValidAudience = audience,
ValidIssuer = issuer,
IssuerSigningTokens = signingTokens,
CertificateValidator = X509CertificateValidator.None
};
ClaimsPrincipal principal;
try
{
// Validate token.
SecurityToken validatedToken;
principal = tokenHandler.ValidateToken(
accessToken,
validationParameters,
out validatedToken);
}
catch (SecurityTokenValidationException)
{
LoggingUtilities.Logger.TraceWarning("Failed to validate the JWT.");
throw;
}
catch (Exception)
{
LoggingUtilities.Logger.TraceWarning("Failed to validate the JWT.");
throw;
}
return principal;
}

Categories

Resources