Asp.net core identity change username/email - asp.net-core

The default identity change username/email with confirmation logic doesn't make sense.
Set app with require email confirmation.
Set require confirmed email to sign in.
User then changes email, enters email incorrectly, logs out.
Now the user is locked out. Email has changed but requires
confirmation to sign in and no email confirmation link because
address entered incorrectly.
Have I setup my application wrong or did Microsoft not design Identity very well?
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
//...
var email = await _userManager.GetEmailAsync(user);
if (Input.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
}
StatusMessage = "<strong>Verify your new email</strong><br/><br/>" +
"We sent an email to " + Input.Email +
" to verify your address. Please click the link in that email to continue.";
}
//...
await _signInManager.RefreshSignInAsync(user);
return RedirectToPage();
}

Your issue is using SetEmailAsync for this purpose. That method is intended to set an email for a user when none exists currently. In such a case, setting confirmed to false makes sense and wouldn't cause any problems.
There's another method, ChangeEmailAsync, which is what you should be using. This method requires a token, which would be obtained from the email confirmation flow. In other words, the steps you should are:
User submits form with new email to change to
You send a confirmation email to the user. The email address the user is changing to will need to be persisted either in the confirmation link or in a separate place in your database. In other words, the user's actual email in their user record has not changed.
User clicks confirmation link in email. You get the new email address they want to change to either from the link or wherever you persisted it previously
You call ChangeEmailAsync with this email and the token from from the confirmation link.
User's email is now changed and confirmed.
EDIT
FWIW, yes, this appears to be an issue with the default template. Not sure why they did it this way, since yes, it very much breaks things, and like I said in my answer, ChangeEmailAsync exists for this very purpose. Just follow the steps I outlined above and change the logic here for what happens when the user submits a new email address via the Manage page.
EDIT #2
I've filed an issue on Github for this. I can't devote any more time to it at the moment, but I'll try to submit a pull request for a fix if I have time and no one else beats me to it. The fix is relatively straight-forward.
EDIT #3
I was able to get a basic email change flow working in a fork. However, the team has already assigned out the issue and seem to be including it as part of a larger overhaul of the Identity UI. I likely won't devote any more time to this now, but encourage you to follow the issue for updates from the team. If you do happen to borrow from my code to implement a fix now, be advised that I was attempting to create a solution with a minimal amount of entropy to other code. In a real production app, you should persist the new email somewhere like in the database instead of passing it around in the URL, for example.

As already identified, the template definitely provides the wrong behaviour. You can see the source for the template in the https://github.com/aspnet/Scaffolding repo here.
I suggest raising an issue on the GitHub project so this is changed. When the templates are updated, they'll no doubt have to account for both the case when confirmation is enabled and when it's not. In your case, you can reuse the logic that already exists in OnPostSendVerificationEmailAsync() relatively easily.
A more general implementation would look something like this:
public partial class IndexModel : PageModel
{
// inject as IOptions<IdentityOptions> into constructor
private readonly IdentityOptions _options;
// Extracted from OnPostSendVerificationEmailAsync()
private async Task SendConfirmationEmail(IdentityUser user, string email)
{
var userId = await _userManager.GetUserIdAsync(user);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = userId, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(
email,
"Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
}
public async Task<IActionResult> OnPostAsync()
{
//... Existing code
var email = await _userManager.GetEmailAsync(user);
var confirmationEmailSent = false;
if (Input.Email != email)
{
if(_options.SignIn.RequireConfirmedEmail)
{
// new implementation
await SendConfirmationEmail(user, Input.Email);
confirmationEmailSent = true;
}
else
{
// current implementation
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
}
}
var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email);
if (!setEmailResult.Succeeded)
{
var userId = await _userManager.GetUserIdAsync(user);
throw new InvalidOperationException($"Unexpected error occurred setting email for user with ID '{userId}'.");
}
}
// existing update phone number code;
await _signInManager.RefreshSignInAsync(user);
StatusMessage = confirmationEmailSent
? "Verification email sent. Please check your email."
: "Your profile has been updated";
return RedirectToPage();
}
public async Task<IActionResult> OnPostSendVerificationEmailAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = await _userManager.GetEmailAsync(user);
await SendConfirmationEmail(user, email);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToPage();
}
}

Related

How to forbid a locked out user to login in .NET Core 6 Identity

I have a custom user store in my .NET Core 6 site. The corresponding user table in DB contains a field called Lockout.
When a user signs in, I need that field to be used to forbid the user to login if that field is true.
The Login action of the Account Controller has this code:
var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
var appUser = await _userManager.FindByNameAsync(model.Login);
if (appUser.Roles == null || appUser.Roles.Count == 0)
{
await PerformLogOff();
return Json("ERROR: No tiene autorización para ingresar al sistema.");
}
else
{
returnUrl ??= Request.Path.ToString();
appUser.LastLoggedOn = DateTime.Now;
if (!appUser.FirstLoggedOn.HasValue)
appUser.FirstLoggedOn = appUser.LastLoggedOn;
await _userManager.UpdateAsync(appUser);
return Json(returnUrl);
}
}
else if (result.IsLockedOut)
return Json("ERROR: Su usuario está bloqueado.");
else if (result.RequiresTwoFactor)
return Json("ERROR: El usuario requiere autenticación de doble factor.");
else
return Json("ERROR: Nombre de usuario o contraseña incorrectos.");
When a user is locked out, I need PasswordSignInAsync to return result.IsLockedOut.
How can I do it? Should I create a custom SignInManager? Notice that this has nothing to do with locking a user out when he fails to enter the password several times in login screen.
Thanks
Jaime
You can do it your self by adding another filed (e.g. IsActive) to the your ApplicationUser class. Then you can check it before the line "_signInManager.PasswordSignInAsync" with some code like this:
var user = await _userManager.Users.Where(s => s.UserName == model.Username).FirstOrDefaultAsync();
if (user != null)
{
if (!user.IsActive)
{
ModelState.AddModelError("", "User has been deactivated");
//Log the failure
return View();
}
}
I have read source code about SignInManager.cs. And it should support forbid lockout user.
For more details, check the blog below, it contains the code useful to you.
User Lockout with ASP.NET Core Identity
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IactionResult> Login(UserLoginModel userModel, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(userModel);
}
var result = await _signInManager.PasswordSignInAsync(userModel.Email, userModel.Password, userModel.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
var forgotPassLink = Url.Action(nameof(ForgotPassword),"Account", new { }, Request.Scheme);
var content = string.Format("Your account is locked out, to reset your password, please click this link: {0}", forgotPassLink);
var message = new Message(new string[] { userModel.Email }, "Locked out account information", content, null);
await _emailSender.SendEmailAsync(message);
ModelState.AddModelError("", "The account is locked out");
return View();
}
else
{
ModelState.AddModelError("", "Invalid Login Attempt");
return View();
}
}
If you really want result.IsLockedOut to be true then I can see two options:
You would need to extend UserManager or AspNetUserManager and override virtual method IsLockedOutAsync (code) so it will be returning false based on your Lockout property.
I don't know internals of your store but you could also manually set original LockoutEnd field in database to date from the next century e.g.:3000-01-01 when your application logic wants to set it to true or clear the field to unlock the user
For both options you need to watch out - SignInManager will only attempt to check lockouts if feature is enabled (code).
To enable it you should make sure that your custom UserStore implements IUserLockoutStore<TUser> interface. Simple override of SupportsUserLockout (code) virtual property to return always true in your custom UserManager might not work - because there are few other operations made on UserStore to count failures etc.
Probably you are not looking for opinion but I feel that your idea from comment to load user in Login action and check your custom property is the cleanest. It is not only about hacking your solution here - but it is usualy very harmful in development teams to change well documented, battle-tested frameworks like Identity to behave in a different way.

User getting created before custom user validator runs in Identity core

I am using identity core for user management in .net core 3.1 web api. Now, I want to check the users email for something and if it meets the requirement only then he will be created. The code below tells a lot about what I want to achieve
I have a custom user validator as below:
public class CustomEmailValidator<TUser> : IUserValidator<TUser>
where TUser : User
{
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user)
{
User userFromEmail = null;
if(!string.IsNullOrEmpty(user.Email))
userFromEmail = manager.FindByEmailAsync(user.Email).Result;
if (userFromEmail == null)
return Task.FromResult(IdentityResult.Success);
return Task.FromResult(
IdentityResult.Failed(new IdentityError
{
Code = "Err",
Description = "You are already registered with us."
}));
}
}
I add the validator in my startup as below:
services.AddDbContext<DataContext>(x => x.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.User.RequireUniqueEmail = false;
opt.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789._-";
opt.Password.RequireDigit = true;
opt.Password.RequiredLength = 6;
opt.Password.RequireNonAlphanumeric = true;
opt.Password.RequireUppercase = true;
opt.Password.RequireLowercase = true;
})
.AddUserValidator<CustomEmailValidator<User>>();
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<DataContext>();
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
As can be seen, I want to use the default user validation and my custom validation too. The problem being the user gets created right after the default validation and the email always turns out as exists in my custom validation. I don't really want to override my default validations.
Creating the user as below:
[HttpPost("Register")]
public async Task<IActionResult> Register(UserForRegisterDto userForRegister)
{
var userToCreate = _mapper.Map<User>(userForRegister);
var result = await _userManager.CreateAsync(userToCreate, userForRegister.Password);
if (result.Succeeded)
{
var roleresult = await _userManager.AddToRoleAsync(userToCreate, "Member");
return Ok(roleresult);
}
return BadRequest(result.Errors);
}
Note This is not my actual use case. I know I can check for unique email in my default validation by making opt.User.RequireUniqueEmail = true. This is just to clear a concept for further development.
Update After further debugging, I see that the custom validation method is called twice. Once before user creation and once after creation for some reason. I insert a new unique email and the custom validation passes success and after user creation, custom validation is called again and find the email registered already and throws an error message. This is weird
Found out that AddToRoleAsync was calling the custom validator again and was finding the user present in the table. Had to include a check whether the user found in the table with the same email is the same as user getting getting updated.
Code below:
public class CustomEmailValidator<TUser> : IUserValidator<TUser>
where TUser : User
{
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
TUser user)
{
User userFromEmail = null;
if(!string.IsNullOrEmpty(user.Email))
userFromEmail = manager.FindByEmailAsync(user.Email).Result;
if (userFromEmail == null)
return Task.FromResult(IdentityResult.Success);
else {
if(string.Equals(userFromEmail.Id, user.Id))
{
return Task.FromResult(IdentityResult.Success);
}
}
return Task.FromResult(
IdentityResult.Failed(new IdentityError
{
Code = "Err",
Description = "You are already registered with us."
}));
}
}
This should help a lot of people

Identity Server 4 User Impersonation

I am struggling to implement a Impersonation feature into the Identity Server 4 Service. I understand that there's a lot of people who are against implementing it the way I want to but I really need the full redirect back to the SSO server in order to generate a new list of claims. The user that is being impersonated will have a completely different set of Rules associated with them as claims, so it must come from the IdSrvr.
I have read through https://github.com/IdentityServer/IdentityServer4/issues/853, and also IdentityServer4 - How to Implement Impersonation
Here's what I've attempted so far, We did this perfectly inside of Identity Server 3 with ACR values and the Pre-Auth even on the UserService.
This controller method I am calling from one of the Clients of my identity server:
public IActionResult Impersonate(string userIdToImpersonate, string redirectUri)
{
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(){RedirectUri = redirectUri, Items = { {"acr_values", $"userIdToImpersonate:{userIdToImpersonate}"}}});
}
Here is my OnRedirectToProvider:
OnRedirectToIdentityProvider = context =>
{
if (context.Properties.Items.ContainsKey("acr_values"))
{
context.ProtocolMessage.AcrValues = context.Properties.Items["acr_values"].ToString();
}
return Task.CompletedTask;
}
This is where i start to get lost, at the moment, I've inherited from the AuthorizeInteractionResponseGenerator class and implemented my own with the override to the ProcessLoginAsync (this is the only thing i could find that was close to the pre-auth event previously)
protected override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
{
if (!request.IsOpenIdRequest) return await base.ProcessLoginAsync(request);
var items = request.GetAcrValues();
if (items.Any(i => i.Contains("userIdToImpersonate")) && request.Subject.IsAuthenticated())
{
//handle impersonation
var userIdToImpersonate = items.FirstOrDefault(m => m.Contains("userIdToImpersonate")).Split(':').LastOrDefault();
request.Subject = await _signInManager.ImpersonateAsync(userIdToImpersonate);
//var userToImpersonate = await _signInManager.UserManager.FindByIdAsync(userIdToImpersonate);
//if (userToImpersonate == null) return await base.ProcessLoginAsync(request);
//var userBeingImpersonated = await _signInManager.UserManager.FindByIdAsync(userIdToImpersonate);
//var currentUserIdentity = await _signInManager.CreateUserPrincipalAsync(userBeingImpersonated);
//var currentClaims = currentUserIdentity.Claims.ToList();
//currentClaims.Add(new Claim(IdentityServiceClaimTypes.ImpersonatedById, request.Subject.IsBeingImpersonated() ? request.Subject.GetClaimValue(IdentityServiceClaimTypes.ImpersonatedById) : _signInManager.UserManager.GetUserId(request.Subject)));
//request.Subject = new ClaimsPrincipal(new ClaimsIdentity(currentClaims));
//return await base.ProcessLoginAsync(request);
return new InteractionResponse();
}
else
{
return await base.ProcessLoginAsync(request);
}
}
As you can see, i've tried a couple different things here, When not using OIDC as a authentication scheme, and my IdServer/Site is the same site, I had a function that impersonation worked with. Which is where _signInManager.ImpersonateAsync(...) is. Here is that Implementation:
public async Task<ClaimsPrincipal> ImpersonateAsync(string userIdToImpersonate)
{
var userBeingImpersonated = await UserManager.FindByIdAsync(userIdToImpersonate);
if (userBeingImpersonated == null) return null;
var currentUserIdentity = await CreateUserPrincipalAsync(userBeingImpersonated);
var currentClaims = currentUserIdentity.Claims.ToList();
currentClaims.Add(new Claim(IdentityServiceClaimTypes.ImpersonatedById, Context.User.IsBeingImpersonated() ? Context.User.GetClaimValue(IdentityServiceClaimTypes.ImpersonatedById) : UserManager.GetUserId(Context.User)));
//sign out current user
await SignOutAsync();
//sign in new one
var newIdentity = new ClaimsPrincipal(new ClaimsIdentity(currentClaims));
await Context.SignInAsync(IdentityConstants.ApplicationScheme, newIdentity);
return Context.User;
}
In an effort to simply 'replace' who was signing in, or at least who the identity server was thinking was signing in, i just replaced Request.Subject with the Impersonation Result. This doesn't actually change anything that I can find, at least not on my client app. If i use the redirect URI of 'https://localhost:44322/signin-oidc' (localhost because i'm running the sites locally), I get a "Correlation failed at signin-oidc redirect" message. If anyone has implemented something like this or done anything similar I would greatly appreciate the help getting this complete.
Suggestions welcome for completely different implementations, this was just my best stab at what worked flawlessly with idsrvr3.

Extended user login

based on the ASP.NET Core Visual Studio emplate with individual user accounts and adding IdentityServer4 to the project as described in IdentityServer4: Using ASP.NET Core Identity, the relevant part for the login in the account-controller looks something like this:
ApplicationUser user = null;
// login with username and password
if (!string.IsNullOrEmpty(model.Username) && !string.IsNullOrEmpty(model.Password))
{
var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: true);
if (result.Succeeded)
{
user = await _userManager.FindByNameAsync(model.Username);
}
}
if (user != null)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));
// redirect
}
With this login, identityserver4 correctly issues the authentication scheme "Cookies" and creates the required tokens (access_token, id_token or whatever is configured).
Now to the problem I am having: I want to extend the username/password login with an additional option, namely an access key login. This means, that the user provides only a key he owns (similar to a password).
Please put aside all security and use-case concerns you might now have.
My code looks something like this:
// login with access key
if (!string.IsNullOrEmpty(model.AccessKey))
{
var result = await _signInManager.AccessKeySignInAsync(model.AccessKey);
if (result.SignInResult.Succeeded)
{
user = result.User;
}
}
if (user != null)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));
// redirect
}
Simply put, I have extended the SignInManager and the UserManager with the functionality i needed. The login "logic" works correctly (the UserLoginSuccessEvent is fired properly with the expected user). However, IdentityServer4 does not seem to notice any of this. The log pretty much stays empty (except the mentioned event). My question now is: How do I inform IdentityServer4 that this was a successful user login and the tokens can be issued?
In case you need the implementation of the SignInManager.AccessKeySignInAsync method:
public async Task<(SignInResult SignInResult, ApplicationUser User)> AccessKeySignInAsync(string accessKey)
{
if (accessKey == null)
{
throw new ArgumentNullException(nameof(accessKey));
}
var user = await _userManager.FindByAccessKeyAsync(accessKey);
return (user != null ? SignInResult.Success : SignInResult.Failed, user);
}

How to extend/customize MVC4 Internet Application WebSecurity/SimpleMembership

I've been trying my best to search for more information on how to modify/extend/customize the default membership system available in MVC4 Internet Application (EF 5 Code First) in Visual Studio 2012 Express.
I would like to know how to implement email verification such that when a user registers an email is sent with an activation link. When they click on the link their account is activated and they can log in using their username or email.
I would also like to know how to implement simple Roles for registered users by assigning a default role during registration.
Similar Questions:
How do I manage profiles using SimpleMembership?
How do you extend the SimpleMembership authentication in ASP.NET MVC4
But I would really like to work with the existing simplemembership system.
This post is quite close:
http://blog.longle.net/2012/09/25/seeding-users-and-roles-with-mvc4-simplemembershipprovider-simpleroleprovider-ef5-codefirst-and-custom-user-properties/
I've also seen this post:
http://weblogs.asp.net/jgalloway/archive/2012/08/29/simplemembership-membership-providers-universal-providers-and-the-new-asp-net-4-5-web-forms-and-asp-net-mvc-4-templates.aspx
This is the closest I've found so far:
http://weblogs.asp.net/thangchung/archive/2012/11/15/customize-the-simplemembership-in-asp-net-mvc-4-0.aspx
This is also useful but for WebPages:
http://blog.osbornm.com/archive/2010/07/21/using-simplemembership-with-asp.net-webpages.aspx
I was hoping to find a more comprehensive walk-through on the proper way to extend it.
It does not look like you got any answer.
Unless I do not fully understand what you want to do, there is no need to modify/extend/customize the default SimpleMembership to provide an email registration mechanism, or assign a default role during registration, as all of this can be done inside AccountController.
As an example, here is a register method I am using:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid) //TODO Change this to use a worker to send emails.
{
// Check if email exists already before creating new user
using (UsersContext db = new UsersContext())
{
UserProfile email = db.UserProfiles.FirstOrDefault(u => u.Email.ToLower() == model.Email.ToLower());
UserProfile uName =
db.UserProfiles.FirstOrDefault(u => u.UserName.ToLower() == model.UserName.ToLower());
// Attempt to register the user
try
{
if (email == null && uName == null && this.IsCaptchaVerify("Captcha is not valid"))
{
bool requireEmailConfirmation = !WebMail.SmtpServer.IsEmpty();
string confirmationToken = WebSecurity.CreateUserAndAccount(model.UserName, model.Password, new
{
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Email
},
requireEmailConfirmation);
if (requireEmailConfirmation)
{
EmailViewModel eml = new EmailViewModel
{
ToEmail = model.Email,
Subject = "Confirmez votre inscription",
FirstName = model.FirstName,
LastName = model.LastName,
Body = confirmationToken
};
UserMailer.ConfirmRegistration(eml).SendAsync();
Response.Redirect("~/Account/Thanks");
}
else
{
WebSecurity.Login(model.UserName, model.Password);
Response.Redirect("~/");
}
}
else
{
if (email != null)
ModelState.AddModelError("Email", "Email address already exists. Please enter a different email address.");
if (uName != null)
ModelState.AddModelError("UserName", "User Name already exists. Please enter a different user name.");
if (!this.IsCaptchaVerify("Captcha is not valid"))
TempData["ErrorMessage"] = "Captcha is not valid";
}
}
catch (MembershipCreateUserException e)
{
ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
}
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
There is no default role assigned here, but it would be easy to add once EmailConfirmation is validated.
As the question is quite old, I hope it help someone!