Search code examples
asp.netasp.net-mvcasp.net-coreasp.net-identityasp.net-identity-2

how to revoke/invalidate/cancel old email confirmation token (identity)


i allow newly created users who know their password and isn't confirmed yet to change their registration email (as long as it's not registered in my database)

the problem is that if they changed the email, i generate new email confirmation token, but the old token could still validate them(the one i issue on registration), which pretty much could mean that people could use their registration mail at first, change it to some other mail they don't have access to, and validate from the old one, which is a big security hole for me to just leave

is there any way to remove/revoke the old token? (technically i could create a new user and delete the old one, the old token wouldn't work on new user, yet i think there should be a better solution for this)


Solution

  • I added the following properties to my ApplicationUser class

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

    This holds on to the confirmation token to be validated against when confirming email token.

    I then added the following to my ApplicationUserManager which is a UserManager<ApplicationUser> derived class.

    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;
            user.EmailConfirmed = false;
            await UpdateAsync(user);
        }
        return token;
    }
    
    public override async System.Threading.Tasks.Task<string> GeneratePasswordResetTokenAsync(string userId) {
        var token = await base.GeneratePasswordResetTokenAsync(userId);
        if (!string.IsNullOrEmpty(token)) {
            var x = await FindByIdAsync(userId);
            x.ResetPasswordToken = token;
            await UpdateAsync(x);
        }
        return token;
    }
    
    public override async System.Threading.Tasks.Task<IdentityResult> ConfirmEmailAsync(string userId, string token) {
        var result = await base.ConfirmEmailAsync(userId, token);
        if (result.Succeeded) {
            var x = await FindByIdAsync(userId);
            x.EmailConfirmationToken = null;
            await UpdateAsync(x);
        }
        return result;
    }
    
    public override async System.Threading.Tasks.Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword) {
        var result = await base.ResetPasswordAsync(userId, token, newPassword);
        if (result.Succeeded) {
            var x = await FindByIdAsync(userId);
            x.ResetPasswordToken = null;
            await UpdateAsync(x);
        }
        return result;
    }
    

    The following Extensions were added to be able to find the user based on their stored 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 static Task<string> FindIdByResetPasswordTokenAsync(this UserManager<ApplicationUser> manager, string token) {
            string result = null;
    
            ApplicationUser user = manager.Users.SingleOrDefault(u => u.ResetPasswordToken != null && u.ResetPasswordToken == token);
    
            if (user != null) {
                result = user.Id;
            }
    
            return Task.FromResult(result);
        }
    }