Asp.Net Identity Invalid Token for password reset or email confirmation

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.