Search code examples
c#asp.net-coreasp.net-core-identityforgot-password

How can I retrieve a user from a password reset token in ASP.NET Core?


I have an ASP.NET Core 2.1 web application and am adding forgot password functionality. I have looked at several examples, and they seem to take one of two approaches. The first approach is to include either the user id or the user's email in the password reset url along with the password reset token. The second approach is to include only the password reset token in the password reset url and then require the user to enter identifying information (such as email) when attempting to change the password (Binary Intellect example). Is there a way to look up the user given just the password reset token?

My team lead has asked me to pass just the token in the password reset url and then look up the user. My initial research makes me believe that I would have to manually keep record of the user id and token relationship, but am hoping that there's something built in. I have reviewed the ASP.NET Core UserManager documentation, but did not find any methods for retrieving a user for a given token.

Here's some of the example code embedding the user id in the password reset URL (Microsoft Password Recovery Doc):

var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);

Solution

  • There is a way to get the UserId from the password reset token, but in my opinion it's tricky and a lot of work.

    What are the defaults

    If you have some codes like the following,

    services.AddIdentity<AppUser, AppRole>(options =>
    {
        ...
    }
    .AddEntityFrameworkStores<AppIdentityDbContext>()
    .AddDefaultTokenProviders();
    
    

    the last line .AddDefaultTokenProviders() adds 4 default token providers, which are used to generate tokens for reset passwords, change email and change phone number options, and for two factor authentication token generation, into the pipeline:

    1. DataProtectorTokenProvider
    2. PhoneNumberTokenProvider
    3. EmailTokenProvider
    4. AuthenticatorTokenProvider

    The first one, DataProtectorTokenProvider, is what we're looking for. It uses data protection to serialize/encrypt those tokens.

    And within the DataProtectorTokenProvider, its protector is default to the name of "DataProtectorTokenProvider".

    How tokens are generated

    If you look at GenerateAsync() method inside DataProtectorTokenProvider, you can kind of tell the token consists of:

    • Utc timestamp of the token creation (DateTimeOffset.UtcNow)
    • userId
    • Purpose string
    • Security stamp, if supported

    The generate method concatenates all those, transform them to a byte array, and calls the protector inside to protect/encrypt the payload. Finally the payload is converted to a base 64 string.

    How to get User Id

    To get the userId from a token, you need to do the reverse engineering:

    • Convert the token from base 64 string back to the byte array
    • Call the protector inside to unprotect/decrypt the byte array
    • Read off the Utc timestamp
    • Read userId

    The tricky part here is how to get the same DataProtector used to generate those token!

    How to get the default Data Protector

    Since the default DataProtectorTokenProvider is DIed into the pipeline, the only way I can think of to get the same DataProtector is to use the default DataProtectorTokenProvider to create a protector with the same default name, "DataProtectorTokenProvider", used to generate tokens!

    public class GetResetPasswordViewModelHandler : IRequestHandler<...>
    {
        ...
        private readonly IDataProtector _dataProtector;
    
        public GetResetPasswordViewModelHandler(...,
           IDataProtectionProvider dataProtectionProvider)
        {
            ...
            _dataProtector = dataProtectionProvider.CreateProtector("DataProtectorTokenProvider");
            // OR
            // dataProtectionProvider.CreateProtector(new DataProtectionTokenProviderOptions().Name);
        }
    
        public async Task<ResetPasswordViewModel> Handle(GetResetPasswordViewModel query, ...)
        {
            // The password reset token comes from query.ResetToken
            var resetTokenArray = Convert.FromBase64String(query.ResetToken);
    
            var unprotectedResetTokenArray = _dataProtector.Unprotect(resetTokenArray);
    
            using (var ms = new MemoryStream(unprotectedResetTokenArray))
            {
                using (var reader = new BinaryReader(ms))
                { 
                    // Read off the creation UTC timestamp
                    reader.ReadInt64();
    
                    // Then you can read the userId!
                    var userId = reader.ReadString();
    
                    ...
                }
            }
    
            ...
        }
    }
    

    Screenshot: enter image description here

    My 2 cents

    It seems like it's a lot of work just try to read the userId off a password reset token. I understand your team lead probably doesn't want to expose the user id on the password reset link, or (s)he thinks it's redundant since the reset token has the userId.

    If you're using integer to represent the userId and don't want to expose that to public, I would change it to GUID.

    If you have to use integer as your userId, I would just create a column of the type unique_identifier off the user profile (I would call it PublicToken) and use that to identifier a user for all public matters.

    var callbackUrl = Url.Action("resetPassword", "account", new
    {
        area = "",
        rt = passwordResetToken,   // reset token
        ut = appUser.Id            // user token, use GUID user id or appUser.PublicToken
    }, protocol: Request.Scheme);