I’m an avid user on StackOverflow in questions about Asp.Net Identity and I attempt to answer most of the interesting questions. And the same question comes up quite often: users try to confirm their email via a confiramtion link or reset their password via reset link and in both cases get “Invalid Token” error.
There are a few possible solutions to this problem.
Password Reset Token vs Email Confirmation Token
First I would like to go through token generation process, as it might be confusing.
If you explore UserManager
object, you will find 3 public methods that can generate you a token:
GenerateUserTokenAsync
GeneratePasswordResetTokenAsync
GenerateEmailConfirmationTokenAsync
Difference in them is small but important.
If you look on the signature of GenerateUserTokenAsync
public virtual async Task<string> GenerateUserTokenAsync(string purpose, TKey userId)
You see it is taking a purpose string and a User.Id
.
If you look on the source code of GeneratePasswordResetTokenAsync
and GenerateEmailConfirmationTokenAsync
, you will see this:
public virtual Task<string> GeneratePasswordResetTokenAsync(TKey userId)
{
ThrowIfDisposed();
return GenerateUserTokenAsync("ResetPassword", userId);
}
public virtual Task<string> GenerateEmailConfirmationTokenAsync(TKey userId)
{
ThrowIfDisposed();
return GenerateUserTokenAsync("Confirmation", userId);
}
So PasswordReset and EmailConfirmation tokens are just tokens generated with a specific purpose. And if you follow the code execution path, you’ll see that the purpose
string is used as a part of encryption, as one of the encryption keys for the tokens.
When the tokens comes back into the system and you call ResetPasswordAsync
, the token goes through the reverse process – it eventually ends in VerifyUserTokenAsync(userId, "ResetPassword", token)
where “ResetPassword” is a purpose
. If you call ConfirmEmailAsync
then the token is passed to VerifyUserTokenAsync(userId, "Confirmation", token)
where “Confirmation” is a purpose
.
Do you see the pattern here? Value of purpose
string is one of the encryption keys in the token, so it is important to provide the correct key, i.e. you can’t call ResetPasswordAsync
on a token generated by GenerateEmailConfirmationTokenAsync
Token is changed in transit
Current (1st May 2015) Visual Studio 2013 MVC template with Identity includes these lines for account email confirmation:
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 <a href=\"" + callbackUrl + "\">here</a>");
I.e. token is generated, then used as a parameter in generating a URL and then passed into an email text. Now, token is a Base64 string. Base64 strings are not safe to pass as a URL parameters as special characters there can be interpreted incorrectly by browsers. So you need to do url-encode them to avoid mis-interpretation of the token:
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
code = System.Web.HttpUtility.UrlEncode(code);
// the rest of the snippet
Don’t forget that there are 2 places where you generate tokens – on user registration and on password reset. You need to add Url-Encoding in both of the places.
Security Stamp is Null
Credit goes to this answer on SO. If your UserManager
supports Security Stamp and if you use the framework-provided classes, it supports this. Your token generation process uses this piece of code:
string stamp = null;
if (manager.SupportsUserSecurityStamp)
{
stamp = await manager.GetSecurityStampAsync(user.Id);
}
writer.Write(stamp ?? "");
where writer
is a MemoryStream
writer that eventually is passed through data-protection, Base64 and returned to your controller. In other words, if SecurityStamp
on your user record is null, String.Empty
is written to the token.
On token validation process you can see
if (manager.SupportsUserSecurityStamp)
{
var expectedStamp = await manager.GetSecurityStampAsync(user.Id).WithCurrentCulture();
return stamp == expectedStamp;
}
So if expectedStamp
is null
again, we are comparing String.Empty
to null
. And they are not equal.
This looks like a bug in Identity, but possibly this is intentional for security reasons. I have started a discussion about this on Identity forum, maybe this will be fixed.
So solution to the problem is to make sure that SecurityStamp
always has a value on your user records.
Machine Keys are not matching
Another reason for this problem might come from your different servers. If your token was generated on one server, and then attempting to validate it on another. Reason for that is that the token is protected via MachineKey.Protect
. That is configured on OWIN initialisation:
builder.Properties[Constants.SecurityDataProtectionProvider] = new MachineKeyDataProtectionProvider().ToOwinFunction();
and comes down to using of MachineKey.Protect
in a wrapper MachineKeyDataProtector
namespace Microsoft.Owin.Host.SystemWeb.DataProtection
{
internal class MachineKeyDataProtector
{
private readonly string[] _purposes;
public MachineKeyDataProtector(params string[] purposes)
{
_purposes = purposes;
}
public virtual byte[] Protect(byte[] userData)
{
return MachineKey.Protect(userData, _purposes);
}
public virtual byte[] Unprotect(byte[] protectedData)
{
return MachineKey.Unprotect(protectedData, _purposes);
}
}
}
And if your servers have different machine keys, token from one server will not validate on another server.
The solution to this problem is to provide a <MachineKey>
section in your web.config
and make sure that is always the same on all the servers.
There are a multiple guides online how to generated a MachineKey, I’ll leave that for you to discover.