ASP.NET Identity 2.0随机无效令牌

pro*_*tor 6 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();
}
Run Code Online (Sandbox Code Playgroud)

这是提供"无效令牌"错误的确认页面

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

Sha*_*tin 14

现场演示项目

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

它从本教程开始,然后我删除了这个NuGet演示代码附带的内容:

Install-Package -Prerelease Microsoft.AspNet.Identity.Samples 
Run Code Online (Sandbox Code Playgroud)

出于您的目的,我的演示代码比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
    }
}
Run Code Online (Sandbox Code Playgroud)

AccountController.cs.

然后AccountController,我们使用静态提供程序而不是创建一个新提供程序.

userManager.UserTokenProvider = 
    new DataProtectorTokenProvider<User>(
        Startup.DataProtectionProvider.Create("UserToken"));
Run Code Online (Sandbox Code Playgroud)

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

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

DataProtectorTokenProvider.Create(string[] purposes)方法需要一个purposes参数.以下是MSDN对此的评价:

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

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

  1. user.Id
  2. "ConfirmUser"
  3. ApplicationUserManager你检索的目的 GetOwinContext()....

这是您的代码作为代码段.

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

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

var manager = Context.GetOwinContext()
                  .GetUserManager<ApplicationUserManager>();

var result = manager.ConfirmEmail(userId, HttpUtility.UrlDecode(code));
Run Code Online (Sandbox Code Playgroud)

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

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

已经TokenLifespan过期了吗?

您已经提到无效令牌消息是随机出现的.可能是令牌已过期.本教程指出默认生命周期为一天.您可以像这样更改它:

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

为什么三个UserManager实例?

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

  1. 这是什么类型的manager
  2. 为什么要创建一个userManager而不是使用现有的manager
  3. 为什么manager.UserTokenProvider不使用userManager.UserTokenProvider
  4. 你为什么UserManager要从第三个实例中获得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
}
Run Code Online (Sandbox Code Playgroud)

我想知道我们是否可以简化以上只使用一个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
}
Run Code Online (Sandbox Code Playgroud)

如果您使用此方法,请确保"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);
Run Code Online (Sandbox Code Playgroud)

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

结论

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

在Azure网站上进行此工作

  1. 使用SqlCompact而不是localdb.
  2. 使用app.GetDataProtectionProvider()不是DpapiDataProtectionProvider因为Dpapi不适用于Web场.