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

Posted

技术标签:

【中文标题】Asp.NET Identity 2 给出“无效令牌”错误【英文标题】:Asp.NET Identity 2 giving "Invalid Token" error 【发布时间】:2014-10-13 20:17:54 【问题描述】:

我正在使用 Asp.Net-Identity-2 并尝试使用以下方法验证电子邮件验证码。但我收到 "Invalid Token" 错误消息。

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

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

我生成令牌的操作是(即使我在此处检查令牌,我也会收到“无效令牌”消息):

[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

我检查令牌的操作是(在这里,当我检查结果时,我总是得到“无效令牌”):

[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

我不知道可能缺少什么或出了什么问题......

【问题讨论】:

【参考方案1】:

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

1。 URL 编码问题(如果问题“随机”发生)

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

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

正如 oão Pereira 在他的 cmets 中所说,UrlDecode 不是(或有时不是?)必需的。请尝试两者。谢谢。

2。不匹配的方法(电子邮件与密码令牌)

例如:

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

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

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

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

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

即使你正在使用:

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

随着

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

错误仍然可能发生。

我的旧代码说明了原因:

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(...);
    

和:

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;
    

请注意,在这段代码中,每次创建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");
    

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;
    

和:

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;
        
    

它不能称为优雅的解决方案,但它击中了根源并解决了我的问题。

【讨论】:

对这个很棒的答案只有一个注释! :) 令牌必须是 UrlEncoded,但它不应该是 UrlDecoded,至少在 MVC 中作为方法参数接收时,因为它是自动解码的。如果我们再次对其进行解码,我们会使令牌无效,因为 + 字符被替换为空格。 这解决了我的问题,因为我需要使用令牌across projects, instances and computers。我为此实现了一个自定义AES encryption,详情请参阅我的答案:***.com/a/56355282/1216595 使用解决方案 #3 解决了 Asp Net Core 3.1 应用程序中的问题 我再次点击了这个答案。上次我使用 UserManager 的单个实例来解决它,该实例注册了使用 UserManager 作为单例的服务。相反,在这个其他项目中,如果我做同样的事情,它会抛出一个异常,说我不能将该服务注册为单例,因为 UserManager 需要一个瞬态范围。您上面的解决方案无法编译(我可以报告很多问题)。那么什么可能是有效的解决方法呢?问题显然是#3(令牌提供者的不同实例) 我又修好了。该问题是由 User 表中缺少 SecurityStamp 列引起的。我已将其删除,但没有该列将无法正常工作【参考方案2】:

因为您在这里生成密码重置令牌:

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

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

result = await UserManager.ConfirmEmailAsync(id, code);

这是 2 个不同的令牌。

在您的问题中,您说您正在尝试验证电子邮件,但您的代码用于密码重置。你在做什么?

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

var emailConfirmationCode = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);

并通过

确认
var confirmResult = await UserManager.ConfirmEmailAsync(userId, code);

如果您需要重置密码,请生成这样的令牌:

var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);

并像这样确认:

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

【讨论】:

如何使 ConfirmEmailAsync 返回 failed 而不是成功,如果该令牌已被使用一次。喜欢用户尝试从他/她的电子邮件地址重新访问链接? @user2904995 使令牌无效,您需要更改SecurityStamp 字段。这将使所有以前有效的令牌失效,包括过去使用过的令牌。【参考方案3】:

即使使用如下代码,我也收到“无效令牌”错误:

var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);

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

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

【讨论】:

在我的情况下,用户是从旧数据库迁移的,因此安全标记为空,我运行它来修复它:UPDATE AspNetUsers SET SecurityStamp = NewID() 我建议使用 UPDATE AspNetUsers SET SecurityStamp = NewID() WHERE SecurityStamp is null 。就我而言,某些用户的 SecurityStamp 很好,我不想惹他们。 要记住的一点是,留给自己的设备的 Identity 生成小写的 guid,而 NewID() 返回一个大写的 guid(至少在 SSMS 中)。考虑使用 LOWER(NewID()) 对我来说实际上是在检查令牌。我通过我的 repo 而不是 UserManager 拉用户,因此我的 repo 用户称为 ResetPasswordAsync。基本相同的问题【参考方案4】:

除此之外,如果没有编码,我还看到代码本身会失败。

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

string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);

然后当我准备回读时:

string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);

说实话,我很惊讶它一开始就没有被正确编码。

【讨论】:

仅在作为reset链接的查询字符串值时才需要编码。如果您在应用程序内部提供密码重置表单,其中代码作为隐藏值或类似的东西传递,则可以在不编码的情况下使用它。 嘿,先生。感谢您的光临!我在 ASP.NET Core 5.0 中通过以下方式做到了:Encoding.UTF8.GetString(Convert.FromBase64String(code));【参考方案5】:

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

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

code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);

我希望这可以帮助其他在 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),以让用户设置密码并完成请求 默认token是base64编码的,不是URL安全的,需要进行URL编码。您可以覆盖或包装令牌提供程序,并返回 base64url 编码的令牌,避免像您已经做过的特殊字符。【参考方案6】:

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 and decrypt a string in 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;
            
        
    
    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();
        
    

以及我在项目中使用的 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);
    

最后在 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

【讨论】:

【参考方案7】:
string code = _userManager.GeneratePasswordResetToken(user.Id);

                code = HttpUtility.UrlEncode(code);

//发送休息邮件


不要解码代码

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

【讨论】:

您对 not 解码代码的评论对我不起作用。只有解码代码才会成功。 @AaronHudon 可能取决于您是通过 url 字符串还是在请求正文(帖子)中发送它。 这似乎取决于您使用的是 WebAPI 还是 MVC 控制器。 MVC控制器URL上的模型Binder默认解码!【参考方案8】:

这是我所做的:在为 URL 编码后解码令牌(简而言之)

首先我必须对生成的用户 GenerateEmailConfirmationToken 进行编码。 (标准以上建议)

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

在您的控制器的“确认”操作中,我必须在验证令牌之前对其进行解码。

    var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
    var result = await userManager.ConfirmEmailAsync(user,decodedCode);

【讨论】:

【参考方案9】:

在这里我遇到了同样的问题,但经过很长时间后,我发现在我的情况下,无效令牌错误是由于我的自定义 Account 类重新声明和覆盖了 Id 属性而引发的。

像这样:

 public class Account : IdentityUser
 
    [ScaffoldColumn(false)]
    public override string Id  get; set;  
    //Other properties ....
 

为了解决这个问题,我刚刚删除了该属性并再次生成了数据库模式以确保安全。

删除它可以解决问题。

【讨论】:

【参考方案10】:

用 asp.net core 解决这个问题,经过大量挖掘后,我意识到我已经在 Startup 中打开了这个选项:

services.Configure<RouteOptions>(options =>

    options.LowercaseQueryStrings = true;
);

这当然会使查询字符串中的令牌无效。

【讨论】:

完全正确!!!你拯救了我的一天,谢谢。【参考方案11】:

以下解决方案在 WebApi 中帮助了我:

注册

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 );

确认

[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");

【讨论】:

奇妙的var encodedToken = HttpUtility.UrlEncode(token); 做了魔法【参考方案12】:

受@cheny 发布的解决方案#3 的启发,我意识到如果您使用相同的UserManager 实例,则生成的代码将被接受。但在实际场景中,验证代码发生在用户单击电子邮件链接后的第二次 API 调用中。 这意味着创建了UserManager 的新实例,并且无法验证第一次调用的第一个实例生成的代码。使其工作的唯一方法是确保在数据库用户表中有SecurityStamp 列。 将使用 UserManager 的类注册为单例会在应用程序启动时引发异常,因为 UserManager 类会自动注册为 Scoped 生命周期

【讨论】:

谢谢,我在用户表中填写了 SecurityStamp 列,它解决了我的问题。【参考方案13】:

确保在生成时使用:

GeneratePasswordResetTokenAsync(user.Id)

并确认您使用:

ResetPasswordAsync(user.Id, model.Code, model.Password)

如果您确定使用了匹配方法,但仍然无法正常工作,请验证user.Id 在两种方法中是否相同。 (有时您的逻辑可能不正确,因为您允许使用相同的电子邮件进行注册表等)

【讨论】:

【参考方案14】:

也许这是一个旧线程,但就这种情况而言,我一直在为这个错误的随机发生而摸不着头脑。我一直在检查所有线程并验证每个建议,但 - 随机似乎 - 一些返回为“无效令牌”的代码。 在对用户数据库进行一些查询后,我终于发现那些“无效令牌”错误与用户名中的空格或其他非字母数字字符直接相关。 那时很容易找到解决方案。只需配置 UserManager 以允许这些字符出现在用户名中。 这可以在用户管理器创建事件之后完成,以这种方式添加一个新的 UserValidator 设置以使相应的属性为 false:

 public static UserManager<User> Create(IdentityFactoryOptions<UserManager<User>> options, IOwinContext context)
    
        var userManager = new UserManager<User>(new UserStore());

        // this is the key 
        userManager.UserValidator = new UserValidator<User>(userManager)  AllowOnlyAlphanumericUserNames = false ;


        // other settings here
        userManager.UserLockoutEnabledByDefault = true;
        userManager.MaxFailedAccessAttemptsBeforeLockout = 5;
        userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(1);

        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        
            userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"))
            
                TokenLifespan = TimeSpan.FromDays(5)
            ;
        

        return userManager;
    

希望这可以帮助像我这样的“迟到者”!

【讨论】:

关于编码/解码以避免空格和其他符号干扰,我正在使用这个很有魅力的建议:***.com/questions/27535233/…【参考方案15】:

确保您生成的令牌不会很快过期 - 我已将其更改为 10 秒进行测试,它总是会返回错误。

    if (dataProtectionProvider != null) 
        manager.UserTokenProvider =
           new DataProtectorTokenProvider<AppUser>
              (dataProtectionProvider.Create("ConfirmationToken")) 
               TokenLifespan = TimeSpan.FromHours(3)
               //TokenLifespan = TimeSpan.FromSeconds(10);
           ;
    

【讨论】:

【参考方案16】:

我们在一组用户中遇到了这种情况,并且一切正常。我们已将其隔离到赛门铁克的电子邮件保护系统,该系统将我们电子邮件中的链接替换为安全链接,这些链接会转到他们的网站进行验证,然后将用户重定向到我们发送的原始链接。

问题是他们正在引入解码...他们似乎在生成的链接上执行 URL 编码,以将我们的链接作为查询参数嵌入到他们的站点,但是当用户单击并 clicksafe.symantec.com 解码时它解码了他们需要编码的第一部分的url,还有我们的查询字符串的内容,然后浏览器被重定向到的URL已经被解码,我们又回到了特殊字符弄乱查询字符串处理的状态后面的代码。

【讨论】:

【参考方案17】:

就我而言,我只需要在发送电子邮件之前执行 HttpUtility.UrlEncode。重置期间没有 HttpUtility.UrlDecode。

【讨论】:

【参考方案18】:

与chenny的3有关。令牌提供者的不同实例

在我的例子中,我每次调用 IDataProtectionProvider.Create 时都会传递一个新的 guid,这会阻止现有代码在后续的 web api 调用中被识别(每个请求都会创建自己的用户管理器)。

将字符串设为静态为我解决了这个问题。

private static string m_tokenProviderId = "MyApp_" + Guid.NewGuid().ToString();
...
manager.UserTokenProvider =
  new DataProtectorTokenProvider<User>(
  dataProtectionProvider.Create(new string[1]  m_tokenProviderId  ))
  
      TokenLifespan = TimeSpan.FromMinutes(accessTokenLifespan)
  ;

【讨论】:

【参考方案19】:

如果有人遇到这种情况,事实证明该令牌不是 URL 友好的,因此我不得不将它包装在 HttpUtility.UrlEncode() 中,如下所示:

var callback = Url.Content($"this.Request.Scheme://this.Request.Hostthis.Request.PathBase/reset-password?token=HttpUtility.UrlEncode(token)&email=user.Email");

【讨论】:

【参考方案20】:

我已经解决了大部分描述的提示“无效令牌”问题。这是我的 blazor 项目解决方案。核心在StringExtensions 类中。

当用户注册他/她的电子邮件时生成电子邮件:

user = new IdentityUser  UserName = email, Email = email ;
var createUser = await _userManager.CreateAsync(user, password);
 if (createUser.Succeeded)
  
      var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
      var baseUri = NavMgr.BaseUri;
      var setNewPasswordUri = baseUri + "confirm-password";
      var urlWithParams = StringExtensions.GenerateUrl(token, emailTo, url);
      await SendAsync( urlWithParams  );   // use your own Email solution send the email
  

电子邮件确认(用户点击邮件中的链接)

@page  "/confirm-email"
 
<h3>Confirm email</h3>
 
@Error
        [Inject]
        UserManager<IdentityUser> UserMgr  get; set; 
 
        [Inject] 
        NavigationManager NavMgr  get; set; 
 
 
        protected override Task OnInitializedAsync()
        
            var url = NavMgr.Uri;
            Token = StringExtensions.GetParamFromUrl(url, "token");
            Email = StringExtensions.GetParamFromUrl(url, "email");
            log.Trace($"Initialised with email=Email , token=Token");
            return ActivateEmailAsync();
        
 
 
        private async Task ActivateEmailAsync()
        
            isProcessing = true;
            Error = null;
 
            log.Trace($"ActivateEmailAsync started for Email");
            isProcessing = true;
            Error = null;
 
            try
            
                var user = await UserMgr.FindByEmailAsync(Email);
                if (user != null)
                
                        if (!string.IsNullOrEmpty(Token))
                        
                            var result = await UserMgr.ConfirmEmailAsync(user, Token);
                            if (result.Succeeded)
                            
// Show user , that account is activated
                            
                            else
                            
                                foreach (var error in result.Errors)
                                
                                    Error += error.Description;
                                
                                log.Error($"Setting new password failed for Email due to the: Error");
                            
                        
                        else
                        
                            log.Error("This should not happen. Token is null or empty");
                        
                
 
            
            catch (Exception exc)
            
                Error = $"Activation failed";
            
            isProcessing = false;
        
 public static class StringExtensions
    
        /// <summary>
        /// Encode string to be safe to use it in the URL param
        /// </summary>
        /// <param name="toBeEncoded"></param>
        /// <returns></returns>
        public static string Encode(string toBeEncoded)
        
            var result = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(toBeEncoded));
            return result;
        
 
        /// <summary>
        /// Decode from the url safe string the original value
        /// </summary>
        /// <param name="toBeDecoded"></param>
        /// <returns></returns>
        public static string Decode(string toBeDecoded)
        
            var decodedBytes = WebEncoders.Base64UrlDecode(toBeDecoded);
            var result = Encoding.UTF8.GetString(decodedBytes);
            return result;
        
 
 
        public static string GenerateUrl(string token, string emailTo, string baseUri, string tokenParamName = "token", string emailParamName = "email")
        
            var tokenEncoded = StringExtensions.Encode(token);
            var emailEncoded = StringExtensions.Encode(emailTo);
            var queryParams = new Dictionary<string, string>();
            queryParams.Add(tokenParamName, tokenEncoded);
            queryParams.Add(emailParamName, emailEncoded);
            var urlWithParams = QueryHelpers.AddQueryString(baseUri, queryParams);
            return urlWithParams;
        
 
 
        public static string GetParamFromUrl(string uriWithParams, string paramName)
        
            var uri = new Uri(uriWithParams, UriKind.Absolute);
            var result = string.Empty;
 
            if (QueryHelpers.ParseQuery(uri.Query).TryGetValue(paramName, out var paramToken))
            
                var queryToken = paramToken.First();
                result  = StringExtensions.Decode(queryToken);
            
            return result;
        
 

【讨论】:

【参考方案21】:

我在重置密码场景中遇到了无效令牌。根本原因是,我正在为不正确的IndentityUser 生成重置令牌。在简化的代码中可以很容易地发现它,但是在更复杂的代码中我花了一些时间来修复它。

我应该使用代码:

 var user = await UserMgr.FindByEmailAsync(Model.Email);
 string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);

但我错了(创建另一个IndentityUser)。

 // This is example "How it should not be done"
 var user = await UserMgr.FindByEmailAsync(Model.Email);
 
 user = new IdentityUser  UserName = email, Email = email ;  // This must not be her !!!! We need to use user found by UserMgr.FindByEmailAsync(Model.Email);
 string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);

完整的简化代码在这里:

private async Task GenerateResetToken()

    var user = await UserMgr.FindByEmailAsync(Model.Email);
    if (user == null)
    
        Model.Error = "Not registered";
    
    else
    
        try
        
            var _userManager = SignInMgr.UserManager;

            UserMgr.FindByEmailAsync(Model.Email);
            string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
            if (resetToken == null)
            
                log.Error("Cannot get token from GeneratePasswordResetTokenAsync");
            
            else
            
                // Reset token generated. Send email to user
            

        
        catch (Exception exc)
        
            log.Error(exc, $"Password reset failed  due to the exc.Message");
        
    

【讨论】:

【参考方案22】:

我的问题是包含 ConfirmationToken 的电子邮件中有错字:

<p>Please confirm your account by <a href=@ViewBag.CallbackUrl'>clicking here</a>.</p>

这意味着额外的撇号被附加到 ConfirmationToken 的末尾。

天啊!

【讨论】:

【参考方案23】:

我的问题是我的重置密码表单中缺少&lt;input asp-for="Input.Code" type="hidden" /&gt; 控件

<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />

【讨论】:

以上是关于Asp.NET Identity 2 给出“无效令牌”错误的主要内容,如果未能解决你的问题,请参考以下文章

将ASP.NET Identity模型移动到类库

Asp.net Core 2.0 Identity.TwoFactorRememberMe 到期

Asp.Net Core 2.1 Identity - UserStore 依赖注入

Microsoft 'Identity Platform' 是不是(轻松)与'(ASP.NET) Identity Framework' - MVC5 & .NET 4.7.2 集成?

ASP.Net Identity 2 使用 SMS 密码登录 - 不使用双因素身份验证

ASP.NET Identity 2.0 注销不起作用