Identity Framework test if confirm email token is expired

风格不统一 提交于 2019-12-03 04:59:14

I found a way to parse the token for the date issued, which you can then check to see if is within the allowed timespan (default of 24hours if not specified).

Identity.cs

ApplicationUserManager

public IDataProtector Protector { get; set; }

public TimeSpan TokenLifespan { get; set; }

ApplicationUserManager Create()

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

I found 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). Also, I apologize if its in poor form to answer a question that you, yourself put a bounty on, but I couldn't help but continue looking for a better solution.

I get around this by keeping/storing a copy of the generated token

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

and associating it with the user in derived 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;
}

When the token is provided for confirmation, a search for the user via the token is done.

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

If the token matches a known user that indicates that it was a validly issued token.

Will then attempt to confirm token with User manager.

If confirmation fails then token has expired and an appropriate action is taken.

Else if the token confirmed, it is removed from associated user and thus invalidating the reuse of that token.

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

A similar approach was implemented for password reset as well.

Here comes an .NET Core 2.1 adaption of the solution provided by @Nkosi :

ApplicationUser class

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

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

}

UserManager Extension

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

Update: The CustomUserManager has to be added to services in Startup.cs in ConfigureServices Method.

services.AddTransient<CustomUserManager>();

Without this, DependencyInjection fails.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!