Asp.NET Identity 2 giving “Invalid Token” error

前端 未结 21 1723
情话喂你
情话喂你 2020-11-27 03:04

I\'m using Asp.Net-Identity-2 and I\'m trying to verify email verification code using the below method. But I am getting an \"Invalid Token\"

相关标签:
21条回答
  • 2020-11-27 03:27

    I encountered this problem and resolved it. There are several possible reasons.

    1. URL-Encoding issues (if problem occurring "randomly")

    If this happens randomly, you might be running into url-encoding problems. For unknown reasons, the token is not designed for url-safe, which means it might contain invalid characters when being passed through a url (for example, if sent via an e-mail).

    In this case, HttpUtility.UrlEncode(token) and HttpUtility.UrlDecode(token) should be used.

    As oão Pereira said in his comments, UrlDecode is not (or sometimes not?) required. Try both please. Thanks.

    2. Non-matching methods (email vs password tokens)

    For example:

        var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
    

    and

        var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
    

    The token generated by the email-token-provide cannot be confirmed by the reset-password-token-provider.

    But we will see the root cause of why this happens.

    3. Different instances of token providers

    Even if you are using:

    var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
    

    along with

    var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
    

    the error still could happen.

    My old code shows why:

    public class AccountController : Controller
    {
        private readonly UserManager _userManager = UserManager.CreateUserManager(); 
    
        [AllowAnonymous]
        [HttpPost]
        public async Task<ActionResult> ForgotPassword(FormCollection collection)
        {
            var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
            var callbackUrl = Url.Action("ResetPassword", "Account", new { area = "", UserId = user.Id, token = HttpUtility.UrlEncode(token) }, Request.Url.Scheme);
    
            Mail.Send(...);
        }
    

    and:

    public class UserManager : UserManager<IdentityUser>
    {
        private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
        private static readonly UserManager Instance = new UserManager();
    
        private UserManager()
            : base(UserStore)
        {
        }
    
        public static UserManager CreateUserManager()
        {
            var dataProtectionProvider = new DpapiDataProtectionProvider();
            Instance.UserTokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());
    
            return Instance;
        }
    

    Pay attention that in this code, every time when a UserManager is created (or new-ed), a new dataProtectionProvider is generated as well. So when a user receives the email and clicks the link:

    public class AccountController : Controller
    {
        private readonly UserManager _userManager = UserManager.CreateUserManager();
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> ResetPassword(string userId, string token, FormCollection collection)
        {
            var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
            if (result != IdentityResult.Success)
                return Content(result.Errors.Aggregate("", (current, error) => current + error + "\r\n"));
            return RedirectToAction("Login");
        }
    

    The AccountController is no longer the old one, and neither are the _userManager and its token provider. So the new token provider will fail because it has no that token in it's memory.

    Thus we need to use a single instance for the token provider. Here is my new code and it works fine:

    public class UserManager : UserManager<IdentityUser>
    {
        private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
        private static readonly UserManager Instance = new UserManager();
    
        private UserManager()
            : base(UserStore)
        {
        }
    
        public static UserManager CreateUserManager()
        {
            //...
            Instance.UserTokenProvider = TokenProvider.Provider;
    
            return Instance;
        }
    

    and:

    public static class TokenProvider
    {
        [UsedImplicitly] private static DataProtectorTokenProvider<IdentityUser> _tokenProvider;
    
        public static DataProtectorTokenProvider<IdentityUser> Provider
        {
            get
            {
    
                if (_tokenProvider != null)
                    return _tokenProvider;
                var dataProtectionProvider = new DpapiDataProtectionProvider();
                _tokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());
                return _tokenProvider;
            }
        }
    }
    

    It could not be called an elegant solution, but it hit the root and solved my problem.

    0 讨论(0)
  • 2020-11-27 03:28

    tl;dr: Register custom token provider in aspnet core 2.2 to use AES encryption instead of MachineKey protection, gist: https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b

    i ran into the same issue with aspnet core 2.2, as cheny pointed out the instances of the token provider needs to be the same. this does not work for me because

    • i got different API-projects which does generate the token and receive the token to reset password
    • the APIs may run on different instances of virtual machines, so the machine key would not be the same
    • the API may restart and the token would be invalid because it is not the same instance any more

    i could use services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path")) to save the token to the file system and avoid restart and multiple instance sharing issues, but could not get around the issue with multiple projects, as each project generates a own file.

    the solution for me is to replace the MachineKey data protection logic with an own logic which does use AES then HMAC to symmetric encrypt the token with a key from my own settings which i can share across machines, instances and projects. I took the encryption logic from Encrypt and decrypt a string in C#? (Gist: https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs) and implemented a custom TokenProvider:

        public class AesDataProtectorTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
        {
            public AesDataProtectorTokenProvider(IOptions<DataProtectionTokenProviderOptions> options, ISettingSupplier settingSupplier)
                : base(new AesProtectionProvider(settingSupplier.Supply()), options)
            {
                var settingsLifetime = settingSupplier.Supply().Encryption.PasswordResetLifetime;
    
                if (settingsLifetime.TotalSeconds > 1)
                {
                    Options.TokenLifespan = settingsLifetime;
                }
            }
        }
    
        public class AesProtectionProvider : IDataProtectionProvider
        {
            private readonly SystemSettings _settings;
    
            public AesProtectionProvider(SystemSettings settings)
            {
                _settings = settings;
    
                if(string.IsNullOrEmpty(_settings.Encryption.AESPasswordResetKey))
                    throw new ArgumentNullException("AESPasswordResetKey must be set");
            }
    
            public IDataProtector CreateProtector(string purpose)
            {
                return new AesDataProtector(purpose, _settings.Encryption.AESPasswordResetKey);
            }
        }
    
        public class AesDataProtector : IDataProtector
        {
            private readonly string _purpose;
            private readonly SymmetricSecurityKey _key;
            private readonly Encoding _encoding = Encoding.UTF8;
    
            public AesDataProtector(string purpose, string key)
            {
                _purpose = purpose;
                _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
            }
    
            public byte[] Protect(byte[] userData)
            {
                return AESThenHMAC.SimpleEncryptWithPassword(userData, _encoding.GetString(_key.Key));
            }
    
            public byte[] Unprotect(byte[] protectedData)
            {
                return AESThenHMAC.SimpleDecryptWithPassword(protectedData, _encoding.GetString(_key.Key));
            }
    
            public IDataProtector CreateProtector(string purpose)
            {
                throw new NotSupportedException();
            }
        }
    

    and the SettingsSupplier i use in my project to supply my settings

        public interface ISettingSupplier
        {
            SystemSettings Supply();
        }
    
        public class SettingSupplier : ISettingSupplier
        {
            private IConfiguration Configuration { get; }
    
            public SettingSupplier(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public SystemSettings Supply()
            {
                var settings = new SystemSettings();
                Configuration.Bind("SystemSettings", settings);
    
                return settings;
            }
        }
    
        public class SystemSettings
        {
            public EncryptionSettings Encryption { get; set; } = new EncryptionSettings();
        }
    
        public class EncryptionSettings
        {
            public string AESPasswordResetKey { get; set; }
            public TimeSpan PasswordResetLifetime { get; set; } = new TimeSpan(3, 0, 0, 0);
        }
    

    finally register the provider in Startup:

     services
         .AddIdentity<AppUser, AppRole>()
         .AddEntityFrameworkStores<AppDbContext>()
         .AddDefaultTokenProviders()
         .AddTokenProvider<AesDataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider);
    
    
     services.AddScoped(typeof(ISettingSupplier), typeof(SettingSupplier));
    
    //AESThenHMAC.cs: See https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs
    
    0 讨论(0)
  • 2020-11-27 03:29

    The following solution helped me in WebApi:

    Registration

    var result = await _userManager.CreateAsync(user, model.Password);
    
    if (result.Succeeded) {
    EmailService emailService = new EmailService();
    var url = _configuration["ServiceName"];
    var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
    var encodedToken = HttpUtility.UrlEncode(token);
    
    // .Net Core 2.1, Url.Action return null
    // Url.Action("confirm", "account", new { userId = user.Id, code = token }, protocol: HttpContext.Request.Scheme);
    var callbackUrl = _configuration["ServiceAddress"] + $"/account/confirm?userId={user.Id}&code={encodedToken}";
    var message = emailService.GetRegisterMailTemplate(callbackUrl, url);
    
    await emailService.SendEmailAsync( model.Email, $"please confirm your registration {url}", message );
    }
    

    Confirm

    [Route("account/confirm")]
    [AllowAnonymous]
    [HttpGet]
    public async Task<IActionResult> ConfirmEmail(string userId, string code) {
      if (userId == null || code == null) {
        return Content(JsonConvert.SerializeObject( new { result = "false", message = "data is incorrect" }), "application/json");
      }
    
      var user = await _userManager.FindByIdAsync(userId);
      if (user == null) {
        return Content(JsonConvert.SerializeObject(new { result = "false", message = "user not found" }), "application/json");
      }
    
      //var decodedCode = HttpUtility.UrlDecode(code);
      //var result = await _userManager.ConfirmEmailAsync(user, decodedCode);
    
      var result = await _userManager.ConfirmEmailAsync(user, code);
    
      if (result.Succeeded)
        return Content(JsonConvert.SerializeObject(new { result = "true", message = "ок", token = code }), "application/json");
      else
        return Content(JsonConvert.SerializeObject(new { result = "false", message = "confirm error" }), "application/json");
    }
    
    0 讨论(0)
  • 2020-11-27 03:29

    Hit this issue with asp.net core and after a lot of digging I realised I'd turned this option on in Startup:

    services.Configure<RouteOptions>(options =>
    {
        options.LowercaseQueryStrings = true;
    });
    

    This of course invalidated the token that was in the query string.

    0 讨论(0)
  • 2020-11-27 03:31

    In my case, I just need to do HttpUtility.UrlEncode before sending an email. No HttpUtility.UrlDecode during reset.

    0 讨论(0)
  • 2020-11-27 03:32

    Here is what I did: Decode Token after encoding it for URL (in short)

    First I had to Encode the User GenerateEmailConfirmationToken that was generated. (Standard above advice)

        var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
        var encodedToken = HttpUtility.UrlEncode(token);
    

    and in your controller's "Confirm" Action I had to decode the Token before I validated it.

        var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
        var result = await userManager.ConfirmEmailAsync(user,decodedCode);
    
    0 讨论(0)
提交回复
热议问题