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);
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.
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:
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".
If you look at GenerateAsync()
method inside DataProtectorTokenProvider
, you can kind of tell the token consists of:
DateTimeOffset.UtcNow
)userId
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.
To get the userId
from a token, you need to do the reverse engineering:
userId
The tricky part here is how to get the same DataProtector
used to generate those token!
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();
...
}
}
...
}
}
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);