Claims Not Being Cleared on Logout - c#

I am using asp.net core api 2.2 and I am using token authentication.
I generate the tokens like this
Claim[] claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, employee.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,new DateTimeOffset(now).ToUnixTimeSeconds().ToString()),
new Claim(ClaimTypes.Role, allRoles)
// TODO: add additional claims here
};
var tokenExpirationMins = configuration.GetValue<int>("Auth:Jwt:TokenExpirationInMinutes");
SymmetricSecurityKey issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Auth:Jwt:Key"]));
JwtSecurityToken token = new JwtSecurityToken(
issuer: configuration["Auth:Jwt:Issuer"],
audience: configuration["Auth:Jwt:Audience"],
claims: claims,
notBefore: now,
expires: now.Add(TimeSpan.FromMinutes(tokenExpirationMins)),
signingCredentials: new SigningCredentials(
issuerSigningKey, SecurityAlgorithms.HmacSha256)
);
string encodedToken = new JwtSecurityTokenHandler().WriteToken(token);
I store the response data in LocalStorage and when I do an ajax request I send the "Bearer Access Token" to the server and then the built in Asp.net Identity / Core take care of authenticating it via the Authorize tags.
In the controller I have something like this
[AllowAnonymous]
[HttpGet("GetItems")]
public IActionResult GetItems()
{
var isMember = User.FindFirst(ClaimTypes.NameIdentifier) != null;
return OK();
}
Now this method is used by logged in users and logged out users. Basically the code is all the same except if they are a "member" they see a bit more data.
Now what I noticed is that when a "member" would login and go to this area and gets the "Items" back with all the fields shown and then logout and go back to that page the line of code that checks if they got a Claim will still be populated.
var isMember = User.FindFirst(ClaimTypes.NameIdentifier) != null;
If I where to refresh the page and go back to it then this would be null. For whatever reason it still holding onto the claims.
This is leading to them see all fields when they should only be seeing a reduced amount of fields as they logged out.
Do I have to clear something else on logout?

Related

ASP.NET Core 5 Authorization Policy not detecting claim

I'm creating a web API with ASP.NET Core 5 and Identity Framework to handle users. I'm trying to add an authorization policy that requires a user to have a "canEdit" claim. In my Startup Class, I've added the following code in the ConfigureServices method:
services.AddAuthorization(options =>
{
options.AddPolicy("IsUser", policy => policy.RequireAuthenticatedUser());
options.AddPolicy("CanEdit", policy => policy.RequireClaim("canEdit"));
}
In my Configure class I have:
// some middleware
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
});
I've used this policy in my controller like so:
[HttpGet("edit")]
[Authorize(Policy = "CanEdit")]
public async Task<IActionResult> EditSomething()
{
\\Stuff here
}
The "IsUser" policy works, but the "CanEdit" policy doesn't. I've got a user with the "canEdit" claim in the AspNetUserClaims table, however when I try to login with this user, I still get a 403 Forbidden response. I'm using JSON web tokens for authentication. I've tried adding a "canEdit" claim to the list of claims in the JWT, thinking that maybe the claim is detected in the token, but this also did not work. I've checked that the user has the claim in the first place, by using
var userClaims = await _userManager.GetClaimsAsync(currentUser);
and the correct claim shows. I tried adding the policy to other methods in other controllers, and I checked capitalisation and different names for the claim, with no luck.
What did I miss here to keep the "CanEdit" policy from working?
Edit:
Here's what I have for creating JWTs, if it helps:
public async Task<string> CreateToken(AppUser user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["jwtSecret"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.DisplayName),
new Claim(ClaimTypes.Email, user.Email)
};
var userClaims = await _userManager.GetClaimsAsync(user);
claims.AddRange(userClaims);
var tokenDescriptor = new SecurityTokenDescriptor{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
Thanks to #TarikGen's suggestion, I checked the claims in HttpContext.User and managed to see the issue.
The claim I had in the AspNetUserClaims table for "canEdit" actually had a claim type of "role", which was the root of the problem. All I had to do was change the claim type to "canEdit" to fix the issue.
Extra note:
I did notice because of my mistake that "role" seems to be a keyword for the claim type, since the list of Claims in HttpContext.User shows a claim type of "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", instead of just "role". Upon checking, I found that, had I made my policy to RequireClaim("role"), I would have been unauthorized because of this.

How do I persist multiple ClaimTypes.Role values in User's Claims list?

I'm building a ASP.NET Core (v3.1) module and I just managed to configure an OpenIdConnect Authentication. Now, I need to get all User's roles from an API in order to give or deny access to them, then I added multiple Claim values to the same Claim role "ClaimTypes.Role" in the User's Claims list through OnAuthorizationCodeReceived Event like so:
OnAuthorizationCodeReceived = async (context) =>
{
// Uses the authentication code and gets the access and refresh token
var client = new HttpClient();
var response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest()
{
Address = urlServer + "/connect/token",
ClientId = "hybrid",
Code = context.TokenEndpointRequest.Code,
RedirectUri = context.TokenEndpointRequest.RedirectUri,
}
if (response.IsError) throw new Exception(response.Error);
var identity = new ClaimsIdentity(context.Principal.Identity);
var listRoles = GenericProxies.RestGet<List<string>>(urlGetRoles, response.AccessToken); // GET request to API
listRoles.ForEach(role => identity.AddClaim(new Claim(ClaimTypes.Role, role)));
context.HttpContext.User = new ClaimsPrincipal(identity);
context.HandleCodeRedemption(response.AccessToken, response.IdentityToken);
}
While debugging, I noticed all roles are added the the User's Claims list after this line:
context.HttpContext.User = new ClaimsPrincipal(identity);
But, apparently, in my Home controller (that is where the user is being redirected to, after authenticated), when I access HttpContext.User, I can't seem to find any of the roles I added before except for "Admin" (which I'm guessing is a default ClaimTypes.Role value).
[Authorize]
public IActionResult Index()
{
if (User.IsInRole("SomeRole"))
{
return RedirectToAction("SomeAction", "SomeController");
}
else
{
return RedirectToAction("Forbidden", "Error");
}
}
Reading some other posts forums and topics, I found that this is probably a context persistence issue, which I tried to solve with this code in my Account controller:
public async Task Login(string returnUrl = "/")
{
await HttpContext.ChallengeAsync(
"OIDC",
new AuthenticationProperties
{
AllowRefresh = false,
IsPersistent = true,
RedirectUri = returnUrl
});
}
Some examples said that I could use context.Principal.AddIdentity(identity); in order to persist the new Claims list, but then I got the following error:
InvalidOperationException: only a single identity supported
IdentityServer4.Hosting.IdentityServerAuthenticationService.AssertRequiredClaims(ClaimsPrincipal principal)
Summing up, I must find a way to persist the role claims I added to the User's Claims list but I got no success until now.
Update on that, if that's useful to anyone.
I deduced that the problem whas with context.HttpContext.User = new ClaimsPrincipal(identity);, which I understood previously to be the part of the code that handled the persistence of the new claims.
Actually, I did noticed there was a context.Principal attribute of the type ClaimsPrincipal, and looked like it was the actual Current context, so I digged into it and tried to figure out a way to add elements to its "IEnumerable<Claim> Claims" attribute which is readonly.
After a while, I found the following solution that worked just fine for me:
Instead of
var identity = new ClaimsIdentity(context.Principal.Identity);
var listRoles = GenericProxies.RestGet<List<string>>(urlGetRoles, response.AccessToken); // GET request to API
listRoles.ForEach(role => identity.AddClaim(new Claim(ClaimTypes.Role, role)));
I tried
var identity = context.Principal.Identity as ClaimsIdentity;
if(identity != null)
{
var listRoles = GenericProxies.RestGet<List<string>>(urlGetRoles, response.AccessToken); // GET request to API
foreach (var role in listRoles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}

Password Updates Automatically

Asp.Net Web API with .net core is updating the password automatically on JWT token generation.
So firstly, I had an MVC5 application with asp net membership tables, and wanted to create an API for the same with .net core.
And to support both MVC5 Web APP and WEB API. I added four more columns for AspNetUsers table (ConcurrencyStamp, LockoutEnd, NormalizedEmail, NormalizedUserName).
Although I'm able to get JWT token without any issues, it's also updating the password each time I generate the JWT token which is not allowing users to login from MV5 web APP.
Below is the JWT generate token code
[Route("login")] // /login
[HttpPost]
public async Task<ActionResult> Login([FromBody] LoginViewModel
model)
{
try
{
var user = await
_userManager.FindByNameAsync(model.Username);
if (user != null && await
_userManager.CheckPasswordAsync(user, model.Password))
{
var claim = new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.Id)
};
var signinKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["Jwt:SigningKey"]));
int expiryInMinutes =
Convert.ToInt32(_configuration["Jwt:ExpiryInMinutes"]);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Site"],
claims: claim,
audience: _configuration["Jwt:Site"],
expires: DateTime.UtcNow.AddMinutes(expiryInMinutes),
signingCredentials: new SigningCredentials(signinKey,
SecurityAlgorithms.HmacSha256)
);
return Ok(
new
{
token = new
JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo,
userName = user.UserName
});
}
return Unauthorized();
}
catch (Exception ex)
{
return Unauthorized();
}
}
Please let me know how to stop updating the PasswordHash and SecurityStamp column in AspNetUsers on generating JWT token.
Update: CheckPasswordAsync(used in web API) method is updating the password field and PasswordSignInAsync method is used in web app
#KirkLarin, thanks a lot and it helped me to solve the problem by adding the below code in StartUp.cs file under Configure service method
public void ConfigureServices(IServiceCollection services)
{
services.Configure<PasswordHasherOptions>(options => options.CompatibilityMode =
PasswordHasherCompatibilityMode.IdentityV2);
}

Authorization Issue with JWT token creation Method (User Roles)

Below I have a working JWT token creation method which will work without the authorization tag at the top of the method. I have also created User Roles which was working before I previously implemented the token. I believe that since the token implementation method I have been unable to configure Authorization by role and instead the code thinks that any authorization requires the JWT token. When I pass the token into postman the authentication will work under simple [Authorize]. But I need the create token method to be restricted so that only registered users can use it.
[Authorize(Roles = "Users")]
[HttpPost("api/auth/token")]
public async Task<IActionResult> CreateToken([FromBody]
CredentialViewModel model)
{
try
{
var user = await userManager.FindByNameAsync(model.UserName);
if (user != null)
{
if (_hasher.VerifyHashedPassword(user, user.PasswordHash, model.Password) == PasswordVerificationResult.Success)
{
// Get the claims from the user
var userClaims = await userManager.GetClaimsAsync(user);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, user.APIKey.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email??"")
}.Union(userClaims);
//*********************************
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Tokens:Issuer"],
audience: _config["Tokens:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddDays(10),
signingCredentials: creds
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
}
}
catch (Exception ex)
{
_logger.LogError($"Exception thrown while creating JWT: {ex}");
}
return BadRequest();
}
example json input
{
"username" : "user02",
"password" : "test123"
}
Make sure to set the RoleClaimType value properly where you verify the JWT tokens, or in your Startup.cs file.
using System.Security.Claims;
....
var tokenValidationParameters = new TokenValidationParameters
{
......
.....
RoleClaimType = ClaimTypes.Role
};

Generate Identity from bearer token

Is there a way to take a Bearer Token string and convert it to the Identity object manually in asp.net?
Cheers,
Aziz
This is a pretty old question, but I think answer was still missing. I was able to regenerate Principal by using the following line
var ticket = Startup.OAuthOptions.AccessTokenFormat.Unprotect(accessToken);
var identity = ticket.Identity;
First you need to crate some claims based on token then create ClaimsIdentity and use it to authorize the user.
public ActionResoult Login(string token)
{
if(_tokenManager.IsValid(token))
{
// optionally you have own user manager which returns roles and user name from token
// no matter how you store users and roles
var user=_myUserManager.GetUserRoles(token);
// user is valid, going to authenticate user for my App
var ident = new ClaimsIdentity(
new[]
{
// adding following 2 claim just for supporting default antiforgery provider
new Claim(ClaimTypes.NameIdentifier, token),
new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"),
// an optional claim you could omit this
new Claim(ClaimTypes.Name, user.Username),
// populate assigned user's role form your DB
// and add each one as a claim
new Claim(ClaimTypes.Role, user.Roles[0]),
new Claim(ClaimTypes.Role, user.Roles[1]),
// and so on
},
DefaultAuthenticationTypes.ApplicationCookie);
// Identity is sign in user based on claim don't matter
// how you generated it
HttpContext.GetOwinContext().Authentication.SignIn(
new AuthenticationProperties { IsPersistent = false }, ident);
// auth is succeed, just from a token
return RedirectToAction("MyAction");
}
// invalid user
ModelState.AddModelError("", "We could not authorize you :(");
return View();
}
Now you could use Authorize filter as well:
[Authorize]
public ActionResult Foo()
{
}
// since we injected user roles to Identity we could do this as well
[Authorize(Roles="admin")]
public ActionResult Foo()
{
// since we injected our authentication mechanism to Identity pipeline
// we have access current user principal by calling also
// HttpContext.User
}
Also I encourage you to have look Token Based Authentication Sample from my github repo as a very simple working example.
The token just holds claims and it's just used for authentication into the resource. If one of those claims held user information you could create an identity and assign the claims to it.
public void ValidateBearerToken(OwinContext context)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
byte[] securityKey = GetBytes("some key"); //this should come from a config file
SecurityToken securityToken;
var validationParameters = new TokenValidationParameters()
{
ValidAudience = "http://localhost:2000",
IssuerSigningToken = new BinarySecretSecurityToken(securityKey),
ValidIssuer = "Self"
};
var auth = context.Request.Headers["Authorization"];
if (!string.IsNullOrWhiteSpace(auth) && auth.Contains("Bearer"))
{
var token = auth.Split(' ')[1];
var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);
context.Request.User = principal;
}
}
catch (Exception ex)
{
var message = ex.Message;
}
}

Categories

Resources