ASP.NET Identity 2.0随机无效令牌

时间:2015-03-06 01:26:58

标签: c# asp.net asp.net-identity-2

有时,用户在点击其电子邮件确认链接时会收到无效令牌。我无法弄清楚为什么,这纯粹是随机的。

以下是创建用户的代码:

IdentityResult result = manager.Create(user, "Password134567");
if (result.Succeeded)
{
    var provider = new DpapiDataProtectionProvider("WebApp2015");
    UserManager<User> userManager = new UserManager<User>(new UserStore<User>());
    userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(provider.Create(user.Id));
    manager.UserTokenProvider = new DataProtectorTokenProvider<User>(provider.Create("ConfirmUser"));

    var emailInfo = new Email();

    string code = HttpUtility.UrlEncode(Context.GetOwinContext().GetUserManager<ApplicationUserManager>().GenerateEmailConfirmationToken(user.Id));
    string callbackUrl = IdentityHelper.GetUserConfirmationRedirectUrl(code, user.Id, Request);

    if (email.IndexOf("@") != -1)
    {
        if (assignedId == 0)
        {
            lblError.Text = "There was an error adding this user";
            return;
        }
        string emailcontent = emailInfo.GetActivationEmailContent(assignedId, callbackUrl, userRole);
        string subject = emailInfo.Subject;
        if (string.IsNullOrEmpty(subject))
        {
            subject = "Your Membership";
        }
        Context.GetOwinContext()
               .GetUserManager<ApplicationUserManager>()
               .SendEmail(user.Id, subject, emailcontent);

        if (user.EmailConfirmed)
        {
            IdentityModels.IdentityHelper.SignIn(manager, user, isPersistent: false);
            IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
        }
        else
        {
            ErrorMessage.ForeColor = Color.Green;
            ErrorMessage.Text = "An email has been sent to the user, once they verify their email they are ready to login.";
        }
    }
    else
    {
        ErrorMessage.ForeColor = System.Drawing.Color.Green;
        ErrorMessage.Text = "User has been created.";
    }

    var ra = new RoleActions();
    ra.AddUserToRoll(txtEmail.Text, txtEmail.Text, userRole);
}
else
{
    ErrorMessage.Text = result.Errors.FirstOrDefault();
}

以下是提供“无效令牌”错误的确认页面

protected void Page_Load(object sender, EventArgs e)
{
    var code = IdentityHelper.GetCodeFromRequest(Request);
    var userId = IdentityHelper.GetUserIdFromRequest(Request);
    if (code != null && userId != null)
    {
        var manager = Context.GetOwinContext()
                             .GetUserManager<ApplicationUserManager>();
        var confirmId = manager.FindById(userId);
        if (confirmId != null)
        {
            var result = manager.ConfirmEmail(userId, HttpUtility.UrlDecode(code));
            if (result.Succeeded)
            {
                return;
            }
            else
            {
                lblError.Text = result.Errors.FirstOrDefault();
                txtNewPassword.TextMode= TextBoxMode.SingleLine;
                txtNewPassword.Text = "Error contact support";
                txtNewPassword2.TextMode= TextBoxMode.SingleLine;
                txtNewPassword2.Text = result.Errors.FirstOrDefault();
                txtNewPassword.Enabled = false;
                txtNewPassword2.Enabled = false;
                imageButton1.Enabled = false;
            }
        }
        else
        {
            lblError.Text = "Account Does Not Exist";
            imageButton1.Enabled = false;
        }
    }
}

4 个答案:

答案 0 :(得分:14)

现场演示项目

我为您创建了一个精简的演示项目。它已在GitHub here上托管,并在Azure here上发布。它按设计工作(请参阅有关Azure网站的编辑),并使用与您使用的类似但不完全相同的方法。

this tutorial开始,然后我删除了这个NuGet演示代码附带的内容:

Install-Package -Prerelease Microsoft.AspNet.Identity.Samples 

出于您的目的,my demo code比NuGet示例更具相关性,因为它仅关注令牌创建和验证。特别是,看看这两个文件:

Startup.Auth.cs

我们每个应用程序启动时只会对IDataProtectionProvider进行一次实例化。

public partial class Startup
{
    public static IDataProtectionProvider DataProtectionProvider 
    { 
        get; 
        private set; 
    }

    public void ConfigureAuth(IAppBuilder app)
    {
        DataProtectionProvider = 
            new DpapiDataProtectionProvider("WebApp2015");

        // other code removed
    }
}

AccountController.cs

然后在AccountController内,我们使用静态提供程序而不是创建新提供程序。

userManager.UserTokenProvider = 
    new DataProtectorTokenProvider<User>(
        Startup.DataProtectionProvider.Create("UserToken"));

这样做可能会删除您所看到的错误。以下是您在进一步排查时需要考虑的一些问题。

您使用两种不同的UserTokenProvider目的吗?

DataProtectorTokenProvider.Create(string[] purposes)方法采用purposes参数。以下是MSDN对此的评价:

  

的目的。用于确保受保护数据的附加熵可能仅为正确目的而不受保护。

当您创建用户code时,您正在使用(至少)两个不同的目的:

  1. user.Id
  2. "ConfirmUser"
  3. 您使用ApplicationUserManager检索的GetOwinContext()...的目的。
  4. 这里将您的代码作为代码段。

    userManager.UserTokenProvider = 
        new DataProtectorTokenProvider<User>(provider.Create(user.Id));
    
    manager.UserTokenProvider = 
        new DataProtectorTokenProvider<User>(provider.Create("ConfirmUser"));
    
    string code = Context
        .GetOwinContext()
        .GetUserManager<ApplicationUserManager ()      
        .GenerateEmailConfirmationToken(user.Id)
    

    验证code时,您可能使用了错误的用途。您在哪里为UserTokenProvider指定用于确认电子邮件的ApplicationUserManager?它的目的论点必须相同!

    var manager = Context.GetOwinContext()
                      .GetUserManager<ApplicationUserManager>();
    
    var result = manager.ConfirmEmail(userId, HttpUtility.UrlDecode(code));
    

    令牌很有可能无效,因为您有时使用不同的UserTokenProvider创建目的而不是您用于验证。

    为什么会这样呢?彻底搜索您的代码,找到分配给UserTokenProvider的所有地方。也许你在某个意外的地方覆盖它(比如在属性或IdentityConfig.cs文件中),这样它似乎是随机的。

    TokenLifespan已过期吗?

    您已经提到无效令牌消息是随机出现的。可能是令牌已过期。 This tutorial注意到默认生命周期为一天。您可以这样更改:

    manager.UserTokenProvider = 
        new DataProtectorTokenProvider<ApplicationUser>
          (dataProtectionProvider.Create("WebApp2015"))
          {                    
             TokenLifespan = TimeSpan.FromHours(3000)
          };
    

    为什么有三个UserManager个实例?

    以下是有关创建确认令牌的代码的一些注释。您似乎正在使用三个单独的UserManager实例,包括派生的ApplicationUserManager类型。那是什么意思?

    1. 这里manager的类型是什么?
    2. 为什么要创建userManager而不是使用现有的manager
    3. 为什么使用manager.UserTokenProvider而不是userManager.UserTokenProvider
    4. 为什么从UserManager
    5. 获得第三个Context个实例?

      请注意,我删除了大量代码,专注于您的令牌创建。

      // 1. 
      IdentityResult result = manager.Create(user, "Password134567");
      
      if (result.Succeeded)
      {
          var provider = new DpapiDataProtectionProvider("WebApp2015");
      
          // 2. 
          UserManager<User> userManager = 
              new UserManager<User>(new UserStore<User>());
      
          userManager.UserTokenProvider = 
              new DataProtectorTokenProvider<User>(provider.Create(user.Id));
      
          // 3.
          manager.UserTokenProvider = 
              new DataProtectorTokenProvider<User>(provider.Create("ConfirmUser"));
      
          // 4. 
          string raw = Context.GetOwinContext()
                       .GetUserManager<ApplicationUserManager>()
                       .GenerateEmailConfirmationToken(user.Id)
      
          // remaining code removed
      }
      

      我想知道我们是否可以简化以上操作,只使用一个UserManager实例,如下所示。

      // 1. 
      IdentityResult result = manager.Create(user, "Password134567");
      
      if (result.Succeeded)
      {
          var provider = new DpapiDataProtectionProvider("WebApp2015");
      
          manager.UserTokenProvider = 
              new DataProtectorTokenProvider<User>(provider.Create(user.Id));
      
          // 3.
          var provider = provider.Create("ConfirmUser");
          manager.UserTokenProvider = 
              new DataProtectorTokenProvider<User>(provider);
      
          // 4. 
          string raw = manager.GenerateEmailConfirmationToken(user.Id);
      
          // remaining code removed
      }
      

      如果您使用此方法,请确保在确认电子邮件时使用相同的"ConfirmUser"目的参数。

      IdentityHelper内的内容是什么?

      由于错误是随机发生的,因此我发现IdentityHelper方法可能会对code的某些东西做一些时髦的事情。这些方法中的内容是什么?

      • IdentityHelper.GetUserConfirmationRedirectUrl()
      • IdentityHelper.RedirectToReturnUrl()
      • IdentityHelper.GetCodeFromRequest()
      • IdentityHelper.GetUserIdFromRequest()

      我可能会编写一些测试来确保您的流程创建的原始code始终与您的流程从code检索的原始Request匹配。在伪代码中:

      var code01 = CreateCode();
      var code02 = UrlEncode(code01);
      var request = CreateTheRequest(code02);
      var response = GetTheResponse();
      var code03 = GetTheCode(response);
      var code04 = UrlDecode(code03);
      Assert.AreEquals(code01, code04);
      

      运行上述10,000次以确保不存在任何问题。

      结论

      我强烈怀疑问题在于在令牌创建期间使用一个purposes参数而在确认期间使用另一个app.GetDataProtectionProvider()参数。仅用一个目的,你可能没事。

      在Azure网站上进行此工作

      1. 使用SqlCompact而不是localdb。
      2. 使用DpapiDataProtectionProvider而非{{1}},因为Dpapi不适用于网络农场。

答案 1 :(得分:2)

网站是否托管在多个网络服务器上? 如果是这样,您不能在这里使用DPAPI。它是特定于机器的。 您需要使用其他数据保护提供商。

答案 2 :(得分:1)

代码中可能存在一些无效字符(令牌)。因此,当它出现在任何网址中时,我们需要使用HttpUtility.UrlEncode(token)HttpUtility.UrlDecode(token)

在此处查看详情: Identity password reset token is invalid

答案 3 :(得分:1)

虽然我认为肖恩提供了很好的反馈来解决这些问题。你提出的一条评论让我觉得它可能只是一个替代的令牌问题。另请参阅有关多个服务器和令牌的注释。

  

......这完全是随机的

我不认为它是随机的;-)。但是对于一些用户来说,有什么能让它在很多时候起作用呢。

为不同的页面或APP生成令牌,或者来自同一APP且刚刚过期的令牌。例如,令牌在短期内有效。存在于浏览器中。令牌来自类似的应用程序或来自不同表单/页面上的同一应用程序。 自从为域生成令牌以来,浏览器显示令牌。

但是在分析时,令牌没有匹配,或者刚刚过期。

考虑令牌的生命周期。