Using OWIN Identity v2 Claims to track custom properties in WebAPI2 app - c#

getting my head wrapped around the new Identity framework and am trying to figure out how best to handle custom user properties. I have tried extending the IdentityUser, which works to store the information, but so far is requiring an additional db call to get the property back out. I am looking at switching to using claims to store/retrieve this information.
First, the specific prop I want to store/retrieve is not unique to an individual user (many to one). Consider grouping users together in a custom Group structure. I want to store the GroupId for use in other related entities.
I am able to store the GroupId (currently using the ClaimTypes.NameIdentifier which I don't think it the correct usage for that type, but...). But, when I go to retrieve that value, the claim type isn't found in the claims collection. It's in the db, so I know it's there. I'm missing something.
FWIW: Since it's WebAPI, I'm not using a traditional sign-in. I'm using token auth.
When I create the user, I have something like:
public async Task<IdentityResult> CreateUserAsync(string email, string password, string groupId)
{
var userId = ObjectId.GenerateNewId(DateTime.UtcNow).ToString(); // yes, it's a NoSQL store
var user = new ApplicationUser
{
Id = userId,
UserName = email
};
var claim = new IdentityUserClaim { ClaimType = ClaimTypes.NameIdentifier, ClaimValue = groupId, UserId = userId, Id = ObjectId.GenerateNewId(DateTime.UtcNow).ToString() };
user.Claims.Add(claim);
var result = await _UserManager.CreateAsync(user, password);
return result;
}
That creates what looks to be an appropriate db entry.
When I retrieve the value, I get null reference errors. Here's that code via an extension method:
public static string GetGroupId(this IIdentity identity)
{
var claimsIdentity = identity as ClaimsIdentity;
return claimsIdentity == null ? "" : claimsIdentity.FindFirst(ClaimTypes.NameIdentifier).Value;
}
The error hits when trying to get Value as the FindFirst is returning a null value.
Any hints or better/best practices here would be appreciated! Honestly, I'd prefer to just store this on the ApplicationUser : IdentityUser object, but I can't find a simple way of retrieving that of User.Identity in my api controller context without an additional call to the db.

Your gut feeling about storing extra data as a claim is correct, but implementation is a bit broken.
I recommend to have your own claim types created for your domain information. Do not reuse claim types provided from framework. Reason for that is ClaimTypes.NameIdentifier represents User.Id.
The framework itself adds standard list of claims to all users:
User.Id => represented as ClaimTypes.NameIdentifier
Username => represented as 'ClaimTypes.Name'
ProviderName => represented as ClaimTypes.ProviderName (not 100% sure about this one); Usually value is "ASP.NET Identity"
SecurityStamp value (not sure what the claim type name for it)
All the roles assigned to the user are stored as ClaimTypes.Role
So in your case you have tried to overwrite claim with value of User.Id which is quite important, I would think -)
Now, let's try to fix your coding problems. When you create a user, you add claims after you have created a user object:
public async Task<IdentityResult> CreateUserAsync(string email, string password, string groupId)
{
var user = new ApplicationUser
{
Id = userId,
UserName = email
};
var userCreateResult = await _UserManager.CreateAsync(user, password);
if(!userCreateResult.IsSuccess)
{
// user creation have failed - need to stop the transaction
return userCreateResult;
}
// better to have a class with constants representing your claim types
var groupIdClaim = new Claim("MyApplication:GroupClaim", ObjectId.GenerateNewId(DateTime.UtcNow).ToString());
// this will save the claim into the database. Next time user logs in, it will be added to Principal.Identity
var claimAddingResult = await _UserManager.AddClaimAsync(userId, groupIdClaim);
return claimAddingResult;
}
As for extension methods I usually work with IPrincipal or ClaimsPrincipal. But IIdentity is also workable. Don't forget you can access ClaimsPrincipal anywhere by calling ClaimsPrincipal.Current.
This is how I usually work with extension methods:
public static string GetGroupId(this ClaimsPrincipal principal)
{
var groupIdClaim = principal.Claims.FirstOrDefault(c => c.Type == "MyApplication:GroupClaim");
if (personIdClaim != null)
{
return groupIdClaim.Value;
}
return String.Empty;
}
So in your methods you'd retrieve assigned groupId for the currently logged in user like this:
var groupId = ClaimsPrincipal.Current.GetGroupId();
Hope this clarifies your confusion!

Related

How to create identity user based on claims prinicpals [duplicate]

This question already has answers here:
JWT Authentication - UserManager.GetUserAsync returns null
(3 answers)
Closed 2 years ago.
I am using asp.net mvc with work/school accounts authentication. Currently I'm trying to implement identity into to the user process.
Here is my ApplicationUser class:
public class ApplicationUser: IdentityUser
{
public ICollection<Semester> Semesters { get; set; }
}
So far, identity works just fine, there is just one problem. When I log into the app with my school account, I can call the ClaimsPrincipals as User in the Controllers. To get the current ApplicationUser you can use the UserManager (await _userManager.GetUserAsync(User), with User being the ClaimsPrincipals) But since I haven't stored my school account in the database, the result will be null. If I create a new ApplicationUser like the following
var newUser = new ApplicationUser()
{
UserName = User.Identity.Name,
Email = User.Identity.Name
};
await _userManager.CreateAsync(newUser);
await _userManager.AddClaimsAsync(newUser, User.Claims);
This will succesfully create and save the new user to the database with the claims. But then when I try to get the new created ApplicationUser with await _userManager.GetUserAsync(User) the result will still be null. If I access my DbContext and get all ApplicationUsers, the newly created ApplicationUser is there. So, how can I create an ApplicationUser based on the ClaimsPrincipals I get from my school account login?
Credits to #poke for this.
UserManager.GetUserAsync internally uses UserManager.GetUserId to retrieve the user id of the user which is then used to query the object from the user store (i.e. your database).
GetUserId basically looks like this:
public string GetUserId(ClaimsPrincipal principal)
{
return principal.FindFirstValue(Options.ClaimsIdentity.UserIdClaimType);
}
So this returns the claim value of Options.ClaimsIdentity.UserIdClaimType. Options is the IdentityOptions object that you configure Identity with. By default the value of UserIdClaimType is ClaimTypes.NameIdentifier, i.e. "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier".
So when you try to use UserManager.GetUserAsync(HttpContext.User), where that user principal has a UserID claim, the user manager is simply looking for a different claim.
You can fix this by either switchting to the ClaimTypes.NameIdentifier:
new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
})
Or you configure Identity properly so it will use your UserID claim type:
// in Startup.ConfigureServices
services.AddIdentity(options => {
options.ClaimIdentity.UserIdClaimType = "UserID";
});
Source:
https://stackoverflow.com/a/51122850/3850405
The claims from an external provider will be specific to that provider. It is not logging into the local identity store in your app, it is just claiming to know who the user is. So you need to log the user to your store (SignInManager) before you can use it for authorization. If you don't care about protecting resources and just want to know the user you can directly map to your internal store
The claims in the header need to be intercepted by the ASPNET 'middleware' using an authentication provider which will then set the User object in the HttpContext. Once you have the user, you would need to map your local user store to those from the school account, then get the claims as a separate call from the result. Usually the email is the subject claim and can be used for mapping:
var userName = User.Identity.Name;
var user = _userManager.FindByNameAsync(userName);
var claims = _userManager.GetClaimsAsync(user);

How can i find the current user id logged in .Net Core

I'm trying to get the current user id logged in so I can pull details down for this user as well assign details to the user e.g. booking events, seeing events they have booked, etc.
I have been trying to do like this
var USERID = User.Claims.FirstOrDefault(c => c.Type == "Id")
but I get an error when trying to assign to int.
I have my service method within my data project connecting to db then I pass these methods from the service into the controller, if that helps at all.
Any help would be great.
You can use the IHttpContextAccessor to access to HttpContext and get Current UserId like this
public int GetCurrentUserId()
{
var claimsIdentity = _contextAccessor.HttpContext.User.Identity as ClaimsIdentity;
var userDataClaim = claimsIdentity?.FindFirst("Id");//or claimsIdentity?.FindFirst(ClaimTypes.NameIdentifier);
var userId = userDataClaim?.Value;
return string.IsNullOrWhiteSpace(userId) ? 0 : int.Parse(userId);
}
Or
int userId = Convert.ToInt32((User.Identity as ClaimsIdentity).FindFirst(ClaimTypes.Name).Value);
The correct way to do this is via User.FindFirstValue(ClaimTypes.NameIdentifier).

How can I retrieve a user from a password reset token in ASP.NET Core?

I have an ASP.NET Core 2.1 web application and am adding forgot password functionality. I have looked at several examples, and they seem to take one of two approaches. The first approach is to include either the user id or the user's email in the password reset url along with the password reset token. The second approach is to include only the password reset token in the password reset url and then require the user to enter identifying information (such as email) when attempting to change the password (Binary Intellect example). Is there a way to look up the user given just the password reset token?
My team lead has asked me to pass just the token in the password reset url and then look up the user. My initial research makes me believe that I would have to manually keep record of the user id and token relationship, but am hoping that there's something built in. I have reviewed the ASP.NET Core UserManager documentation, but did not find any methods for retrieving a user for a given token.
Here's some of the example code embedding the user id in the password reset URL (Microsoft Password Recovery Doc):
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
There is a way to get the UserId from the password reset token, but in my opinion it's tricky and a lot of work.
What are the defaults
If you have some codes like the following,
services.AddIdentity<AppUser, AppRole>(options =>
{
...
}
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
the last line .AddDefaultTokenProviders() adds 4 default token providers, which are used to generate tokens for reset passwords, change email and change phone number options, and for two factor authentication token generation, into the pipeline:
DataProtectorTokenProvider
PhoneNumberTokenProvider
EmailTokenProvider
AuthenticatorTokenProvider
The first one, DataProtectorTokenProvider, is what we're looking for. It uses data protection to serialize/encrypt those tokens.
And within the DataProtectorTokenProvider, its protector is default to the name of "DataProtectorTokenProvider".
How tokens are generated
If you look at GenerateAsync() method inside DataProtectorTokenProvider, you can kind of tell the token consists of:
Utc timestamp of the token creation (DateTimeOffset.UtcNow)
userId
Purpose string
Security stamp, if supported
The generate method concatenates all those, transform them to a byte array, and calls the protector inside to protect/encrypt the payload. Finally the payload is converted to a base 64 string.
How to get User Id
To get the userId from a token, you need to do the reverse engineering:
Convert the token from base 64 string back to the byte array
Call the protector inside to unprotect/decrypt the byte array
Read off the Utc timestamp
Read userId
The tricky part here is how to get the same DataProtector used to generate those token!
How to get the default Data Protector
Since the default DataProtectorTokenProvider is DIed into the pipeline, the only way I can think of to get the same DataProtector is to use the default DataProtectorTokenProvider to create a protector with the same default name, "DataProtectorTokenProvider", used to generate tokens!
public class GetResetPasswordViewModelHandler : IRequestHandler<...>
{
...
private readonly IDataProtector _dataProtector;
public GetResetPasswordViewModelHandler(...,
IDataProtectionProvider dataProtectionProvider)
{
...
_dataProtector = dataProtectionProvider.CreateProtector("DataProtectorTokenProvider");
// OR
// dataProtectionProvider.CreateProtector(new DataProtectionTokenProviderOptions().Name);
}
public async Task<ResetPasswordViewModel> Handle(GetResetPasswordViewModel query, ...)
{
// The password reset token comes from query.ResetToken
var resetTokenArray = Convert.FromBase64String(query.ResetToken);
var unprotectedResetTokenArray = _dataProtector.Unprotect(resetTokenArray);
using (var ms = new MemoryStream(unprotectedResetTokenArray))
{
using (var reader = new BinaryReader(ms))
{
// Read off the creation UTC timestamp
reader.ReadInt64();
// Then you can read the userId!
var userId = reader.ReadString();
...
}
}
...
}
}
Screenshot:
My 2 cents
It seems like it's a lot of work just try to read the userId off a password reset token. I understand your team lead probably doesn't want to expose the user id on the password reset link, or (s)he thinks it's redundant since the reset token has the userId.
If you're using integer to represent the userId and don't want to expose that to public, I would change it to GUID.
If you have to use integer as your userId, I would just create a column of the type unique_identifier off the user profile (I would call it PublicToken) and use that to identifier a user for all public matters.
var callbackUrl = Url.Action("resetPassword", "account", new
{
area = "",
rt = passwordResetToken, // reset token
ut = appUser.Id // user token, use GUID user id or appUser.PublicToken
}, protocol: Request.Scheme);
I believe there is no way you can do that you can pass user email then find it look for user in your code
public async Task<IActionResult> ResetPassword([FromBody]ResetPasswordViewModel model)
{
if (string.IsNullOrEmpty(model.Token) || string.IsNullOrEmpty(model.Email))
{
return RedirectToAction("Index", "Error", new { statusCode = AppStatusCode.NotFound });
}
var isResetTokenValid = await _userManager.CheckValidResetPasswordToken(model.Token, model.Email);
if (!isResetTokenValid || string.IsNullOrEmpty(model.Email))
{
return StatusCode(AppStatusCode.ResetPassTokenExpire);
}
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
return Ok();
}
await _userManager.ResetPasswordAsync(user, model.Token, model.Password);
return Ok();
}
You can view the implementaion detail here
What I do in this case is I keep that new token in a cache or sql table with user id in it. That way you first query that table containing reset token, validate it if you need it and get user.

Custom UserValidator Affects Newly Created User Added to a Role

I'm trying to build a multi tenant application in asp.net core and registered asp.net identity like this:
services.AddIdentity<TUserIdentity, TUserIdentityRole>(options =>
{
options.User.RequireUniqueEmail = false;
})
.AddUserValidator<MultitenantUserValidator<TUserIdentity>>()
.AddEntityFrameworkStores<TContext>()
.AddDefaultTokenProviders();
My MultitenantUserValidator class:
public class MultitenantUserValidator<TUser> : IUserValidator<TUser>
where TUser : UserIdentity
{
public async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user)
{
bool combinationExists = await manager.Users
.AnyAsync(x => x.UserName == user.UserName
&& x.Email == user.Email
&& x.TenantId == user.TenantId);
if (combinationExists)
return IdentityResult.Failed(new IdentityError { Description = "The specified username and email are already registered in the given tentant" });
return IdentityResult.Success;
}
}
This is a part of my DbSeedingHelper Class
var result = await userManager.CreateAsync(user, Users.AdminPassword);
if (result.Succeeded)
{
// Add user to role
var userRoleresult = await userManager.AddToRoleAsync(user, AuthorizationConsts.AdministrationRole);
}
When it executes the line to add the created user to role, the identity result fails with the error from the MultitenantUserValidator: "The specified username and email are already registered in the given tenant".
I don't know why it goes back to the validator when trying to add the already created user to a role. I thought it's only meant to be called when the user is being created. Because of this, I can't add the user to a role. How can I solve this?
An identity user is validated whenever it is updated, not just when it is created. This is to ensure that the user stays valid at all times.
This obviously means that your validator should be able to verify existing users as well. A logic that checks for matching combinations, which would include the current user itself, is obviously not a good idea there.
A better check would be to make sure that there is no user with the same combination that isn’t the same user.
bool combinationExists = await manager.Users.AnyAsync(x => x.Id != user.Id && …);
Note that both user name and email address are usually unique within ASP.NET Core Identity. So it is impossible for users to have two distinct users with the the same user name and email address.
If you want your multi-tenancy to be based on the fact that you can register with a single username/email onto multiple tenants but have a separate identity, then this won’t work with the default setup.

How to get logged in users data (in a secure way)

I'm using ASP.NET MVC Entity Framework.
In my application I'm currently able to login and retrieve some data, but I'm not sure if the way I do it is a secure way.
Here is how my application works:
Currently when a user logs in, I make use of the following to store the username in a cookie:
FormsAuthentication.SetAuthCookie(user.Username, true);
Then I have a RoleProvider class that contains the following method, which returns a string array with the users role:
public override string[] GetRolesForUser(string username)
{
List<string> rolesList = new List<string>();
string role = CurrentlyLoggedInUser.User.Role.Name;
rolesList.Add(role);
string[] rolesArray = rolesList.ToArray();
return rolesArray;
}
Then I have the following class, that stores data of the currently logged in user:
public class CurrentlyLoggedInUser
{
private const string UserKey = "MyWebApp.Infrastructure.UserKey";
public static User User
{
get
{
MyDBEntities db = new MyDBEntities();
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
return null;
}
var user = HttpContext.Current.Items[UserKey] as User; //we set the
if (user == null)
{
user = db.Users.FirstOrDefault(x => x.Username == HttpContext.Current.User.Identity.Name);
if (user == null)
{
return null;
}
HttpContext.Current.Items[UserKey] = user;
}
return user; //Then we return the user object
}
}
}
Then whenever I need to query the database to find any related data of the user, I retrieve the users Id as follow:
int id = CurrentlyLoggedInUser.User.UserId;
Now I can make queries using this Id.
When I'm done, I logout and call the following:
FormsAuthentication.SignOut();
Is this recommended and secure enough?
Consider requiring https to secure you login form, don't forget about Authorize attribute on your MVC controllers and/or actions (or, preferably set Authorize attribute as a global filter) and on the basic level that's enough. #Chad-Nedzlek is not absolutely correct, FormsAuthentication, you involve, takes care about your authentication cookie encryption, so it's not so easy for anyone to impersonate any other. But you have to take care about preventing bruteforce attacks when implementing your custom login: consider password lifetime limitation, required complexity etc.
And if you are thinking about future extensibility, SSO for web, mobile apps and API, or possibility of easy use of external (Facebook, Google etc) login, consider switching to OpenId Connect.

Categories