Reputation: 595
I have built a web app using ASP.NET MVC. I originally did not enable the "forgot password" bits. I have about 3 people using the service now, and I added Email Confirmation and Forgot Password functionality in anticipation of the real deployment within the month. I want my current users (including me) to have access to the forgot password functionality, but I cannot seem to find a way to do that when EmailConfirmed is false for all of us. EmailConfirmed must be true before forgot password emails will be sent.
Goal: Allow current users to use reset password functionality
Problem: Existing users have not confirmed their email, therefore they are not eligible for password reset.
Solution possibilities:
Does anyone know how to do this? Am I looking at the right solution? Finally, I should note that I'm using the LocalDB on IIS, so I don't have direct access to edit the table. I do have the .mdf file, but it seems that directly editing that may be more difficult than other possible solutions I assume to exist. I would greatly appreciate your help!
Thanks!
Upvotes: 0
Views: 91
Reputation: 944
Users without a second form of identity verification (such as a confirmed email address, phone number, security questions, pin, etc.) cannot be trusted to a password reset.
If you face this problem again, the best possible solution (in my opinion) would be to alert users after they log in that they do not have a confirmed email, and that they risk losing access to their account if they do not confirm an email promptly. This would still leave some stubborn users out of luck who refuse to confirm an email and lose their password, but in that case, you could work with that smaller pool of users on a case-by-case basis.
To accomplish the above suggested solution, you could start by alerting the user about a lack of email immediately after login, which would involve creating a redirect in the Account
controller. Below is a modified version of the stock login method generated by Visual Studio when creating a new ASP.NET 4.5.2 MVC Web Application. Pay particular attention to the SignInStatus.Success
case, which redirects to a new controller action I've created named RequestEmailConfirmation
.
Controllers/AccountController.cs:
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
// Redirect user to confirm an email address.
if (!(await UserManager.IsEmailConfirmedAsync(User.Identity.GetUserId())))
{
return RedirectToAction("RequestEmailConfirmation", "Account");
}
else
{
return RedirectToLocal(returnUrl);
}
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
// GET: /Account/RequestEmailConfirmation
public ActionResult RequestEmailConfirmation()
{
return View();
}
This modified login action will ignore any returnUrl
the user may have had. It's possible to maintain that information if desired, but for this example, it is lost if the user is redirected to confirm an email.
Next, we look at a new view page and a view model to accompany the RequestEmailConfirmation
action. The view presents a simple form for the user to type and then re-type their email, and the view model stores that information to pass to another controller action we'll see later. Note that the user is logged in at this point and could choose to ignore the request for email confirmation, instead navigating away to other password-protect content on the site.
Views/Account/RequestEmailConfirmation.cshtml
@using MyWebApplication.Models
@model RequestEmailConfirmationViewModel
@{
ViewBag.Title = "Request Email Confirmation";
}
<h2>@ViewBag.Title.</h2>
<div class="row">
<div class="col-md-8">
<section id="loginForm">
@using (Html.BeginForm("RequestEmailConfirmation", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.AntiForgeryToken()
<h4>Confirm Email Address</h4>
<span class="text-danger">You have not yet confirmed an email address for this account. Please confirm your email address below to ensure access to your account in the event of a lost or forgotten password.</span>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.ConfirmEmail, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.ConfirmEmail, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.ConfirmEmail, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Confirm" class="btn btn-default" />
</div>
</div>
}
</section>
</div>
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Models/RequestEmailConfirmationViewModel.cshtml:
using System.ComponentModel.DataAnnotations;
namespace MyWebApplication.Models
{
public class RequestEmailConfirmationViewModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email")]
public string Email { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name = "Confirm Email")]
[Compare("Email", ErrorMessage = "The email and confirmation email do not match.")]
public string ConfirmEmail { get; set; }
}
}
With the email confirmation request form in place, we move on to three more controller actions in the Account
controller to handle the form submission, the "thank you for submitting a confirmation email" page, and the action to handle the confirmation of the email when the user visits the link provided in the confirmation email. The important bits here are the email confirmation token generation, and the subsequent confirmation of that token receipt, which allows us to safely set the EmailConfirmed
value in the user database. Two new views are also created below, the first to display the "thank you for submitting a confirmation email" message, and the second to display a "thank you for confirmation your email" message.
Controllers/AccountController.cs (cont.):
// POST: /Account/Register
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> RequestEmailConfirmation(RequestEmailConfirmationViewModel model)
{
if (ModelState.IsValid)
{
// Send an email confirmation.
string userId = User.Identity.GetUserId();
string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Confirm Email Address", "Please confirm your email address by clicking <a href=\"" + callbackUrl + "\">here</a>.");
return RedirectToAction("RequestEmailConfirmationSubmitted", "Home");
}
// If we got this far, something failed, redisplay form
return View(model);
}
// GET: /Account/RequestEmailConfirmationSubmitted
[AllowAnonymous]
public ActionResult RequestEmailConfirmationSubmitted()
{
return View();
}
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
Views/Account/RequestEmailConfirmationSubmitted.cshtml
@{
ViewBag.Title = "Email Address Submitted";
}
<h2>@ViewBag.Title.</h2>
<div>
<p>
Thank you for submitting your email address. An email with a confirmation link will be sent to you shortly to verify your ownership.
</p>
</div>
Views/Account/ConfirmEmail.cshtml:
@{
ViewBag.Title = "Email Address Confirmed";
}
<h2>@ViewBag.Title.</h2>
<div>
<p>
Thank you for confirming your email.
</p>
</div>
With the above code, I'm assuming you have already setup an email service to actually send the confirmation email to the user. Once the user has confirmed their email, they will be able to safely use the password reset functionality. While I have not fully tested this code, this should provide you with an adequate solution to your problem.
Upvotes: 0
Reputation: 595
Since I only had three users, I wound up deleting the files from
C:\inetpub\wwwroot\appname\App_Data
There was an .mdf file and an .ldf file. These are the files used for the LocalDB. I lost all existing user registrations, but when the (3) users re-registered, they were prompted for email confirmation (which is what I wanted). This is a workable solution for early-stage programs, but it's an awful solution to the problem. I wound up corrupting the .mdf file which is the only reason I even tried deleting/regenerating it. This is here for reference, but don't try this as a solution to your site's issue unless you're the only user! Still looking for better options, for future reference! Thanks!
Upvotes: 1