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
.
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");
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With