ASP.NET Core JWT mapping role claims to ClaimsIdentity - c#

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.

Related

How to mock "aud" claim in c# Moq

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

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;
}

IdentityServer4 custom AuthenticationHandler can't find all claims for a user

I am using the IdentityServer4 sample that uses Asp.Net Identity and EntityFramework.
I am trying to create group controls using custom policies based on claims/roles.
My problem is that when I try and get the users claims in the authorization handler the claims I am looking for are not returned.
Looking at the database in SSMS I find the claims/roles I created are in tables called "AspNetRoles", "AspNetRoleClaims", "AspNetUserClaims" along with the user I created being in "AspNetUsers" and keys for the user and role being in "AspNetUserRoles".
When I call to get the users claims for authorization the list of claims seems to come from the "IdentityClaims" table.
There doesn't seem to be a simple way to check for claims in "AspNetClaims" like there is for claims in "IdentityClaims" so I assume I've made an error somewhere.
I've looked around a fair bit for a solution and tried a fair few things but I can't find anything that works.
Below is the code i thought would me most relevant to the question along with some screenshots taken of the running code.
Any help would be much appreciated, thanks in advance.
Code
MvcClient.Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthorization(options =>
{
options.AddPolicy("AdminRights", policy =>
{
policy.Requirements.Add(new AdminRequirement());
policy.RequireAuthenticatedUser();
policy.AddAuthenticationSchemes("Cookies");
});
});
services.AddSingleton<IAuthorizationHandler, AdminRequirementHandler>();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token token"; // NEW CHANGE (token)
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("AdminPermission"); // NEW CHANGE
options.Scope.Add("offline_access");
});
}
MvcClient.Controllers.HomeController
[Authorize(Policy = "AdminRights")]
public IActionResult Administrator()
{
return View();
}
IdentityServerWithAspIdAndEF.Startup (Called last in Configure)
private async Task CreateSuperuser(IServiceProvider serviceProvider, ApplicationDbContext context)
{
//adding custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roleNames = { "Administrator", "Internal", "Customer" };
foreach (var roleName in roleNames)
{
//creating the roles and seeding them to the database
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (roleExist)
await RoleManager.DeleteAsync( await RoleManager.FindByNameAsync(roleName) );
var newRole = new IdentityRole(roleName);
await RoleManager.CreateAsync(newRole);
if(roleName == "Administrator")
await RoleManager.AddClaimAsync(newRole, new Claim("AdminPermission", "Read"));
}
//creating a super user who could maintain the web app
var poweruser = new ApplicationUser
{
UserName = Configuration.GetSection("UserSettings")["UserEmail"],
Email = Configuration.GetSection("UserSettings")["UserEmail"]
};
string UserPassword = Configuration.GetSection("UserSettings")["UserPassword"];
var _user = await UserManager.FindByEmailAsync(Configuration.GetSection("UserSettings")["UserEmail"]);
if (_user != null)
await UserManager.DeleteAsync( await UserManager.FindByEmailAsync(Configuration.GetSection("UserSettings")["UserEmail"]) );
var createPowerUser = await UserManager.CreateAsync(poweruser, UserPassword);
if (createPowerUser.Succeeded)
{
//here we tie the new user to the "Admin" role
await UserManager.AddToRoleAsync(poweruser, "Administrator");
await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Create"));
await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Update"));
await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Delete"));
}
}
IdentityServerWithAspIdAndEF.Config
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource() // NEW CHANGE
{
Name = "AdminPermission",
DisplayName = "Admin Permission",
UserClaims =
{
"AdminPermission",
}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
{
Scopes = // NEW CHANGE
{
new Scope("AdminPermission", "Admin Permission")
{
UserClaims = { "AdminPermission" }
}
}
}
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false, // NEW CHANGE (false)
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
"AdminPermission", // NEW CHANGE
},
AllowOfflineAccess = true,
},
// Other Clients omitted as not used.
};
}
}
IdentityServerWithAspIdAndEF.Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
string connectionString = Configuration.GetConnectionString("DefaultConnection");
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com";
options.ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "implicit";
options.SaveTokens = true;
// options.GetClaimsFromUserInfoEndpoint = true; // NEW CHANGE
// options.ResponseType = "code id_token token"; // NEW CHANGE
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
MvcClient.Authorization
public class AdminRequirementHandler : AuthorizationHandler<AdminRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement)
{
Console.WriteLine("User Identity: {0}", context.User.Identity);
Console.WriteLine("Role is 'Administrator'? : {0}", context.User.IsInRole("Administrator"));
Console.WriteLine("Identities of user:-");
foreach (var v in context.User.Identities)
{
Console.WriteLine("\tName: {0},\tActor: {1},\tAuthType: {2},\tIsAuth: {3}", v.Name, v.Actor, v.AuthenticationType, v.IsAuthenticated);
Console.WriteLine("\n\tClaims from Identity:-");
foreach (var c in v.Claims)
Console.WriteLine("\t\tType: {0},\tValue: {1},\tSubject: {2},\tIssuer: {3}", c.Type, c.Value, c.Subject, c.Issuer);
}
Console.WriteLine("Claims from other source:-");
foreach(Claim c in context.User.Claims)
{
Console.WriteLine("\t\tType: {0},\tValue: {1},\tSubject: {2},\tIssuer: {3}", c.Type, c.Value, c.Subject, c.Issuer);
}
Console.WriteLine("\n *** Starting Authroization. ***\n");
Claim
role = context.User.FindFirst("role"),
accessLevel = context.User.FindFirst("AdminPermission");
if (role == null)
Console.WriteLine("\tUser as no 'role' : '{0}'", role == null ? "null" : role.Value);
else
Console.WriteLine("\tUser has 'role' : '{0}'", role.Value);
if (accessLevel == null)
Console.WriteLine("\tUser has no claim 'AdminPermission' : '{0}'", accessLevel == null ? "null" : accessLevel.Value);
else
Console.WriteLine("\tUser has 'AdminPermission' : '{0}'", accessLevel.Value);
if (role != null && accessLevel != null)
{
if (role.Value == "Administrator" && accessLevel.Value == "Read")
context.Succeed(requirement);
}
else
Console.WriteLine("\n *** Authorization Failue. ***\n");
return Task.CompletedTask;
}
}
Data
ApiClaims :
ApiResources : api1
ApiScopeClaims : AdminPermission, ApiScopeId = 2
ApiScopes : api1, ApiResourceId = 1
: AdminPermission, ApiResourceId = 1
ApiSecrets :
AspNetRoleClaims : AdminPermission, Read, RoleId = b2f03...
AspNetRoles : Administrator, b2f03...
: Customer, 779f7...
: Internal, 10d5d...
AspNetUserClaims : AdminPermission, Create, UserId = 8ee62...
: AdminPermission, Update, UserId = 8ee62...
: AdminPermission, Delete, UserId = 8ee62...
AspNetUserLogins :
AspNetUserRoles : UserId = 8ee62..., RoleId = b2f03...
AspNetUsers : superuser#mail.com, Id = 8ee62...
AspNetUserTokens :
ClientClaims :
ClientCorsOrigins :
ClientGrantTypes : hybrid, ClientId = 1
: client_credentials, ClientId = 1
: client_credentials, ClientId = 2
: password, ClientId = 3
ClientIdPRestrictions :
ClientPostLogoutRedirectUris : http://localhost:5002/signout-callback-oidc, ClientId = 1
ClientProperties :
ClientRedirectUris : http://localhost:5002/signin-oidc, ClientId = 1
Clients : mvc, AllowAccessTokenViaBrowser = 1, AllowOfflineAccess = 1, RequireConsent = 0
: client ...
: ro.client ...
ClientScopes : openid, ClientId = 1
: profile, ClientId = 1
: AdminPermission, ClientId = 1
ClientSecrets : Type = SharedSecret
IdentityClaims : Id IdentityResourceId Type
1 1 sub
2 2 name
3 2 family_name
4 2 given_name
5 2 middle_name
6 2 nickname
7 2 preferred_username
8 2 profile
9 2 picture
10 2 website
11 2 gender
12 2 birthdate
13 2 zoneinfo
14 2 locale
15 2 updated_at
16 3 AdminPermission
IdentityResources : openid
: profile
: AdminPermission
PersistedGrants : [8x] Type = refresh_token
Images
Autos at the beginning of the AuthorizationHandler
Claims shown on the identity server
Claims shows on the MVC client
Log from when an authenticate user attempts to access
JWT received by MVC client
Database tables
--EDIT--
I've Forked your code and solved the issue.
Here is a link to my repo.
https://github.com/derekrivers/IdentityServer4
In order to fix your solution, i changed the server authentication response type too :-
"code Id_token"
In the MVC Client setup in your config.cs I added the following properties :-
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true
I've also removed the adminpermission scope from the mvc client, as it isn't required.
I've also amended the AdminRequirementHandler.cs slightly, but i will let you explore that in my repo.
Basically, We have ensured that the user claims are in the Identity token, and by doing this they are then accessible within you AdminRequirementHandler
Hope this helps.

How to add custom headers to an open id connect authentication request in OWIN middleware

I have a system where I have an MVC website calling a web api. I have used OAUTH and OPENID Connect for my authentication/authorization. I have setup an identity server in another web api project using Thinktecture's IdentityServer3. In the MVC project I am doing the redirect to the identity server in the OWIN Startup class. This is all pretty standard stuff and is working fine.
I have now been asked to put the web api and the identity server behind Azure API Management. This also, is fine as all I need to do from my MVC project is add my subscription key from API management as a header ("Ocp-Apim-Subscription-Key") to any request to the web api. So far so good.
The problem is I now need to add this header to any requests to the identity server and I can't work out how, other than writing my own middleware. My Startup class looks like this:
public class Startup
{
public void Configuration(IAppBuilder app)
{
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(IocHelper.GetContainer()));
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationHelper.GetClientId(),
Authority = ConfigurationHelper.GetSecurityTokenServiceUrl(),
RedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
PostLogoutRedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
ResponseType = "code id_token",
Scope = "openid profile public_api",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(ConfigurationHelper.GetTokenEndpointUrl(), ConfigurationHelper.GetClientId(), ConfigurationHelper.GetClientSecret());
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(ConfigurationHelper.GetUserInfoUrl());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
if (userInfoResponse.IsError)
{
throw new Exception(userInfoResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
foreach (var c in userInfoResponse.Claims)
{
id.AddClaim(new Claim(c.Type, c.Value));
}
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn * 2).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
var claimsIdentity = new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
//claimsIdentity.IsAuthenticated = true;
n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
{
return Task.FromResult(0);
}
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
// DOESN'T WORK
n.OwinContext.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
// ALSO DOESN'T WORK
n.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
return Task.FromResult(0);
}
}
});
}
}
Is there a way to hi-jack the request to the identity server and add my own header in there?

Categories

Resources