I've built an MVC 5 website. Adding a user generates and sends an email with a confirmation token. When the user clicks the emailed link, the site responds, asking the user to set an initial password.
When the user tries to set an initial password, the user has not logged in yet, so User.Identity.GetUserId() is null. How do I pass the user ID from the confirmation link to the call to SetPassword()?
AccountController.cs
// 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);
if (result.Succeeded)
using (var s = new UserStore())
{
var u = s.FindByIdAsync(userId).Result;
await s.SetEmailConfirmedAsync(u, true);
return View("ConfirmEmail", new { Id = userId }); // This doesn't seem to make Id available.
}
else
return View("Error");
}
ConfirmEmail.cshtml
#{
ViewBag.Title = "Confirm Email";
}
<h2>#ViewBag.Title.</h2>
<div>
<p>
Thank you for confirming your email.
Please #Html.ActionLink("click here to create a password.",
"SetPassword", "Manage",
routeValues: null, // I've tried passing new { Id = Model.Id }, but Id isn't available at run-time.
htmlAttributes: new { id = "loginLink" })
</p>
</div>
ManageController.cs
// POST: /Manage/SetPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> SetPassword(SetPasswordViewModel model)
{
if (ModelState.IsValid)
{
var result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (result.Succeeded)
{
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
return RedirectToAction("Index", new { Message = ManageMessageId.SetPasswordSuccess });
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
A simple fix is to add a parameter to the SetPassword method:
public async Task<ActionResult> SetPassword(SetPasswordViewModel model, Guid userIdentifier)
The ConfirmEmail.cshtml would then change to:
Please #Html.ActionLink("click here to create a password.",
"SetPassword", "Manage",
routeValues: null, // I've tried passing new { Id = Model.Id }, but Id isn't available at run-time.
htmlAttributes: new { id = "loginLink", userIdentifier = Model.ID })
When the user clicks the link, you should now have the userIdentifier filled in with the ID of the user the email was sent to.
It's not super secure as they can pass any Guid (if they can figure out which one to send).
You may want to track a PasswordRequest object by ID (instead of userIdentifier) that can expire the email in which case they need to request a new one. This way once the PasswordRequest record is used, it can be deleted to prevent reuse as well.
Note: PasswordRequest is just a made up name.
You collect the password when you first create a user so that you don't need an id for your UserManager.CreateAsync(). You'd send the confirmation email token in the register action after you've created a new row (and generated the id).
The SetPassword() action is for an existing and authenticated user to change their password. You should not mark that as [AllowAnonymous].
you can use the query string, in the confirmation token you can encrypt the username or userid and when you reach the reset page decrypt it and set the Identity to whoever it was.
Edit: In addition to that you can create a new table in the database InitialSetup, with columns user, EncryptedId(guid), Initial.
When someone clicks the email link they will need to input username, and new password. That is when you will check if the input username is the same as the EncryptedId(guid).
Related
I'm going to have a subscription flow as follows:
User goes to pricing page, chooses plan, is redirected to ../Register?planId=1
User registers with username and password, is forwarded to billing page, which needs to be ../Subscription/Billing?planId=1
I added an integer, planId, to my RegisterViewModel
On pricing page, I have the links working correctly.
For register controller, I have:
[AllowAnonymous]
public ActionResult Register(RegisterViewModel model, int planId)
{
if (Request.IsAuthenticated) {
return RedirectToAction("Pricing", "Home");
}
RegisterViewModel model1 = new RegisterViewModel();
model1.planId = Convert.ToInt32(Request.QueryString["planId"]);
return View(model1);
}
And in Register view I have:
#Html.HiddenFor(m => m.planId)
However, this value is blank everytime I've run the application. If I can get the planId to be included as part of the register form submission, then I think I can redirect the controller to "../Subscription/Billing?planId=1" after registration.
Here's the current register post controller, where I think I just need to add the planid to the redirectToAction:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, Role = "Admin", ReportsTo = "", ActiveUntil = DateTime.Now.AddDays(-1) };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
// string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
// var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
// await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking here");
return RedirectToAction("Pricing", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
My question is, how do I get the planid to pass from the query string to a hidden field in the login form?
You can try model1.planId = planId
I'm told to make admin have a functionality to change other users password without knowing their original password. I wrote a code that changes and saves password successfully in database, but when I try to login as that user I can't.
UsersController:
public ActionResult ChangePassword()
{
return View();
}
[HttpPost]
public ActionResult ChangePassword(int id, ViewModels.ChangePasswordViewModel model)
{
if (!SessionControlService.CheckIsLoginStillTrue(_loginsService, HttpContext))
return RedirectToAction("Login", "Account");
if (!User.IsInAnyRoles("Admin", "PropertyManager"))
return RedirectToAction("Error", "Errors",
new { error = Facility.Web.Resources.Resources.ErrorNotHavePermission });
var user = _userService.GetUser(id);
if (user == null)
return RedirectToAction("Error", "Errors",
new { error = Facility.Web.Resources.Resources.ErrorURLNotExist });
user.Password = model.NewPassword;
_userService.UpdateUser(user);
return RedirectToAction("Details", new { id = id });
}
Why can't I use the changed password which is saved in the database to login?
How can I make this work?
In ASP.NET MVC5, password is hashed... you cannot save a plaintext password like that.
You need to use these two methods:
var manager = new ApplicationUserManager(...);
var token = manager.GeneratePasswordResetToken(userId)
manager.ResetPassword(userId, token, newPassword)
You could also try ApplicationUserManager.UpdatePassword(...), or RemovePassword(...) and AddPassword(...)
ApplicationUserManager is normally in IdentityConfig.cs
I'm currently trying to add email confirmation to my website and i'm experiencing a few problems.
I can succesfully register an account, upon registration the confirmationToken gets placed in my database and a email gets send with the query string link: http://www.example.com/RegistrationConfirmation?9ZPwZZrO-UmdpVpxXWjmRw when going to this link the controller action RegistrationConfirmation gets called and the method ConfirmAccount does a query to see if we can find a user with the confirmation token that was passed in the url.
When debugging I get the error "Sequence contains more than one element" on this line: Account user = context.Accounts.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
I'm not sure what's going wrong cause the token is unique and there are no duplicate tokens in the database.
Register HttpPost:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterModel model)
{
string confirmationToken = CreateToken();
Account account = new Account(model.Username, model.Password, model.FirstName, model.LastName, model.Email, false, confirmationToken);
if (DatabaseHandler.isUsernameDuplicate(account.Username))
{
// is duplicate // provide notification
}
else
{
Session["accountID"] = Repository.InsertAccount(new Account(model.Username, model.Password, model.FirstName, model.LastName, model.Email,false, confirmationToken));
// Email Logic
try
{
await client.SendMailAsync(message);
}
catch (Exception e)
{
ModelState.AddModelError("", "Problem sending email: " + e.Message);
}
return View("ConfirmEmail");
}
return View();
}
RegistrationConfirmation HttpGet:
[HttpGet]
[AllowAnonymous]
public ActionResult RegisterConfirmation(string Id)
{
if (ConfirmAccount(Id))
{
return RedirectToAction("ConfirmationSuccess");
}
return RedirectToAction("ConfirmationFailure");
}
ConfirmAccount method:
private bool ConfirmAccount(string confirmationToken)
{
RecipeDbContext context = new RecipeDbContext();
Account user = context.Accounts.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
if (user != null)
{
user.IsConfirmed = true;
DbSet<Account> dbSet = context.Set<Account>();
dbSet.Attach(user);
context.Entry(user).State = EntityState.Modified;
context.SaveChanges();
return true;
}
return false;
}
Remove SingleorDefault() and check the result.. you will get to know its not duplicate and if you want to avoid this error then use FirstorDefault().
Turned out nothing was wrong with the query itself, the RegisterConfirmation HttpGet did not seem to get the confirmToken value so I added "Id = Request.QueryString.ToString();" and everything is working perfect now :)
I am trying to use asp.net identity for authentication, I am having some issues with encoding/decoding.
User clicks on forgot password link, so we call out:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[PassModelStateToTempData]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
logger.Info("reset_password attempting for {0}", model.Email);
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
this.Flash("Please check your email, we have sent you instructions on how to reset your password");
return RedirectToAction("ForgotPassword");
}
string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
logger.Debug("forgot_password code {0}", code);
var callbackUrl = Url.Action("ResetPassword", "Session", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
messagingService.ResetPassword(user.Email, callbackUrl);
this.Flash("Please check your email, we have sent you instructions on how to reset your password");
logger.Debug("remind_me successfully send out email to {0} {1}", model.Email, callbackUrl);
return RedirectToAction("ForgotPassword");
}
logger.Info("reset_password failed for {0}", model.Email);
// If we got this far, something failed, redisplay form
return RedirectToAction("ForgotPassword");
}
User gets email then clicks link so we run:
[HttpGet]
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
if (code == null)
{
this.Flash("Invalid login token, please enter your email address again");
return RedirectToAction("ForgotPassword");
}
var vm = new ResetPasswordViewModel
{
Code = code
};
return View(vm);
}
We pass on token into view - we ask for email and password, then user hits post and we run:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return RedirectToAction("ResetPassword");
}
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{
logger.Info("reset_password user not found [{0}]", model.Email);
// Don't reveal that the user does not exist
return RedirectToAction("ResetPasswordConfirmation", "Session");
}
var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Session");
}
AddErrors(result);
return RedirectToAction("ResetPassword", new { code = model.Code });
}
For some reason tokens seem to not match, here are an example of the token I am getting - why the case difference?
Token:
2015-10-14 13:06:52.7545|DEBUG|Controllers.Application|forgot_password code BoUZZ9OS7rEkKMkEJzerWdds4dZLHFTHO/EkjQC2Zr8YJvCyjsXUKBRLZk8jmAqhjyxOzgqOLdJ8P/ji8y+om2ne7bcsLICzcdLSHzrP6BNEr1/+HKvHcYan+JzAX7Ifpgq7casmMj4f9esAdxejLA==
Notice the case difference:
2015-10-14 13:07:29.7164|INFO|Controllers.Application|reset_password attempting for my.email#gmail.com with token: bouzz9os7rekkmkejzerwdds4dzlhftho/ekjqc2zr8yjvcyjsxukbrlzk8jmaqhjyxozgqoldj8p/ji8y+om2ne7bcsliczcdlshzrp6bner1/+hkvhcyan+jzax7ifpgq7casmmj4f9esadxejla== -> Invalid token.
Your MVC routing is set up to generate lowercase URLs:
routes.LowercaseUrls = true;
This means that your codes are also being converted to lowercase. Possible solutions are:
Turn off LowercaseUrls if you can (or want)
Use MVC attribute routing, though this can be quite a switch.
The simplest option for you may be to simply create the URL yourself:
//Generate the URL without the code parameter
var callbackUrl = Url.Action(
"ResetPassword",
"Session",
new { userId = user.Id },
protocol: Request.Url.Scheme);
//Manually add the code, remembering to encode it
callbackUrl = callbackUrl + "&code=" HttpUtility.UrlEncode(code);
I am trying to create a password recover feature in Rick Anderson's post here (http://www.asp.net/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity). This basically allows a user who has lost pass to get an email with a link containing a token. When they are verified on arrival back to site they get a rest page. Everything worked fine in Rick's example, except when I got to the line of code where the callbackURL is generated I got a Bad Request error. As far as I could tell it is caused by all those extra characters in the token and browsers won't accept? Could someone point me to a solution? Thanks, Sanjeev
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking here: link");
ViewBag.Link = callbackUrl;
return View("ForgotPasswordConfirmation");
}
// If we got this far, something failed, redisplay form
return View(model);
}
Use HttpUtility.UrlEncode on callbackUrl before you add it to the string.