We are very accustomed to how user registration works online. Enter your email, possibly pick a username, enter password, re-enter the password, get a confirmation email, click on a link in that email and voilĂ , account created.
However, there a are a few things that can go wrong.
Imagine this scenario. You want to create a new account with an hypothetical website named TheWebsite. You go through all the normal process but you mistype your email. Instead of [email protected] you type [email protected]. But not all is bad, you typed your username correctly: johnsmith.
You wait for the confirmation email, but it doesn’t arrive. It’s 6pm, time to go home. You’ll check it tomorrow, surely it will have arrived by then.
Tomorrow comes, and you forget that you didn’t confirm your email, so you try to login, johnsmith + password. And it works, happy days.
Years pass, and you’ve been using TheWebsite happily. All is well until one day try to log in and you get an invalid login error. You try again, and again you get an error.
This can’t be right you think. I always use johnsmith and the same password. You click the reset password link, but no reset email ever arrives.
This is a tragedy, all those pictures, and other stuff you’ve uploaded to TheWebsite. You can’t access them anymore.
What happened?
Well, turns out that the person that actually owned the [email protected] was a kid that was curious and clicked the email from TheService asking him to verify his email address.
He himself forgot about this until a couple of years later when he heard about TheWebsite from some friends, and decided to try it. He tried to create an account and got an “account already exists” error. He used the password reset functionality and that’s that. He now owns the original John Smith’s account.
Although this story might seem convoluted it does happen. Brock Allen when talking about ASP.NET Identity Core in NDC described a situation where this had happened to someone from his family.
So what can be done?
A possible solution would be to ask for the password again when clicking the email confirmation link. This way Jon Smith wouldn’t have been able to confirm John Smith’s email, because he wouldn’t know the password.
This introduces a new problem though. What about if between choosing a password an receiving the confirmation email you forget the password?
Well, then bye bye ability to create an account with that email. And goodbye cool username you managed to pick, won’t be able to use those again.
Is there no hope?
There is. Here’s a radical idea: don’t let the user pick anything until s/he confirms the email address.
The process would be:
- Click Sign Up
- Enter email
- Tell user to check email and click on the email confirmation link
- User clicks on email confirmation link and is taken to a page where he enters
- Username (if required, some websites only require an email)
- Password
- Retype password
- Done
It’s like saying I’ll only talk to you when you confirm you own that email address. After that you can tell me what you want.
Trying to create an actual implementation
Last week I wrote a step by step guide on how to create a web application from scratch that is able to deal with user registration using email confirmation.
It was while researching ASP.NET Identity Core that I saw that NDC video with Brock Allen and learned about this problem.
I decided to try to implement this approach using ASP.NET Identity, but unfortunately couldn’t get to a reliable implementation.
A scenario where the user picks an invalid password or a username that is already taken will leave the account for the user’s email in limbo.
Here’s a snippet that demonstrates the problem when using ASP.NET Identity:
[HttpPost]
public async Task<IActionResult> VerifyEmail(string id, string token, string username, string password, string repassword)
{
var user = await _userManager.FindByIdAsync(id);
var confirmEmailResult = await _userManager.ConfirmEmailAsync(user, token);
if (!confirmEmailResult.Succeeded)
throw new InvalidOperationException("Invalid token");
var addPasswordResult = await _userManager.AddPasswordAsync(user, password);
if (!addPasswordResult.Succeeded)
{
addPasswordResult.Errors.ToList().ForEach(error => ModelState.AddModelError(string.Empty, error.Description));
return View(); //goes back to the form where the user chooses the username and password
}
//...
If the user mistypes the passwords the email gets confirmed but the user has no password set (it is possible to create users without password in ASP.NET Identity). The token for confirming the email cannot be used again, so the user won’t ever be able to set the password.
One could think that we should only confirm the email token after setting the user’s password, unfortunately that does not work because the token becomes invalid (won’t match the user) if you change any of the user’s properties.
Also, we can’t validate the email and then redirect the user to another page to pick a username and password. That other page could be abused by another party to take over the account simply by navigating to it before the user did.
So it seems there’s no reliable way of doing this with ASP.NET Identity, at least without creating custom email confirmation tokens that are still valid if we set the user’s password.
The idea is still valid though, however it’s hard not to feel that we are fighting the framework while trying to do it.