Does anyone know how to enable a user to change username/email with ASP.NET identity with email confirmation? There\'s plenty of examples on how to change the password but I
Trailmax got most of it right, but as the comments pointed out, the user would be essentially stranded if they were to mess up their new email address when updating.
To address this, it is necessary to add additional properties to your user class and modify the login. (Note: this answer will be addressing it via an MVC 5 project)
Here's where I took it:
1. Modify your User object First, let's update the Application User to add the additional field we'll need. You'll add this in the IdentiyModel.cs file in your Models folder:
public class ApplicationUser : IdentityUser
{
public async Task GenerateUserIdentityAsync(UserManager manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
[MaxLength(256)]
public string UnConfirmedEmail { get; set; }//this is what we add
}
If you want to see a more in depth example of that being done, check out this here http://blog.falafel.com/customize-mvc-5-application-users-using-asp-net-identity-2-0/ (that is the example I used)
Also, it doesn't mention it in the linked article, but you'll want to update your AspNetUsers table as well:
ALTER TABLE dbo.AspNetUsers
ADD [UnConfirmedEmail] NVARCHAR(256) NULL;
2. Update your login
Now we need to make sure our login is checking the old email confirmation as well so that things can be "in limbo" while we wait for the user to confirm this new email:
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var allowPassOnEmailVerfication = false;
var user = await UserManager.FindByEmailAsync(model.Email);
if (user != null)
{
if (!string.IsNullOrWhiteSpace(user.UnConfirmedEmail))
{
allowPassOnEmailVerfication = true;
}
}
// This now counts login failures towards account lockout
// To enable password failures to trigger account lockout, I changed to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: true);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return allowPassOnEmailVerfication ? RedirectToLocal(returnUrl) : RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
That's it...you are essentially done! However, I always get annoyed by half answers that don't walk you past potential traps you'll hit later on, so let's continue our adventure, shall we?
3. Update your Manage/Index
In our index.cshtml, let's add a new section for email. Before we get there though, let's go add the field we need in ManageViewmodel.cs
public class IndexViewModel
{
public bool HasPassword { get; set; }
public IList Logins { get; set; }
public string PhoneNumber { get; set; }
public bool TwoFactor { get; set; }
public bool BrowserRemembered { get; set; }
public string ConfirmedEmail { get; set; } //add this
public string UnConfirmedEmail { get; set; } //and this
}
Jump into the index action in our Manage controller to add that to our viewmodel:
var userId = User.Identity.GetUserId();
var currentUser = await UserManager.FindByIdAsync(userId);
var unConfirmedEmail = "";
if (!String.IsNullOrWhiteSpace(currentUser.UnConfirmedEmail))
{
unConfirmedEmail = currentUser.UnConfirmedEmail;
}
var model = new IndexViewModel
{
HasPassword = HasPassword(),
PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
Logins = await UserManager.GetLoginsAsync(userId),
BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId),
ConfirmedEmail = currentUser.Email,
UnConfirmedEmail = unConfirmedEmail
};
Finally for this section we can update our index to allow us to manage this new email option:
- Email:
-
@Model.ConfirmedEmail
@if (!String.IsNullOrWhiteSpace(Model.UnConfirmedEmail))
{
- Unconfirmed: @Model.UnConfirmedEmail @Html.ActionLink("Cancel", "CancelUnconfirmedEmail",new {email=Model.ConfirmedEmail})
}
else
{
@Html.ActionLink("Change Email", "ChangeEmail")
}
4. Add those new modifications
First, let's add ChangeEmail:
View Model:
public class ChangeEmailViewModel
{
public string ConfirmedEmail { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
[DataType(DataType.EmailAddress)]
public string UnConfirmedEmail { get; set; }
}
Get Action:
public ActionResult ChangeEmail()
{
var user = UserManager.FindById(User.Identity.GetUserId());
var model = new ChangeEmailViewModel()
{
ConfirmedEmail = user.Email
};
return View(model);
}
View:
@model ProjectName.Models.ChangeEmailViewModel
@{
ViewBag.Title = "Change Email";
}
@ViewBag.Title.
@using (Html.BeginForm("ChangeEmail", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
New Email Address:
@Html.ValidationSummary("", new { @class = "text-danger" })
@Html.HiddenFor(m=>m.ConfirmedEmail)
@Html.LabelFor(m => m.UnConfirmedEmail, new { @class = "col-md-2 control-label" })
@Html.TextBoxFor(m => m.UnConfirmedEmail, new { @class = "form-control" })
}
HttpPost Action:
[HttpPost]
public async Task ChangeEmail(ChangeEmailViewModel model)
{
if (!ModelState.IsValid)
{
return RedirectToAction("ChangeEmail", "Manage");
}
var user = await UserManager.FindByEmailAsync(model.ConfirmedEmail);
var userId = user.Id;
if (user != null)
{
//doing a quick swap so we can send the appropriate confirmation email
user.UnConfirmedEmail = user.Email;
user.Email = model.UnConfirmedEmail;
user.EmailConfirmed = false;
var result = await UserManager.UpdateAsync(user);
if (result.Succeeded)
{
string callbackUrl =
await SendEmailConfirmationTokenAsync(userId, "Confirm your new email");
var tempUnconfirmed = user.Email;
user.Email = user.UnConfirmedEmail;
user.UnConfirmedEmail = tempUnconfirmed;
result = await UserManager.UpdateAsync(user);
callbackUrl = await SendEmailConfirmationWarningAsync(userId, "You email has been updated to: "+user.UnConfirmedEmail);
}
}
return RedirectToAction("Index","Manage");
}
Now add that warning:
private async Task SendEmailConfirmationWarningAsync(string userID, string subject)
{
string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
var callbackUrl = Url.Action("ConfirmEmail", "Account",
new { userId = userID, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(userID, subject,
"Please confirm your account by clicking here");
return callbackUrl;
}
And now finally, we can put in the cancellation of the new email address:
public async Task CancelUnconfirmedEmail(string emailOrUserId)
{
var user = await UserManager.FindByEmailAsync(emailOrUserId);
if (user == null)
{
user = await UserManager.FindByIdAsync(emailOrUserId);
if (user != null)
{
user.UnConfirmedEmail = "";
user.EmailConfirmed = true;
var result = await UserManager.UpdateAsync(user);
}
}
else
{
user.UnConfirmedEmail = "";
user.EmailConfirmed = true;
var result = await UserManager.UpdateAsync(user);
}
return RedirectToAction("Index", "Manage");
}
5. Update ConfirmEmail (the very very last step)
After all this back and forth we can now confirm the new email, which means we should remove the old email at the same time.
var result = UserManager.ConfirmEmail(userId, code);
if (result.Succeeded)
{
var user = UserManager.FindById(userId);
if (!string.IsNullOrWhiteSpace(user.UnConfirmedEmail))
{
user.Email = user.UnConfirmedEmail;
user.UserName = user.UnConfirmedEmail;
user.UnConfirmedEmail = "";
UserManager.Update(user);
}
}