身份框架测试确认电子邮件令牌是否已过期

Identity Framework test if confirm email token is expired

是否可以使用 Identity Framework 的 UserManager 测试确认电子邮件令牌是否已过期?不管是什么错误,从以下:

var result = await UserManager.ConfirmEmailAsync(userId, code);

我收到一般性 "Invalid Token" 错误。

我通过 keeping/storing 生成的令牌的副本

解决了这个问题
public class ApplicationUser : IdentityUser {
    public string EmailConfirmationToken { get; set; }
    public string ResetPasswordToken { get; set; }
}

并将其与派生 UserManager<ApplicationUser> 中的用户相关联。

public override async System.Threading.Tasks.Task<string> GenerateEmailConfirmationTokenAsync(string userId) {
    /* NOTE:
        * The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
        * (like when the user's password changes), the tokens will always be the same, and remain valid. 
        * So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
        */
    //await base.UpdateSecurityStampAsync(userId);

    var token = await base.GenerateEmailConfirmationTokenAsync(userId);
    if (!string.IsNullOrEmpty(token)) {
        var user = await FindByIdAsync(userId);
        user.EmailConfirmationToken = token; //<<< Last issued token
        //Note: If a token is generated then the current email is no longer confirmed.
        user.EmailConfirmed = false;
        await UpdateAsync(user);
    }
    return token;
}

提供令牌进行确认后,将完成通过令牌搜索用户。

public static class ApplicationUserManagerExtension {
    public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken) {
        string result = null;
        ApplicationUser user = manager.Users.SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);
        if (user != null) {
            result = user.Id;
        }
        return Task.FromResult(result);
    }
}

如果令牌与已知用户匹配,则表明它是有效颁发的令牌。

然后将尝试与用户管理员确认令牌。

如果确认失败,则令牌已过期并采取适当的措施。

否则,如果令牌已确认,则会将其从关联用户中删除,从而使该令牌的重用无效。

public override async System.Threading.Tasks.Task<IdentityResult> ConfirmEmailAsync(string userId, string token) {
    var user = await FindByIdAsync(userId);
    if (user == null) {
        return IdentityResult.Failed("User Id Not Found");
    }
    var result = await base.ConfirmEmailAsync(userId, token);
    if (result.Succeeded) {
        user.EmailConfirmationToken = null;
        return await UpdateAsync(user);
    } else if (user.EmailConfirmationToken == token) {
        //Previously Issued Token expired
        result = IdentityResult.Failed("Expired Token");
    }
    return result;
}

密码重置也采用了类似的方法。

我找到了一种方法来解析颁发日期的令牌,然后您可以检查它是否在允许的时间跨度内(如果未指定,则默认为 24​​ 小时)。

Identity.cs

ApplicationUserManager

public IDataProtector Protector { get; set; }

public TimeSpan TokenLifespan { get; set; }

ApplicationUserManager 创建()

// Explicitly set token expiration to 24 hours. 
manager.TokenLifespan = TimeSpan.FromHours(24);
var dataProtectionProvider = options.DataProtectionProvider;
manager.Protector = dataProtectionProvider.Create("ASP.NET Identity");

if (dataProtectionProvider != null)
{
    manager.UserTokenProvider =
        new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"))
        {
            TokenLifespan = manager.TokenLifespan
        };
}

AccountController.cs

public async Task<ActionResult> ConfirmEmail(string Code, string UserId)
{
// Try/catch, validation, etc.
var tokenExpired = false;
var unprotectedData = UserManager.Protector.Unprotect(Convert.FromBase64String(Code));
var ms = new MemoryStream(unprotectedData);
using (BinaryReader reader = new BinaryReader(ms))
{
    var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
    var expirationTime = creationTime + UserManager.TokenLifespan;
    if (expirationTime < DateTimeOffset.UtcNow)
    {
        tokenExpired = true;
    }
 }
 // Do something if token is expired, else continue with confirmation
}

我找到了 this blog post and Nkosi's answer to be extremely helpful, and if you want to go through the Identity source code, Microsoft has it here (The previous versions of Identity for MVC5 and lower here)。另外,如果回答您自己悬赏的问题形式不佳,我深表歉意,但我忍不住继续寻找更好的解决方案。

这是对@Nkosi 提供的解决方案的 .NET Core 2.1 改编:

应用程序用户class

public class ApplicationUser : IdentityUser 
{
    public string EmailConfirmationToken { get; set; }
    public string ResetPasswordToken { get; set; }
}

派生用户管理器class

public class CustomUserManager : UserManager<ApplicationUser>
{
    public CustomUserManager(IUserStore<ApplicationUser> store, 
        IOptions<IdentityOptions> optionsAccessor, 
        IPasswordHasher<ApplicationUser> passwordHasher, 
        IEnumerable<IUserValidator<ApplicationUser>> userValidators, 
        IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators, 
        ILookupNormalizer keyNormalizer, 
        IdentityErrorDescriber errors, 
        IServiceProvider services, 
        ILogger<UserManager<ApplicationUser>> logger) 
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {

    }

    public override async Task<string> GenerateEmailConfirmationTokenAsync(ApplicationUser user)
    {
        /* NOTE:
            * The default UserTokenProvider generates tokens based on the users's SecurityStamp, so until that changes
            * (like when the user's password changes), the tokens will always be the same, and remain valid. 
            * So if you want to simply invalidate old tokens, just call manager.UpdateSecurityStampAsync().
            */
        //await base.UpdateSecurityStampAsync(userId);

        var token = await base.GenerateEmailConfirmationTokenAsync(user);
        if (!string.IsNullOrEmpty(token))
        {
            user.EmailConfirmationToken = token; //<<< Last issued token
            //Note: If a token is generated then the current email is no longer confirmed.
            user.EmailConfirmed = false;
            await UpdateAsync(user);
        }
        return token;
    }

    public override async Task<IdentityResult> ConfirmEmailAsync(ApplicationUser user, string token)
    {
        if (user == null)
        {
            return IdentityResult.Failed(new IdentityError {Description = "User not found."});
        }
        var result = await base.ConfirmEmailAsync(user, token);
        if (result.Succeeded)
        {
            user.EmailConfirmationToken = null;
            return await UpdateAsync(user);
        }
        else if (user.EmailConfirmationToken == token)
        {
            //Previously Issued Token expired
            result = IdentityResult.Failed(new IdentityError { Description = "Expired token." });
        }
        return result;
    }

}

用户管理器扩展

public static class ApplicationUserManagerExtension
{
    public static Task<string> FindIdByEmailConfirmationTokenAsync(this UserManager<ApplicationUser> manager, string confirmationToken)
    {
        string result = null;

        ApplicationUser user = manager.Users
            .SingleOrDefault(u => u.EmailConfirmationToken != null && u.EmailConfirmationToken == confirmationToken);

        if (user != null)
        {
            result = user.Id;
        }
        return Task.FromResult(result);
    }
}

更新: 必须在 ConfigureServices 方法的 Startup.cs 中将 CustomUserManager 添加到服务中。

services.AddTransient<CustomUserManager>();

没有这个,DependencyInjection 就会失败。

你可以使用我controller.It的工作伙伴。

        public IActionResult ForgotPassword()
        {
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> ForgotPassword(string Email)
        {
            if (string.IsNullOrEmpty(Email))
            {
                return View();
            }

            var user = await _userManager.FindByEmailAsync(Email);
            if (user == null)
            {
                return View();
            }

            var code =await _userManager.GeneratePasswordResetTokenAsync(user);

            var callback = Url.Action("ResetPassword", "Account", new
            {
                token=code,
            },Request.Scheme);

            // send email
            await _emailSender.SendEmailAsync(Email, "Confirm Password Reset", $"<a href='{callback}'>If you want to reset your password click please !</a>");

            return RedirectToAction("ForgotPasswordConfirmation", "Account");

        }


        public IActionResult ForgotPasswordConfirmation() => View();


        public IActionResult ResetPassword(string token)
        {
            if (token == null)
            {
                return View();
            }

            var model = new ResetPasswordModel()
            {
                Token = token,
            };
            return View(model);
        }

        [HttpPost]
        public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                return RedirectToAction("Index", "Home");
            }

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

            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "Account");
            }

            return View(model);
        }

        public ActionResult ResetPasswordConfirmation() => View();