我正在使用 Asp.Net-Identity-2 ,我正在尝试使用以下方法验证电子邮件验证码。但我收到“无效令牌”错误消息。
我的应用程序的用户管理器是这样的:
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
我不知道可能缺少什么或出了什么问题......
答案 0 :(得分:71)
我遇到了这个问题并解决了它。有几个可能的原因。
如果这是随机发生的,您可能会遇到网址编码问题。 由于未知原因,令牌不是为url-safe设计的,这意味着它在通过url传递时可能包含无效字符(例如,如果通过电子邮件发送)。
在这种情况下,应使用HttpUtility.UrlEncode(token)
和HttpUtility.UrlDecode(token)
。
正如oéPereira在评论中所说,UrlDecode
不是(或有时不是?)。请试试。感谢。
例如:
var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
和
var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
由reset-password-token-provider无法确认email-token-provide生成的令牌。
但我们会看到导致这种情况发生的根本原因。
即使您正在使用:
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;
}
}
}
它不能被称为优雅的解决方案,但它触及根并解决了我的问题。
答案 1 :(得分:50)
因为您在此处为密码重置生成令牌:
string code = UserManager.GeneratePasswordResetToken(user.Id);
但实际上是在尝试验证电子邮件的令牌:
result = await UserManager.ConfirmEmailAsync(id, code);
这是两个不同的令牌。
在您的问题中,您说您正在尝试验证电子邮件,但您的代码是用于重置密码。你在做哪一个?
如果您需要确认电子邮件,请通过
生成令牌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);
答案 2 :(得分:32)
即使使用以下代码,我也会收到“无效令牌”错误:
var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);
在我的情况下,问题结果是我手动创建用户并将他添加到数据库而不使用UserManager.Create(...)
方法。用户存在于数据库中但没有安全标记。
有趣的是GenerateEmailConfirmationToken
返回了一个令牌而没有抱怨缺少安全标记,但该令牌永远无法验证。
答案 3 :(得分:18)
除此之外,我发现代码本身如果没有编码就会失败。
我最近开始以下列方式对我进行编码:
string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
然后当我准备好回读它时:
string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);
说实话,我很惊讶它首先没有被正确编码。
答案 4 :(得分:13)
在我的情况下,我们的AngularJS应用程序将所有加号(+)转换为空格(&#34;&#34;),因此令牌在传回时确实无效。
要解决此问题,在AccountController的ResetPassword方法中,我只是在更新密码之前添加了替换:
code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);
我希望这可以帮助其他任何在Web API和AngularJS中使用Identity的人。
答案 5 :(得分:7)
string code = _userManager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
//发送休息电子邮件
不解码代码
var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
答案 6 :(得分:4)
这就是我所做的:在为URL编码之后解码令牌(简称)
首先,我必须对生成的用户GenerateEmailConfirmationToken进行编码。 (标准以上建议)
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token);
并在您的控制器&#34;确认&#34;动作我必须在验证之前解码令牌。
var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
var result = await userManager.ConfirmEmailAsync(user,decodedCode);
答案 7 :(得分:1)
在@cheny发布的解决方案#3的提示下,我意识到,如果您使用相同的UserManager
实例,则生成的代码将被接受。但是在实际情况下,在用户单击电子邮件链接之后,验证代码会在第二个API调用中发生。
这意味着将创建UserManager
的新实例,并且它无法验证由首次调用的第一个实例生成的代码。使其起作用的唯一方法是确保数据库用户表中有SecurityStamp
列。
将使用UserManager
的类注册为单例会在应用程序启动时引发异常,因为UserManager
类会在Scoped
生命周期内自动注册
答案 8 :(得分:1)
在这里,我遇到了同样的问题,但是经过很多时间后,我发现在我的情况下,无效的令牌错误是由于我的自定义Account类具有重新声明和覆盖的Id属性而引起的。
喜欢:
public class Account : IdentityUser
{
[ScaffoldColumn(false)]
public override string Id { get; set; }
//Other properties ....
}
因此,要解决此问题,我只是删除了该属性,然后再次生成数据库架构以确保确定。
删除此按钮即可解决问题。
答案 9 :(得分:1)
我们已经遇到了一组用户遇到这种情况,一切正常。我们已将其分离到赛门铁克的电子邮件保护系统,该系统将电子邮件中的链接替换为具有安全链接的用户,这些链接将转至其网站进行验证,然后将用户重定向到我们发送的原始链接。
问题是他们正在引入解码......他们似乎在生成的链接上执行URL编码,将我们的链接作为查询参数嵌入到他们的网站中,但是当用户点击并点击安全时,我们会解密。解析url它解码了他们需要编码的第一部分,但是我们的查询字符串的内容,然后浏览器重定向到的URL已被解码,我们又回到了特殊字符处理查询字符串处理的状态背后的代码。
答案 10 :(得分:1)
确保您生成的令牌没有快速过期 - 我已将其更改为10秒进行测试,并且始终会返回错误。
if (dataProtectionProvider != null) {
manager.UserTokenProvider =
new DataProtectorTokenProvider<AppUser>
(dataProtectionProvider.Create("ConfirmationToken")) {
TokenLifespan = TimeSpan.FromHours(3)
//TokenLifespan = TimeSpan.FromSeconds(10);
};
}
答案 11 :(得分:1)
也许这是一个旧线程但是,就这种情况而言,我一直在摸索这个错误的随机发生。我一直在检查所有线程并验证每个建议但是 - 似乎 - 一些代码返回为“无效令牌”。 在对用户数据库进行一些查询之后,我终于发现那些与用户名中的空格或其他非字母数字字符直接相关的“无效令牌”错误。 解决方案很容易找到。只需将UserManager配置为允许用户名中的这些字符。 这可以在用户管理器创建事件之后立即完成,以这种方式将新的UserValidator设置添加到false相应的属性:
_write_msgs_
希望这可以像我一样帮助“迟到”!
答案 12 :(得分:1)
确保在生成时使用:
GeneratePasswordResetTokenAsync(user.Id)
确认您使用:
ResetPasswordAsync(user.Id, model.Code, model.Password)
如果您确定使用的是匹配方法,但仍然无效,请确认user.Id
在两种方法中都相同。 (有时您的逻辑可能不正确,因为您允许使用相同的电子邮件进行注册等。)
答案 13 :(得分:0)
在我的情况下,我只需要在发送电子邮件之前做HttpUtility.UrlEncode。重置期间没有HttpUtility.UrlDecode。
答案 14 :(得分:0)
我的问题是电子邮件中有一个包含ConfirmationToken的错字:
<p>Please confirm your account by <a href=@ViewBag.CallbackUrl'>clicking here</a>.</p>
这意味着多余的撇号被附加到ConfirmationToken的末尾。
D'oh!
答案 15 :(得分:0)
我的问题是我在“重置密码”表单中缺少<input asp-for="Input.Code" type="hidden" />
控件
<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />
答案 16 :(得分:0)
tl; dr:在 aspnet core 2.2 中注册自定义令牌提供程序,以使用AES加密代替MachineKey保护,要领:https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b
我遇到aspnet core 2.2
的同一问题,因为cheny指出令牌提供者的实例必须相同。
这对我不起作用,因为
different API-projects
,它确实生成令牌并
收到令牌以重置密码different instances
个虚拟机上运行,因此机器密钥将不是
一样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
答案 17 :(得分:0)
与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)
};
答案 18 :(得分:0)
使用asp.net内核解决此问题,经过大量挖掘,我意识到我已经在Startup中启用了此选项:
services.Configure<RouteOptions>(options =>
{
options.LowercaseQueryStrings = true;
});
这当然会使查询字符串中的令牌无效。
答案 19 :(得分:0)
以下解决方案在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");
}
答案 20 :(得分:0)
万一有人遇到这种情况,事实证明令牌不是URL友好的,因此我不得不将其包装在HttpUtility.UrlEncode()中,如下所示:
var callback = Url.Content($"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}/reset-password?token={HttpUtility.UrlEncode(token)}&email={user.Email}");