ASP.NET身份验证2.0: 无效令牌的随机性

6
有时用户在点击电子邮件确认链接时会收到无效令牌的提示。我不知道为什么,这完全是随机的。
以下是创建用户的代码:
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;
        }
    }
}

确认链接是什么样子的?你是在进行重定向还是点击事件? - tdbeckett
开始记录已创建和请求的令牌,成功和失败的情况,以查看差异。这可能会提示发生了什么。 - Brian from state farm
您是否验证了用户使用的链接是完全有效的链接?电子邮件因打破文本块而臭名昭著,而且几个电子邮件客户端标记处理将从断开的引用链接创建链接。这意味着用户通常会拥有正确的基本URL,但传递的参数或令牌却是错误的。 - StarPilot
4个回答

15

演示项目

我为您创建了一个简化的演示项目。它托管在GitHub这里,并且在Azure这里上线。它按设计工作(有关Azure网站的编辑请参见下文),并且使用类似但并非完全相同的方法,与您所使用的方法有所不同。

它是从这个教程开始的,然后我删除了这个NuGet演示代码中的累赘部分:

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

对于您的目的,我的演示代码比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对此的解释:

purposes。用于确保受保护的数据只能出于正确的目的而不受保护的附加熵。

当您创建用户code时,您至少使用了两种不同的目的:

  1. user.Id
  2. "ConfirmUser"
  3. 使用GetOwinContext()...检索到的ApplicationUserManager的目的。

这是作为代码片段的代码:

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时,您可能使用了错误的目的。您在哪里为用于确认电子邮件的ApplicationUserManager分配UserTokenProvider?它的目的参数必须相同!

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

var result = manager.ConfirmEmail(userId, HttpUtility.UrlDecode(code));

由于您有时在创建Token时使用了不同的,导致Token无效的可能性很大。请仔细查找代码中所有分配给的位置。也许您在意外的地方覆盖了它(例如在属性或IdentityConfig.cs文件中),因此看起来是随机的。

TokenLifespan是否已过期?

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

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

为什么要使用三个实例?

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

  1. manager的类型是什么?
  2. 为什么创建userManager而不使用现有的manager
  3. 为什么使用manager.UserTokenProvider而不是userManager.UserTokenProvider
  4. 为什么要从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始终与您的过程从Request检索的原始code匹配。伪代码如下:

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参数,在确认期间使用另一个。只使用一个目的,你可能会没问题。

让这在Azure网站上工作

  1. 使用SqlCompact而不是localdb。
  2. 使用app.GetDataProtectionProvider()而不是DpapiDataProtectionProvider,因为Dpapi不能与Web Farm一起使用。

1
TokenLifespan 加 1! - Dunc

2

这个网站是否托管在多个web服务器上? 如果是的话,您不能在此处使用DPAPI,因为它是针对特定机器的。 您需要使用另一个数据保护提供程序。


1
虽然我认为Shaun提供了很好的反馈来解决这些问题。但你所说的一条评论让我想到它可能是一个备用令牌问题。另请参阅关于多个服务器和令牌的注释。
“...它完全是随机的” 我不认为它是随机的;-)。但是,什么会让它在大多数时间内工作,而对某些用户则不起作用。
一个令牌是为不同的页面或应用程序生成的,或者来自相同的应用程序,并且已经过期。例如,令牌有效期很短。存在于浏览器中。该令牌来自类似的应用程序或来自同一应用程序的不同表单/页面。
浏览器呈现令牌,因为该令牌是为该域生成的。
但是当分析令牌时,它与之前的令牌不匹配,或者刚刚过期。
考虑令牌的生命周期。

1

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

在这里查看更多细节: 身份密码重置令牌无效


请在此处查看更多信息:https://dev59.com/D18e5IYBdhLWcg3wwMlW#28983864,可能会有多个令牌验证器(可能未被注意到)。 - cheny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接