Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Identity verify if ResetPassword token has expired

In my API I have 2 endpoints, first that generates email to reset password form (I generate token using UserManager.GeneratePasswordResetTokenAsync).
Second endpoint is for actual password reset (I use UserManager.ResetPasswordAsync).

My requirement was to verify if token that is required for password reset isn't expired.

Searching over GitHub I found this issue and from what I found this isn't possible by design.

However searching deeper I've found that UserManager.ResetPasswordAsync internally uses ValidateAsync from Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider

Having this I've created this extension method:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System;
using System.Globalization;
using System.IO;
using System.Text;

namespace Api.Extensions
{
    public enum TokenValidity
    {
        VALID,
        INVALID,
        INVALID_EXPIRED,
        ERROR
    }

    public static class UserManagerExtensions
    {
        public static TokenValidity IsResetPasswordTokenValid<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user, string token) where TKey : IEquatable<TKey> where TUser : class, IUser<TKey>
        {
            return IsTokenValid(manager, user, "ResetPassword", token);
        }

        public static TokenValidity IsTokenValid<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user, string purpose, string token) where TKey : IEquatable<TKey> where TUser : class, IUser<TKey>
        {
            try
            {
                //not sure if this is needed??
                if (!(manager.UserTokenProvider is DataProtectorTokenProvider<TUser, TKey> tokenProvider)) return TokenValidity.ERROR;

                var unprotectedData = tokenProvider.Protector.Unprotect(Convert.FromBase64String(token));
                var ms = new MemoryStream(unprotectedData);
                using (var reader = ms.CreateReader())
                {
                    var creationTime = reader.ReadDateTimeOffset();
                    var expirationTime = creationTime + tokenProvider.TokenLifespan;

                    var userId = reader.ReadString();
                    if (!String.Equals(userId, Convert.ToString(user.Id, CultureInfo.InvariantCulture)))
                    {
                        return TokenValidity.INVALID;
                    }

                    var purp = reader.ReadString();
                    if (!String.Equals(purp, purpose))
                    {
                        return TokenValidity.INVALID;
                    }

                    var stamp = reader.ReadString();
                    if (reader.PeekChar() != -1)
                    {
                        return TokenValidity.INVALID;
                    }

                    var expectedStamp = "";
                    //if supported get security stamp for user
                    if (manager.SupportsUserSecurityStamp)
                    {
                        expectedStamp = manager.GetSecurityStamp(user.Id);
                    }

                    if (!String.Equals(stamp, expectedStamp)) return TokenValidity.INVALID;

                    if (expirationTime < DateTimeOffset.UtcNow)
                    {
                        return TokenValidity.INVALID_EXPIRED;
                    }

                    return TokenValidity.VALID;
                }
            }
            catch
            {
                // Do not leak exception
            }
            return TokenValidity.INVALID;
        }
    }

    internal static class StreamExtensions
    {
        internal static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true);

        public static BinaryReader CreateReader(this Stream stream)
        {
            return new BinaryReader(stream, DefaultEncoding, true);
        }

        public static BinaryWriter CreateWriter(this Stream stream)
        {
            return new BinaryWriter(stream, DefaultEncoding, true);
        }

        public static DateTimeOffset ReadDateTimeOffset(this BinaryReader reader)
        {
            return new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
        }

        public static void Write(this BinaryWriter writer, DateTimeOffset value)
        {
            writer.Write(value.UtcTicks);
        }
    }
}

so now I'm able to add this check:

if (UserManager.IsResetPasswordTokenValid(user, model.Code) == TokenValidity.INVALID_EXPIRED)
{
    return this.BadRequest("errorResetingPassword", "Link expired");
}

My question are:

1.Is there an easier way of doing this?
My intention is to show user information that link in email has expired, because right now all he can see is that there was problem with resetting password.

2.If there isn't build in method of doing this what are the potential security vulnerabilities? I use my extension method as an additional check. If my method return true I still use ResetPasswordAsync.

like image 557
Misiu Avatar asked Jul 25 '18 12:07

Misiu


1 Answers

The UserManager has VerifyUserTokenAsync and VerifyUserToken methods that you can use.

see Wouter's answer to the question "How can I check if a password reset token is expired?" for more details.

So you could use something like

if (!UserManager.VerifyUserToken(userId, "ResetPassword", model.code)){
  return this.BadRequest("errorResetingPassword", "Link expired");
}
like image 165
J Fay Avatar answered Oct 30 '22 20:10

J Fay