NET 标识2出现“无效令牌”错误

我正在使用 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
    
  • My Action to generate the token is (and even if I check the token here, I get "Invalid token" message):

    [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
    
  • My Action to check the token is (here, I always get "Invalid Token" when I check the result):

    [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
    

I don't know what could be missing or what's wrong...

83283 次浏览

因为您在这里为密码重置生成令牌:

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);

确保在生成时使用:

GeneratePasswordResetTokenAsync(user.Id)

并确认您使用:

ResetPasswordAsync(user.Id, model.Code, model.Password)

如果您确保使用了匹配的方法,但它仍然不起作用,请验证两种方法中的 user.Id是否相同。(有时你的逻辑可能不正确,因为你允许使用相同的电子邮件注册等。)

除此之外,我见过如果没有编码,代码本身就会失败。

我最近开始用下面的方式编码我的:

string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);

等我准备好再读一遍的时候:

string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);

老实说,我很惊讶它一开始就没有被正确编码。

我遇到了这个问题并解决了它。有几个可能的原因。

1. URL-编码问题(如果问题“随机”发生)

如果这是随机发生的,您可能会遇到 url 编码问题。 由于未知的原因,令牌不是为 url-safe 设计的,这意味着在通过 url 传递时它可能包含无效字符(例如,如果通过电子邮件发送)。

在这种情况下,应该使用 HttpUtility.UrlEncode(token)HttpUtility.UrlDecode(token)

正如 Oão Pereira 在他的评论中所说,UrlDecode不是必需的(或者有时不是?)。请两个都试试。谢谢。

2. 非匹配方法(电子邮件与密码令牌)

例如:

    var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);

还有

    var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);

电子邮件令牌提供程序生成的令牌不能由重置密码令牌提供程序确认。

但我们将看到这种情况发生的根本原因。

3. 令牌提供程序的不同实例

即使你正在使用:

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;
}
}
}

这不能说是一个优雅的解决方案,但它击中了问题的根源,解决了我的问题。

即使使用这样的代码,我还是会得到“无效令牌”错误:

var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);

在我的情况下,问题是 我正在手动创建用户并将其添加到数据库中,而不使用 UserManager.Create(...)方法。用户存在于数据库中,但没有安全标记。

有趣的是,GenerateEmailConfirmationToken返回了一个令牌而没有抱怨缺少安全标记,但是这个令牌永远无法验证。

string code = _userManager.GeneratePasswordResetToken(user.Id);


code = HttpUtility.UrlEncode(code);

//发送休息电子邮件


不要破译密码

var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);

在我的示例中,我们的 AngularJS 应用程序将所有加号(+)转换为空格(“”) ,因此当标记被传回时,它确实是无效的。

为了解决这个问题,在 AccountController 中的 ResetPassword 方法中,我只是在更新密码之前添加了一个替换:

code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);

我希望这对其他使用 Web API 和 AngularJS 中的 Identity 的人有所帮助。

也许这是一个老线程,但是,只是为了这个案例,我一直在抓挠我的头与随机发生的这个错误。我一直在检查所有线程,并验证每个建议,但似乎是随机的,一些代码返回为“无效令牌”。 在对用户数据库进行一些查询之后,我终于发现那些与用户名中的空格或其他非字母数字字符直接相关的“无效令牌”错误。 当时的解决方案很容易找到。只需配置 UserManager 以允许用户名中的这些字符。 这可以在用户管理器创建事件之后完成,通过这种方式添加一个新的 UserValidator 设置来假设相应的属性:

 public static UserManager<User> Create(IdentityFactoryOptions<UserManager<User>> options, IOwinContext context)
{
var userManager = new UserManager<User>(new UserStore());


// this is the key
userManager.UserValidator = new UserValidator<User>(userManager) { AllowOnlyAlphanumericUserNames = false };




// other settings here
userManager.UserLockoutEnabledByDefault = true;
userManager.MaxFailedAccessAttemptsBeforeLockout = 5;
userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(1);


var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = TimeSpan.FromDays(5)
};
}


return userManager;
}

希望这对像我这样的“迟到者”有所帮助!

确保您生成的令牌不会很快过期——我将其更改为10秒进行测试,它总是返回错误。

    if (dataProtectionProvider != null) {
manager.UserTokenProvider =
new DataProtectorTokenProvider<AppUser>
(dataProtectionProvider.Create("ConfirmationToken")) {
TokenLifespan = TimeSpan.FromHours(3)
//TokenLifespan = TimeSpan.FromSeconds(10);
};
}

我们遇到过这样的情况,一组用户在其中工作得很好。我们已经将其隔离到赛门铁克的电子邮件保护系统中,该系统用安全链接替换我们电子邮件中与用户的链接,这些安全链接到用户的网站进行验证,然后将用户重定向到我们发送的原始链接。

问题是他们正在引入一个解码器... 他们似乎在生成的链接上做了一个 URL 编码,将我们的链接嵌入到他们的网站中作为一个查询参数,但是当用户点击和 clicksafe.symantec.com 解码 URL 时,它解码了他们需要编码的第一部分,但是也解码了我们的查询字符串的内容,然后浏览器被重定向到的 URL 已经被解码了,我们又回到了特殊字符搞乱后面代码中查询字符串处理的状态。

下面是我所做的: 在对令牌进行 URL 编码后对其进行解码(简而言之)

首先,我必须对生成的 UserGenerateEmailConfirationToken 进行编码

    var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token);

在您的控制器的“确认”操作中,我必须在验证令牌之前解码它。

    var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
var result = await userManager.ConfirmEmailAsync(user,decodedCode);

在我的情况下,我只需要在发送电子邮件之前做 HttpUtility.UrlEncode。在重置期间没有 HttpUtility.UrlDecode。

这里我也有同样的问题,但是在很长一段时间后,我发现在我的例子中,无效的令牌错误是由于我的自定义 Account 类重新声明和覆盖 Id 属性这一事实引起的。

就像这样:

 public class Account : IdentityUser
{
[ScaffoldColumn(false)]
public override string Id { get; set; }
//Other properties ....
}

为了解决这个问题,我删除了这个属性,并重新生成了数据库模式,以确保万无一失。

移除这个问题就解决了。

我的问题是,邮件中包含的确认令牌有一处打印错误:

<p>Please confirm your account by <a href=@ViewBag.CallbackUrl'>clicking here</a>.</p>

这意味着额外的撇号被添加到 ConfirationToken 的末尾。

噢!

我的问题是在我的重置密码表单中缺少一个 <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" />

Aspnet core 2.2中注册自定义令牌提供程序,使用 AES 加密代替 MachineKey 保护,要点: (a href = “ https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b”rel = “ noReferrer”> https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b

我在使用 aspnet core 2.2时遇到了同样的问题,cheny 指出令牌提供程序的实例需要是相同的。 这对我不起作用,因为

  • 我得到了 different API-projects,它可以生成令牌 接收令牌重置密码
  • API 可以在虚拟机的 different instances上运行,因此机器密钥不会是 一样
  • API 可能是 restart,令牌将是无效的,因为它是 不再是 same instance

我可以利用 services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path")) 将令牌保存到文件系统,避免重新启动和多个实例共享问题,但无法解决多个项目的问题,因为每个项目都生成自己的文件。

我的解决方案是用一个自己的逻辑替换 MachineKey 数据保护逻辑,这个逻辑使用 AES then HMAC对称加密令牌,使用我自己设置的一个密钥,我可以在机器、实例和项目之间共享这个密钥。我把加密逻辑从 用 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);
}

最后在启动程序中注册提供程序:

 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

和 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)
};

用 asp.net 核心来解决这个问题,经过大量的研究,我意识到我在 Startup 中打开了这个选项:

services.Configure<RouteOptions>(options =>
{
options.LowercaseQueryStrings = true;
});

这当然使查询字符串中的标记无效。

下面的解决方案在 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");
}

如果有人遇到这种情况,就会发现这个令牌不是 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}");

受到@cheny 提出的解决方案 # 3的启发,我意识到如果使用相同的 UserManager实例,生成的代码将被接受。但在实际场景中,验证代码在用户单击电子邮件链接后的第二个 API 调用中发生。 这意味着创建了 UserManager的一个新实例,并且无法验证由第一个调用的第一个实例生成的代码。使其工作的唯一方法是确保数据库用户表中有 SecurityStamp列。 将使用 UserManager的类注册为 singleton 会在应用程序启动时引发异常,因为 UserManager类会自动注册为 Scoped生存期

我已经解决了大多数描述提示中的“无效令牌”问题。这是我对 Blazor 项目的解决方案。核心在 StringExtensions类中。

当用户注册他/她的电子邮件时生成电子邮件:

user = new IdentityUser { UserName = email, Email = email };
var createUser = await _userManager.CreateAsync(user, password);
if (createUser.Succeeded)
{
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var baseUri = NavMgr.BaseUri;
var setNewPasswordUri = baseUri + "confirm-password";
var urlWithParams = StringExtensions.GenerateUrl(token, emailTo, url);
await SendAsync( urlWithParams  );   // use your own Email solution send the email
}

电子邮件确认(用户点击邮件中的链接)

@page  "/confirm-email"
 

<h3>Confirm email</h3>
 

@Error
        [Inject]
UserManager<IdentityUser> UserMgr { get; set; }
 

[Inject]
NavigationManager NavMgr { get; set; }
 

 

protected override Task OnInitializedAsync()
{
var url = NavMgr.Uri;
Token = StringExtensions.GetParamFromUrl(url, "token");
Email = StringExtensions.GetParamFromUrl(url, "email");
log.Trace($"Initialised with email={Email} , token={Token}");
return ActivateEmailAsync();
}
 

 

private async Task ActivateEmailAsync()
{
isProcessing = true;
Error = null;
 

log.Trace($"ActivateEmailAsync started for {Email}");
isProcessing = true;
Error = null;
 

try
{
var user = await UserMgr.FindByEmailAsync(Email);
if (user != null)
{
if (!string.IsNullOrEmpty(Token))
{
var result = await UserMgr.ConfirmEmailAsync(user, Token);
if (result.Succeeded)
{
// Show user , that account is activated
}
else
{
foreach (var error in result.Errors)
{
Error += error.Description;
}
log.Error($"Setting new password failed for {Email} due to the: {Error}");
}
}
else
{
log.Error("This should not happen. Token is null or empty");
}
}
 

}
catch (Exception exc)
{
Error = $"Activation failed";
}
isProcessing = false;
}
 public static class StringExtensions
{
/// <summary>
/// Encode string to be safe to use it in the URL param
/// </summary>
/// <param name="toBeEncoded"></param>
/// <returns></returns>
public static string Encode(string toBeEncoded)
{
var result = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(toBeEncoded));
return result;
}
 

/// <summary>
/// Decode from the url safe string the original value
/// </summary>
/// <param name="toBeDecoded"></param>
/// <returns></returns>
public static string Decode(string toBeDecoded)
{
var decodedBytes = WebEncoders.Base64UrlDecode(toBeDecoded);
var result = Encoding.UTF8.GetString(decodedBytes);
return result;
}
 

 

public static string GenerateUrl(string token, string emailTo, string baseUri, string tokenParamName = "token", string emailParamName = "email")
{
var tokenEncoded = StringExtensions.Encode(token);
var emailEncoded = StringExtensions.Encode(emailTo);
var queryParams = new Dictionary<string, string>();
queryParams.Add(tokenParamName, tokenEncoded);
queryParams.Add(emailParamName, emailEncoded);
var urlWithParams = QueryHelpers.AddQueryString(baseUri, queryParams);
return urlWithParams;
}
 

 

public static string GetParamFromUrl(string uriWithParams, string paramName)
{
var uri = new Uri(uriWithParams, UriKind.Absolute);
var result = string.Empty;
 

if (QueryHelpers.ParseQuery(uri.Query).TryGetValue(paramName, out var paramToken))
{
var queryToken = paramToken.First();
result  = StringExtensions.Decode(queryToken);
}
return result;
}
 

我在重置密码方案中遇到过无效令牌。根本原因是,我正在为不正确的 IndentityUser生成重置令牌。在简化的代码中可以很容易地发现它,但是在更复杂的代码中修复它花费了我一些时间。

我应该用密码的:

 var user = await UserMgr.FindByEmailAsync(Model.Email);
string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);

但我错了(创建另一个 IndentityUser)。

 // This is example "How it should not be done"
var user = await UserMgr.FindByEmailAsync(Model.Email);
 

user = new IdentityUser { UserName = email, Email = email };  // This must not be her !!!! We need to use user found by UserMgr.FindByEmailAsync(Model.Email);
string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);

完整的简化代码在这里:

private async Task GenerateResetToken()
{
var user = await UserMgr.FindByEmailAsync(Model.Email);
if (user == null)
{
Model.Error = "Not registered";
}
else
{
try
{
var _userManager = SignInMgr.UserManager;


UserMgr.FindByEmailAsync(Model.Email);
string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
if (resetToken == null)
{
log.Error("Cannot get token from GeneratePasswordResetTokenAsync");
}
else
{
// Reset token generated. Send email to user
}


}
catch (Exception exc)
{
log.Error(exc, $"Password reset failed  due to the {exc.Message}");
}
}
}