The code below is called to assign the current authenticated user to a role named Admin. The user has already been created with a correctly configured email address and I can authenticate with it. However, I want to add the user to a Role using the demo code below.
public ActionResult AddUserToRole()
{
var userId = User.Identity.GetUserId();
using (var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext())))
{
IdentityResult result = userManager.AddToRole(userId, "Admin");
}
return RedirectToAction("Index");
}
The user was created using the template code from a new MVC 5 application. The template code calls for an email address for the username. However, the result of the AddToRole line returns result.Succeeded = false with an error of
User name jason#xxxxxxx.xx.xx is invalid, can only contain letters or
digits.
Why does the call to role apply the validator but the call to create a user doesn't? It's all template code.
Now, given that this is all template code, am I missing something or do I need to modify the template to have the user supply a username that isn't an email address.
Many thanks.
I had the same problem. To get around it, add the following line, before calling AddToRole():
userManager.UserValidator = new UserValidator<ApplicationUser>(userManager) { AllowOnlyAlphanumericUserNames = false };
Someone may know a more elegant solution, but this worked for me, so I never investigated further!
HTH
Related
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);
I'm use mvc core 3.1 with Identity 3.1.2, with default built-in UI:
services.AddIdentity<IdentityUser, IdentityRole>(opt =>
{
opt.User.RequireUniqueEmail = true;
opt.SignIn.RequireConfirmedEmail = false;
opt.SignIn.RequireConfirmedAccount = false;
}).AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddDefaultUI();
I wrote hard code that creates initial administrator user:
var userAdminExists = await _userManager.FindByNameAsync("Admin");
if (userAdminExists == null)
{
var user = new IdentityUser
{
Id = Guid.NewGuid().ToString(),
UserName = "Admin",
Email = "xxxxxx#gmail.com",
EmailConfirmed = true
};
var x = await _userManager.CreateAsync(user, _IConfiguration.GetValue<string>("defaultAdminPassword"));
await _userManager.AddToRoleAsync(user, "AdminRole");
}
(Of course I also tried to write the literal password inside the code, for those wondering about the IConfiguration, and I also watched the entries at break-points and everything worked as planned).
the user is created, but when i login with this email address and password in the browser i get "Invalid login attempt." (in normal response, status 200).
I wrote test code:
var pass = _configuration.GetValue<string>("defaultAdminPassword");
var res = await _userManager.CheckPasswordAsync(await _userManager.FindByEmailAsync("xxxx#gmail.com"), pass);
And of course it return true...
furthermore, also if i reset password via email reset link ("Forgot your password?") and i set new password, is not work...
but new users created through the "Register" UI interface, work great.
What am I missing?
When you use the Identity UI to sign in, you end up calling SignInManager.PasswordSignInAsync:
public virtual Task<SignInResult> PasswordSignInAsync(
string userName, string password, bool isPersistent, bool lockoutOnFailure);
As you can see from this signature, the first parameter is userName. When you use the UI as you've described, you provide the email address where the userName is expected. However, when you call FindByEmailAsync and pass the result into CheckPasswordAsync, you find the user based on Email and not UserName, which works.
When you register a new user using the Identity UI, you end up running this code:
await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
As you can see, this sets both the UserName and the Email to Input.Email. This is why you are able to sign in with these accounts.
when i login with this email address and password in the browser i get "Invalid login attempt." (in normal response, status 200).
If RequireConfirmedAccount option is set to true while configuring Identity service as below, the registered user login with that account but not confirming the account, which would cause this issue.
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
"Invalid login attempt" message
So please make sure the user confirm the account before user login app with that account. Besides, if account confirmation is not needed in your app, you can set RequireConfirmedAccount option is set to false.
options => options.SignIn.RequireConfirmedAccount = false
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.
I have two servers that are using the same ASP.NET Core Identity backend. I generate the password reset token with the following:
var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser);
I send this token via an email link. When the user clicks the link, they are taken to a separate site which should provide a UI to change the password. The following code handles the user's password submission of both the token and their new password:
var identityResult = await _userManager.ResetPasswordAsync(applicationUser, code, password);
On the second server, the identity result always returns false because "invalid token".
Looking through the source, I see that the token is generated using the IP address (so I understand why the token validation failed).
My question is how do I enable successful token creation/validation across different machines? In previous forms of ASP.NET, I would likely use a shared machine key to prevent these scenarios. ASP.NET Core doesn't seem to have a similar concept. From what I've read, it seems that this might be a scenario to use the DataProtection API. Unfortunately, I haven't seen any examples as how to apply this to generating the reset token.
Have you tried setting the application name to the same value in both applications?
services.AddDataProtection().SetApplicationName("same for both apps");
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview
P.S - I'm struggling with exactly the same problem.
you should encode your token before you send it. You should do something like this:
var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser);
var encodedCode = HttpUtility.UrlEncode(token);
After encoding it, you must pass the encoded token rather than the generated token.
I faced the similar problem. Its not about 2 servers actually. Its about identity framework. You can derived from usermanager and you can override provider with central one. But I tried something different and it worked.
First of all ConfirmEmail method looks into database, if you have one database the shouldn't be a problem between tokens with more than one server.
In your usermanager you should create dataprovider at your constructor like this.
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
var dataProtectorProvider = Startup.DataProtectionProvider;
var dataProtector = dataProtectorProvider.Create("My Identity");
this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, string>(dataProtector);
//this.UserTokenProvider.TokenLifespan = TimeSpan.FromHours(24);
}
Also you should be see token in your database table for users. After this line of code.
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
UserManager.EmailService = new EmailService();
await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here");
When you see token in your database, check email for same. then click you callback url and correct the encode of url.
For using dataProtectorProvider ;
public partial class Startup
{
public static IDataProtectionProvider DataProtectionProvider { get; set; }
public void ConfigureAuth(IAppBuilder app)
{
DataProtectionProvider = app.GetDataProtectionProvider();
}
}
I using the Internet Application template in C# MVC 4 which generates the Account model and controller for you in Visual Studio. This gives me basic form functionality for logging in. I modified the Register class in the Account model to also take a FirstName, LastName, and Email in addition to the username and password. My table with user information is Users. When a user submits their information WebSecurity.CreateUserAndAccount(model.UserName, model.Password);
is ran and it adds the Username to my Users table. So at that point I have my PK Id and Username in the table. My User class definition has a custom object Portfolio. What I am trying to do is when the user registers they get added to the table and then I find that User and set the values for the rest of the data in the table and instantiate the Portfolio object. This would complete my register process.Currently, it adds the user to the table with just the Username adds them to the role and redirects back to index. My var user = _db.Users.Find(userId) is apparently not finding anything for some reason. I even tried to cheat and hardcode the userId because I knew what the next Id would be in the Users table and it still did not modify the table values. FirstName, LastName, Email all remain null in the table and no object is instantiated for Portfolio. I have my context so it should work. I did something similar in my Seed method and it worked fine. I am not sure why it is not working here. Does anyone have suggestions? Thank you.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model, TeamProject.Models.ProjectDb2 context)
{
if (ModelState.IsValid)
{
// Attempt to register the user
try
{
WebSecurity.CreateUserAndAccount(model.UserName, model.Password);
var userId = _db.Users
.Where(u => u.UserName == model.UserName)
.Select(u => u.Id).First();
var user = _db.Users.Find(userId);
user.FirstName = model.FirstName;
user.LastName = model.LastName;
user.Email = model.Email;
user.Portfolio = new Portfolio
{
Stocks = new List<Stock>
{
}
};
context.Users.AddOrUpdate(user);
System.Web.Security.Roles.AddUserToRole(model.UserName, "Users");
WebSecurity.Login(model.UserName, model.Password);
return RedirectToAction("Index", "Home");
}
As per the above comment:
You do not appear to be calling context.SaveChanges anywhere.