ASP.NET Identity 2.0 令牌随机无效
ASP.NET Identity 2.0 Invalid Token Randomly
有时用户在点击他们的电子邮件确认时会收到无效令牌 link。想不通为什么,纯属随机
这是创建用户的代码:
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();
}
这是给出 'invalid token' 错误的确认页面
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;
}
}
}
代码(令牌)中可能有一些 url-无效字符。因此,当 HttpUtility.UrlEncode(token)
和 HttpUtility.UrlDecode(token)
出现在任何 url 中时,我们需要使用它。
在此处查看详细信息:
Identity password reset token is invalid
网站是否托管在多个网络服务器上?
如果是这样,您不能在此处使用 DPAPI。它是特定于机器的。
您需要使用其他数据保护提供商。
现场演示项目
我已经为您创建了一个简化的演示项目。它托管在 GitHub here and is live on 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 对此的评论:
purposes. Additional entropy used to ensure protected data may only be unprotected for the correct purposes.
当您创建用户 code
时,您(至少)有两个不同的用途:
user.Id
"ConfirmUser"
和
- 您使用
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));
令牌无效的可能性很大,因为您有时使用不同的UserTokenProvider
创建目的而不是用于验证。
为什么有时会这样?彻底搜索您的代码以找到分配给 UserTokenProvider
的所有位置。也许你在意想不到的地方覆盖了它(比如在 属性 或 IdentityConfig.cs 文件中),所以它看起来是随机的。
TokenLifespan
过期了吗?
您提到无效令牌消息是随机出现的。可能是令牌已过期。 This tutorial 注意到默认的生命周期是一天。您可以这样更改它:
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>
(dataProtectionProvider.Create("WebApp2015"))
{
TokenLifespan = TimeSpan.FromHours(3000)
};
为什么三个 UserManager
个实例?
以下是对创建确认令牌的代码的一些评论。您似乎正在使用三个单独的 UserManager
实例,包括派生的 ApplicationUserManager
类型。那是什么?
- 这里
manager
的类型是什么?
- 为什么要创建
userManager
而不是使用现有的 manager
?
- 为什么使用
manager.UserTokenProvider
而不是 userManager.UserTokenProvider
?
- 为什么要从
Context
中获取第三个 UserManager
实例?
请注意,我删除了很多代码以专注于您的令牌创建。
// 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);
运行以上10000次确保无问题
结论
我强烈怀疑问题在于在令牌创建期间使用了一个 purposes
参数,在确认期间使用了另一个参数。仅用于一个目的,您可能会没事。
在 Azure 网站上实现此功能
- 使用 SqlCompact 代替 localdb。
- 使用
app.GetDataProtectionProvider()
而不是 DpapiDataProtectionProvider
因为 Dpapi 不适用于网络场。
虽然我认为 Shaun 提供了很好的反馈来解决此类问题。你发表的一条评论让我认为这可能是一个备用令牌问题。另请参阅有关多个服务器和令牌的评论。
... it's purely random
我不认为它是随机的 ;-)。但是,对于某些用户来说,是什么让它在大部分时间都无法正常工作。
令牌是为不同的页面或APP生成的,或者来自同一个APP并且刚刚过期。例如,短期有效的令牌。存在于浏览器中。令牌来自类似的应用程序或来自不同 form/page 上的同一应用程序。
浏览器显示令牌,因为令牌是为域生成的。
但是在分析时令牌不匹配,或者刚刚过期。
考虑代币的生命周期。
有时用户在点击他们的电子邮件确认时会收到无效令牌 link。想不通为什么,纯属随机
这是创建用户的代码:
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();
}
这是给出 'invalid token' 错误的确认页面
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;
}
}
}
代码(令牌)中可能有一些 url-无效字符。因此,当 HttpUtility.UrlEncode(token)
和 HttpUtility.UrlDecode(token)
出现在任何 url 中时,我们需要使用它。
在此处查看详细信息: Identity password reset token is invalid
网站是否托管在多个网络服务器上? 如果是这样,您不能在此处使用 DPAPI。它是特定于机器的。 您需要使用其他数据保护提供商。
现场演示项目
我已经为您创建了一个简化的演示项目。它托管在 GitHub here and is live on 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 对此的评论:
purposes. Additional entropy used to ensure protected data may only be unprotected for the correct purposes.
当您创建用户 code
时,您(至少)有两个不同的用途:
user.Id
"ConfirmUser"
和- 您使用
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));
令牌无效的可能性很大,因为您有时使用不同的UserTokenProvider
创建目的而不是用于验证。
为什么有时会这样?彻底搜索您的代码以找到分配给 UserTokenProvider
的所有位置。也许你在意想不到的地方覆盖了它(比如在 属性 或 IdentityConfig.cs 文件中),所以它看起来是随机的。
TokenLifespan
过期了吗?
您提到无效令牌消息是随机出现的。可能是令牌已过期。 This tutorial 注意到默认的生命周期是一天。您可以这样更改它:
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>
(dataProtectionProvider.Create("WebApp2015"))
{
TokenLifespan = TimeSpan.FromHours(3000)
};
为什么三个 UserManager
个实例?
以下是对创建确认令牌的代码的一些评论。您似乎正在使用三个单独的 UserManager
实例,包括派生的 ApplicationUserManager
类型。那是什么?
- 这里
manager
的类型是什么? - 为什么要创建
userManager
而不是使用现有的manager
? - 为什么使用
manager.UserTokenProvider
而不是userManager.UserTokenProvider
? - 为什么要从
Context
中获取第三个UserManager
实例?
请注意,我删除了很多代码以专注于您的令牌创建。
// 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);
运行以上10000次确保无问题
结论
我强烈怀疑问题在于在令牌创建期间使用了一个 purposes
参数,在确认期间使用了另一个参数。仅用于一个目的,您可能会没事。
在 Azure 网站上实现此功能
- 使用 SqlCompact 代替 localdb。
- 使用
app.GetDataProtectionProvider()
而不是DpapiDataProtectionProvider
因为 Dpapi 不适用于网络场。
虽然我认为 Shaun 提供了很好的反馈来解决此类问题。你发表的一条评论让我认为这可能是一个备用令牌问题。另请参阅有关多个服务器和令牌的评论。
... it's purely random
我不认为它是随机的 ;-)。但是,对于某些用户来说,是什么让它在大部分时间都无法正常工作。
令牌是为不同的页面或APP生成的,或者来自同一个APP并且刚刚过期。例如,短期有效的令牌。存在于浏览器中。令牌来自类似的应用程序或来自不同 form/page 上的同一应用程序。 浏览器显示令牌,因为令牌是为域生成的。
但是在分析时令牌不匹配,或者刚刚过期。
考虑代币的生命周期。