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
.
VerifyUserToken
You will know that token is invalid, You won't know why it is invalid. I want to explicitly know that userId is valid, purpose is valid, security stamp is valid but token is expired. Hope that makes sense :) – Lightman