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 :)
Related
I've modified the mvc5 registration to allow the user to upload a profile image. Everything works fine, the file gets uploaded, the database get properly populated, and the email confirmation email gets sent.
The problem is that since adding the file handling code, the return view("info") no longer does anything, the registration page just refreshes instead. I've stepped through the code, and it's still hitting return view("info") without any problems. Anybody know the cause/solution? I was wondering if it was something to do with the register action being a async?
Here's the code:
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var userD = new UserDetails();
userD.Name = model.Name;
Regex rgx = new Regex("[^a-zA-Z0-9]");
string UserFolder = rgx.Replace(model.Email + DateTime.Now, "");
userD.UserRootFolder = UserFolder;
//create root folder
string RootPath = #"~/UserFiles/" + UserFolder + "/";
//check it doesnt already exist
if (!Directory.Exists(Server.MapPath(RootPath)))
{
//create the directory
DirectoryInfo di = Directory.CreateDirectory(Server.MapPath(RootPath));
if(!di.Exists)
{
//if the folder still doesnt exist return to view as something went wrong
//return View(model);
}
userD.UserRootFolder = RootPath;
}
HttpPostedFileBase TheFile = model.file;
if (TheFile != null)
{
string path = Server.MapPath(RootPath + model.file.FileName);
model.file.SaveAs(path);
userD.UserPictureLocation = path;
model.file.InputStream.Close();
}
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
PaypalEmail=model.PaypalEmail,
UserDetails = userD
};
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
code = System.Web.HttpUtility.UrlEncode(code);
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");
ViewBag.Message = "Check your email and confirm your account, you must be confirmed "
+ "before you can log in.";
return View("info");
// return RedirectToAction("Index", "Home");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
Do you know if debugging and waiting the email be sent you can render your screen?
A: If it's Yes, I think it's better you register this CONFIRMATION EMAIL in the DB and them make another call to send ALL CONFIRMATION EMAIL that hasn't been sent.
By the way, could you share the code of your View Info and the path?
I've fixed it. I've no idea why, but saving the image after the email gets sent got it working again.
If anyone knows why, then i'd love to know.
I am writing an app (MVC5, Identity 2.0 and Entity Framework 6) that has a Users section that only Administrators can access. The only place to add users is in this section, unless you're registering a new Organisation (which is a parent of Users).
I have everything up and running but want to make the adding of users flow better. At present, the admin creates the user with a hard coded password and an e-mail is sent to them asking them to confirm their account. They click on this and the account is confirmed and then they have to login. Obviously, hard coding passwords is not suitable for a production app!
I would really like it for the user to be added, a random password generated, the confirm account e-mail to be sent and then once the user clicks on it, their account is confirmed and then they are re-directed to a page where they can reset their own password.
So, code ninjas, is this possible? If so, any advice would be appreciated!
Yes it is. You can remove the hard-coded password put in by the admin and replace your call to create user with var result = await UserManager.CreateAsync(user); Provide no password at this point. Make sure to dispatch a mail on creation to user to confirm email. Here's an example.
In the confirmEmail action view, you can create a password set form and post to back to confirmEmail. Sample below:
The HTTP Get ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var confirmed = await UserManager.IsEmailConfirmedAsync(userId);
if(confirmed)
{
return await RedirectToDashboard(userId);
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
if (result.Succeeded)
{
ViewBag.userId = userId;
ViewBag.code = code;
}
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
The HTTP POST to ConfirmEmail from your set password form:
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(SetPasswordViewModel model, string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
if (!ModelState.IsValid)
{
return View(model);
}
var result = await UserManager.AddPasswordAsync(userId, model.NewPassword);
if (result.Succeeded)
{
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
return await RedirectToDashboard(userId);
}
ViewBag.userId = userId;
ViewBag.code = code;
AddErrors(result);
return View(model);
}
Sample form to be put into the ConfirmEmailView
#using (Html.BeginForm("ConfirmEmail", "Account", new { userId = ViewBag.userId, code = ViewBag.code }, FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary("", new { #class = "color_orange" })
#Html.PasswordFor(m => m.NewPassword, new { #class = "form-control", placeholder = "New Password" })
#Html.PasswordFor(m => m.ConfirmPassword, new { #class = "form-control", placeholder = "Confirm Password" })
<input type="submit" value="Set password" class="btn" />
}
Remember to add a model to your confirmEmail view #model [ProjectName].Models.SetPasswordViewModel
And create the SetPasswordViewModel:
public class SetPasswordViewModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
I also need the same functionality. I have achieved this by a dirty code.
[AllowAnonymous]
Public ActionResult ConfirmEmail(string userId, string code)
{
if(userId == null || code == null)
{
return View("Error");
}
var user = UserManager.FindById(userId);
if(user == null)
{
}
else
{
RunAsync(ForceResetPassword(user));
return RedirectToAction("ResetPasswordAfterConfirmation","Account");
}
}
Public async Task<ActionResult> ForceResetPassword(ApplicationUser user)
{
string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("Reset Password", "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");
//Insert send email code here
}
private void RunAsync(Task task)
{
task.ContinueWith(t =>
{
}, TaskContinuationOptions.OnlyOnFaulted);
}
So they'll be receiving confirm email after admin created the account, then after they click the link, they'll be receiving another email again to reset password.
If you have a better way to achieve this, I'll be glad to know also. :)
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'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).
So I've set up a custom login in my MVC application which appears to work...
public ActionResult Login()
{
return View();
}
[HttpPost]
public ActionResult Login(User model, string returnUrl)
{
if (ModelState.IsValid)
{
if (model.Login())
{
FormsAuthentication.SetAuthCookie(model.Username, true);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/") && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "Invalid email address or password.");
}
}
// If execution got this far, something failed, redisplay form
return View(model);
}
public class User
{
public bool Login()
{
var user = db.Users.FirstOrDefault(u => u.EmailAddress == EmailAddress);
if (user == null)
{
throw new ValidationException("User not found.");
}
else
{
// validates whether or not the password on the user record
// that was retrieved by the query matches the password entered at login
return Hashing.ValidatePassword(Password, user.Password);
}
}
}
Unfortunately, there's some conflict between it and the default _LoginPartial.cshtml View (which looks like below):
#model LoganMVC.Models.User
#if (Request.IsAuthenticated) {
<text>
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", new { User.Identity.Name })!
#using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm" })) {
#Html.AntiForgeryToken()
Log off
}
</text>
} else {
<ul>
<li>#Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
<li>#Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
</ul>
}
The ArgumentNullException highlights this line...
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", new { User.Identity.Name })!
Saying...
Value cannot be null or empty.
It's clear that the value that cannot be null or empty is User.Identity.Name, but what isn't clear is WHY IsAuthenticated is true to begin with. This is Forms Authentication and, as far as the software is aware (because I killed the debug server, closed the solution and started debugging fresh again), the application has never run before.
Okay I've been bashing my head against various hard surfaces trying to get this one figured out and eventually did.
After looking at the errors I was initially getting:
Value cannot be null or empty.
And then later
The provided identity of type 'System.Web.Security.FormsIdentity' is marked IsAuthenticated = true but does not have a value for Name. By default, the anti-forgery system requires that all authenticated identities have a unique Name. If it is not possible to provide a unique Name for this identity, consider setting the static property AntiForgeryConfig.AdditionalDataProvider to an instance of a type that can provide some form of unique identifier for the current user.
Were fairly simple fixes.
First off, I had initially set my login cookie to persist. This means that it wouldn't be deleted when the browser is closed and would not expire either. While this did not cause the Value cannot be null or empty error, it was contributory in that the site was finding it and assuming a user had authenticated.
FIX
To fix this, I simply cleared cookies from my browser set the cookie's persist to false. This allowed me to focus more on the actual problem.
Value cannot be null or empty
This problem was actually not as complicated as I thought. When I started taking a closer look at things, I noticed that there was an error in the following code:
[HttpPost]
public ActionResult Login(User model, string returnUrl)
{
if (ModelState.IsValid)
{
if (model.Login())
{
FormsAuthentication.SetAuthCookie(model.Username, true);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/") && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "Invalid email address or password.");
}
}
// If execution got this far, something failed, redisplay form
return View(model);
}
This post captures an instance of my User model from the form (which is the login form so only EmailAddress and Password values are included). It also uses the Username value from this model (which is null) to set the Authentication Cookie.
I changed my .Login() method to a function that returns a User if the email address is found and the stored hash matches a hash of the password entered for the login.
The code, thus, changed to the following (this is only the key point of the change)
// Account.Login Post
if (ModelState.IsValid)
{
var user = model.Login(model.EmailAddress);
if (user == null)
{
ModelState.AddModelError("", "Invalid email address or password.");
}
FormsAuthentication.SetAuthCookie(user.Username, true);
// create session variables n such
}
// User.Login
public User Login(string EmailAddress
{
var user = dbContext.Users.FirstOrDefault(u => u.EmailAddress == Email);
if (user == null)
throw new Exception("User not found.");
if (Hashing.ValidatePassword(Password, user.Password))
return user;
return null; // if we got here, something went wrong
}
Once I'd fixed this, I got another problem later. Some issue to do with the Anti-Forgery system.
While not a solution, I managed a workaround and commented out the following line from my _LoginPartial View:
#Html.AntiForgeryToken()
I had concerns about security but I figured that Forms Auth on a basic asp.net website with a hashed and salted password has been sufficient for quite a number of years, so I shouldn't have any issues with it here.
You probably miss something. In your controller Account you have an Action:
public ActionResult Manage(string name) // I presume name
{
....
}
Then your line should be:
Hello, #Html.ActionLink(User.Identity.Name, "Manage", "Account", new { name=User.Identity.Name })!
The anonymous object which is routeValues
It's n object that contains the parameters for a route. The parameters are retrieved through reflection by examining the properties of the object. The object is typically created by using object initializer syntax.(Definition taken from MSDN) And it should be well formatted.
I hope it will solve the problem otherwise you have to check if the User.Identity is different of null.
I have noticed also somthing else in this line:
var user = db.Users.FirstOrDefault(u => u.EmailAddress == EmailAddress);
Where comes from the EmailAddress after ==?
You should provide a parameter to you Login method to replace your second EmailAddress.
public bool Login(string emailAddress)
{
var user = db.Users.FirstOrDefault(u => u.EmailAddress == emailAddress);
if (user == null)
{
throw new ValidationException("User not found.");
}
else
{
// validates whether or not the password on the user record
// that was retrieved by the query matches the password entered at login
return Hashing.ValidatePassword(Password, user.Password);
}