Asp.NET Identity 2给出"无效令牌"错误

Jul*_*urt 59 c# asp.net asp.net-mvc-5 asp.net-identity-2

我正在使用Asp.Net-Identity-2,我正在尝试使用以下方法验证电子邮件验证码.但我收到"无效令牌"错误消息.

  • 我的应用程序的用户管理器是这样的:

    public class AppUserManager : UserManager<AppUser>
    {
        public AppUserManager(IUserStore<AppUser> store) : base(store) { }
    
        public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
        {
            AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
            AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
    
            manager.PasswordValidator = new PasswordValidator { 
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = true,
                RequireUppercase = true
            };
    
            manager.UserValidator = new UserValidator<AppUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = true,
                RequireUniqueEmail = true
            };
    
            var dataProtectionProvider = options.DataProtectionProvider;
    
            //token life span is 3 hours
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider =
                   new DataProtectorTokenProvider<AppUser>
                      (dataProtectionProvider.Create("ConfirmationToken"))
                   {
                       TokenLifespan = TimeSpan.FromHours(3)
                   };
            }
    
            manager.EmailService = new EmailService();
    
            return manager;
        } //Create
      } //class
    } //namespace
    
    Run Code Online (Sandbox Code Playgroud)
  • 生成令牌的我的行动是(即使我在这里检查令牌,我得到"无效令牌"消息):

    [AllowAnonymous]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult ForgotPassword(string email)
    {
        if (ModelState.IsValid)
        {
            AppUser user = UserManager.FindByEmail(email);
            if (user == null || !(UserManager.IsEmailConfirmed(user.Id)))
            {
                // Returning without warning anything wrong...
                return View("../Home/Index");
    
            } //if
    
            string code = UserManager.GeneratePasswordResetToken(user.Id);
            string callbackUrl = Url.Action("ResetPassword", "Admin", new { Id = user.Id, code = HttpUtility.UrlEncode(code) }, protocol: Request.Url.Scheme);
    
            UserManager.SendEmail(user.Id, "Reset password Link", "Use the following  link to reset your password: <a href=\"" + callbackUrl + "\">link</a>");
    
            //This 2 lines I use tho debugger propose. The result is: "Invalid token" (???)
            IdentityResult result;
            result = UserManager.ConfirmEmail(user.Id, code);
        }
    
        // If we got this far, something failed, redisplay form
        return View();
    
    } //ForgotPassword
    
    Run Code Online (Sandbox Code Playgroud)
  • 我检查令牌的行为是(在这里,当我检查结果时,我总是得到"无效令牌"):

    [AllowAnonymous]
    public async Task<ActionResult> ResetPassword(string id, string code)
    {
    
        if (id == null || code == null)
        {
            return View("Error", new string[] { "Invalid params to reset password." });
        }
    
        IdentityResult result;
    
        try
        {
            result = await UserManager.ConfirmEmailAsync(id, code);
        }
        catch (InvalidOperationException ioe)
        {
            // ConfirmEmailAsync throws when the id is not found.
            return View("Error", new string[] { "Error to reset password:<br/><br/><li>" + ioe.Message + "</li>" });
        }
    
        if (result.Succeeded)
        {
            AppUser objUser = await UserManager.FindByIdAsync(id);
            ResetPasswordModel model = new ResetPasswordModel();
    
            model.Id = objUser.Id;
            model.Name = objUser.UserName;
            model.Email = objUser.Email;
    
            return View(model);
        }
    
        // If we got this far, something failed.
        string strErrorMsg = "";
        foreach(string strError in result.Errors)
        {
            strErrorMsg += "<li>" + strError + "</li>";
        } //foreach
    
        return View("Error", new string[] { strErrorMsg });
    
    } //ForgotPasswordConfirmation
    
    Run Code Online (Sandbox Code Playgroud)

我不知道可能会遗漏什么或者什么是错的......

che*_*eny 81

我遇到了这个问题并解决了它.有几个可能的原因.

1. URL编码问题(如果问题"随机发生")

如果这是随机发生的,您可能会遇到网址编码问题.由于未知原因,令牌不是为url-safe设计的,这意味着它在通过url传递时可能包含无效字符(例如,如果通过电子邮件发送).

在这种情况下,HttpUtility.UrlEncode(token)HttpUtility.UrlDecode(token)应使用.

正如奥雷·佩雷拉在评论中所说,UrlDecode不是(有时不是?).请试试.谢谢.

2.非匹配方法(电子邮件与密码令牌)

例如:

    var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
Run Code Online (Sandbox Code Playgroud)

    var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
Run Code Online (Sandbox Code Playgroud)

由reset-password-token-provider无法确认由email-token-provide生成的令牌.

但我们会看到导致这种情况发生的根本原因.

3.令牌提供者的不同实例

即使你使用:

var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
Run Code Online (Sandbox Code Playgroud)

随着

var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
Run Code Online (Sandbox Code Playgroud)

错误仍然可能发生.

我的旧代码说明了原因:

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(...);
    }
Run Code Online (Sandbox Code Playgroud)

和:

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;
    }
Run Code Online (Sandbox Code Playgroud)

请注意,在此代码中,每次UserManager创建(或new-ed)时,dataProtectionProvider都会生成新的.因此,当用户收到电子邮件并单击链接时:

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");
    }
Run Code Online (Sandbox Code Playgroud)

AccountController不再是旧的,也不是_userManager它的令牌提供者.因此新的令牌提供程序将失败,因为它的内存中没有该令牌.

因此,我们需要为令牌提供程序使用单个实例.这是我的新代码,它工作正常:

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;
    }
Run Code Online (Sandbox Code Playgroud)

和:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它不能称为优雅的解决方案,但它触及了根本并解决了我的问题.

  • 只有一个注意到这个很棒的答案!:)令牌必须是`UrlEncoded`,但它不应该是`UrlDecoded`,至少在MVC中作为方法参数接收时,因为它是自动解码的.如果我们再次解码它,我们使令牌无效,因为`+`字符被替换为空格. (9认同)

tra*_*max 57

因为您在此处为密码重置生成令牌:

string code = UserManager.GeneratePasswordResetToken(user.Id);
Run Code Online (Sandbox Code Playgroud)

但实际上尝试验证电子邮件的令牌:

result = await UserManager.ConfirmEmailAsync(id, code);
Run Code Online (Sandbox Code Playgroud)

这些是2种不同的代币.

在您的问题中,您说您正在尝试验证电子邮件,但您的代码是用于重置密码.你在做哪一个?

如果您需要确认电子邮件,请生成令牌

var emailConfirmationCode = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
Run Code Online (Sandbox Code Playgroud)

并通过确认

var confirmResult = await UserManager.ConfirmEmailAsync(userId, code);
Run Code Online (Sandbox Code Playgroud)

如果需要密码重置,请生成如下标记:

var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
Run Code Online (Sandbox Code Playgroud)

并确认如下:

var resetResult = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
Run Code Online (Sandbox Code Playgroud)


men*_*del 39

即使使用以下代码,我也会收到"无效令牌"错误:

var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);
Run Code Online (Sandbox Code Playgroud)

在我的情况下,问题结果是我手动创建用户并将他添加到数据库而不使用该UserManager.Create(...)方法.用户存在于数据库中但没有安全标记.

有趣的是,GenerateEmailConfirmationToken返回的令牌没有抱怨缺少安全标记,但该令牌永远无法验证.

  • 在我的情况下,用户已经从一个旧的数据库迁移,所以有安全标记,我运行它来修复它:`更新AspNetUsers SET SecurityStamp = NewID()` (5认同)
  • 我建议使用“UPDATE AspNetUsers SET SecurityStamp = NewID() WHERE SecurityStamp is null”。就我而言,某些用户的 SecurityStamp 很好,我不想打扰他们。 (4认同)

LTM*_*MOD 20

除此之外,我已经看到代码本身如果没有编码就会失败.

我最近开始以下列方式编码我的:

string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
Run Code Online (Sandbox Code Playgroud)

然后当我准备好回读它时:

string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);
Run Code Online (Sandbox Code Playgroud)

说实话,我很惊讶它首先没有被正确编码.

  • 它只需要在用作重置链接的查询字符串值时进行编码.如果您在应用程序内部提供密码重置表单(代码作为隐藏值或类似内容传递),则可以在不进行编码的情况下使用它. (4认同)

小智 15

在我的例子中,我们的AngularJS应用程序将所有加号(+)转换为空格(""),因此令牌在传回时确实无效.

要解决此问题,在AccountController中的ResetPassword方法中,我只是在更新密码之前添加了替换:

code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);
Run Code Online (Sandbox Code Playgroud)

我希望这可以帮助其他任何在Web API和AngularJS中使用Identity的人.

  • 对于更正式的方法,我建议`var callbackUrl = new Uri(Request.RequestUri, RequestContext.VirtualPathRoot).AbsoluteUri + $"#/resetPassword?username={WebUtility.UrlEncode(user.UserName)}&amp;code={WebUtility. UrlEncode(code)}";` 将用户名和代码正确 url 编码到客户端页面(例如 Angular),让用户设置密码并完成请求 (3认同)

cyp*_*tus 9

tl;dr:aspnet core 2.2 中注册自定义令牌提供程序以使用 AES 加密而不是 MachineKey 保护,要点:https : //gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b

我遇到了同样的问题aspnet core 2.2,因为 cheny 指出令牌提供程序的实例需要相同。这对我不起作用,因为

  • 我得到了different API-projects哪个生成令牌并接收令牌以重置密码
  • API 可能在different instances虚拟机上运行,因此机器密钥不会相同
  • 该API可以restart和令牌将是无效的,因为它不是same instance任何更多

我可以使用 services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path")) 将令牌保存到文件系统并避免重新启动和多个实例共享问题,但无法解决多个项目的问题,因为每个项目都会生成一个自己的文件。

我的解决方案是用自己的逻辑替换 MachineKey 数据保护逻辑,该逻辑确实用于使用AES then HMAC我自己的设置中的密钥对令牌进行对称加密,我可以在机器、实例和项目之间共享。我从Encrypt 中获取了加密逻辑 并在 C# 中解密了一个字符串? (要点:https : //gist.github.com/jbtule/4336842#file-aesthenhmac-cs)并实现了一个自定义的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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)
    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);
        }
    }
Run Code Online (Sandbox Code Playgroud)
    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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

以及我在项目中使用的 SettingsSupplier 来提供我的设置

    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);
    }
Run Code Online (Sandbox Code Playgroud)

最后在 Startup 中注册提供程序:

 services
     .AddIdentity<AppUser, AppRole>()
     .AddEntityFrameworkStores<AppDbContext>()
     .AddDefaultTokenProviders()
     .AddTokenProvider<AesDataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider);


 services.AddScoped(typeof(ISettingSupplier), typeof(SettingSupplier));
Run Code Online (Sandbox Code Playgroud)
//AESThenHMAC.cs: See https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs
Run Code Online (Sandbox Code Playgroud)


小智 7

string code = _userManager.GeneratePasswordResetToken(user.Id);

                code = HttpUtility.UrlEncode(code);
Run Code Online (Sandbox Code Playgroud)

//发送休息电子邮件


不解码代码

var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password); 
Run Code Online (Sandbox Code Playgroud)