Identity: Validate reset password token

Created on 4 Jan 2015  ·  15Comments  ·  Source: aspnet/Identity

I am currently having to use the following code to validate tokens for the purpose of displaying an error to the user instead of a password reset form:

UserTokenProvider.ValidateAsync("ResetPassword", ...

I'm uneasy about duplicating implementation detail that I copied from UserManager.cs into in my code, incase you change the above literal, etc. Please could this either be a public constant, or even better, could you provide a method for validating password reset tokens, like ValidatePasswordResetTokenAsync(...?

Most helpful comment

read below article
http://tech.trailmax.info/2015/05/asp-net-identity-invalid-token-for-password-reset-or-email-confirmation/

//I use below code
if (!UserManager.VerifyUserToken(userId, "ResetPassword", code))
{
//Error
}

All 15 comments

Can you explain your scenario a bit more, you would like to validate the reset token before letting them reset the password?

Yes, that's correct. Consider the following workflow:

  • User receives a reset token by email.
  • The token has an expiry of 24 hours.
  • They click the 'reset password' link in their email after 25 hours have elapsed.
  • The page displays 'this link has expired'

The last step is not possible without first validating the token. This means users would have to submit their new password (with confirmation password) before discovering the link had expired.

You can't tell if the token is expired or if its just invalid regardless, so the error message should probably Invalid token either way.

Absolutely agree, although the important part is to ensure non-malicious users have a better UX by not having to type their credentials in unnecessarily if the token has expired.

I wouldn't mind saying "Token invalid - this link may have expired" :)

Or in fact, I'd probably keep the message as "This link has expired" - then it's just the malicious users that get the misleading/bad UX, which I'd be happy with!

This is the intended design of the feature.

I feel this design decision goes against Microsoft's own recommended practice for API design:

Except for system failures and operations with potential race conditions, framework designers should design APIs so users can write code that does not throw exceptions. For example, you can provide a way to check preconditions before calling a member so users can write code that does not throw exceptions.

-- https://msdn.microsoft.com/en-us/library/ms229030(v=vs.110).aspx

I have to agree with ljwagerfield. This is a pretty common scenario and to write this off as intended seems a bit careless.

read below article
http://tech.trailmax.info/2015/05/asp-net-identity-invalid-token-for-password-reset-or-email-confirmation/

//I use below code
if (!UserManager.VerifyUserToken(userId, "ResetPassword", code))
{
//Error
}

I know that this is an old thread, but I have similar situation.
I have WebAPI, every method is called from javascript.
When user clicks on (expired) link I'd like to show him information that link is expired or code is invalid.
Can I validate code in some way to check if it has valid length, syntax? Ideally as @ljwagerfield mention I'd like to know that code expired.

I've searched a bit and found this article, reading it I found that in ASP.NET Core this is possible (read validating the token part), but not in 2.1 (https://github.com/aspnet/AspNetIdentity/blob/9c48993a446288032f9824633e6dae81257da06e/src/Microsoft.AspNet.Identity.Core/TotpSecurityStampBasedTokenProvider.cs#L69)

Are there any workarounds?
I don't mind writing additional code that would validate length, syntax, but I have no idea if it would be possible to validate expiration time.

HI there
I have exactly the same issue as Misiu.

I have an angular front end that talks to my WebAPI and I would like to validate the token before forcing the user to enter their e-mail address and new password to then find the token has expired.

@marspd You can do one additional request and inside that method You can call:

if (!UserManager.VerifyUserToken(userId, "ResetPassword", code))
{
//Error
}

but I'd like to avoid that one extra request. I don't mind showing inputs for new password, but I'd liek to give better feedback to user - message that link expired

Thanks Misiu, I hadn't spotted that method, I will give it a go.

@marspd I'm aware this is an old issue, but for me validating token expiration time is still an issue.
I've created extension method:

public static class UserManagerExtensions
{
    public static bool IsTokenExpired<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user, string token) where TKey : IEquatable<TKey> where TUser : class, IUser<TKey>
    {
        try
        {
            var tokenProvider = manager.UserTokenProvider as DataProtectorTokenProvider<TUser, TKey>;
            if (tokenProvider == null) return false;

            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;
                if (expirationTime < DateTimeOffset.UtcNow)
                {
                    return true;
                }
                return false;
            }
        }
        catch
        {
            // Do not leak exception
        }
        return true;
    }
}

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);
    }
}

Code is based on DataProtectorTokenProvider source I found.
I had to copy StreamExtensions class, because it is internal (maybe this could be changed ❓ )

Worst part is casting UserTokenProvider as DataProtectorTokenProvider - I must do that because IUserTokenProvider doesn't have Protector and TokenLifespan properties (maybe this also could be changed ❓ )

Having this extension method in my controller I can check if passed token is valid using this code:

if (UserManager.IsTokenExpired(user, model.Code))
{
    return this.BadRequest("errorResetingPassword", "Link expired");
}

@HaoK any chance StreamExtensions protection level could be changed from internal to public?

@egmfrs I've created second version that is more secure (in my opinion:

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);
        }
    }
})

and usage in controller looks like this:

var tokenIsValid = UserManager.IsResetPasswordTokenValid(user, model.Code);
if (tokenIsValid == TokenValidity.INVALID_EXPIRED)
{
    Logger.Error("Expired");
}

Difference is that in this version I'm verifying userId, purpose and security stamp. Only if they are ok I check expiration date.

Any improvements are welcome.
@HaoK could You look at this? I think that this can solve by design problem.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

VR-Architect picture VR-Architect  ·  6Comments

divega picture divega  ·  5Comments

henningst picture henningst  ·  3Comments

marcuslindblom picture marcuslindblom  ·  6Comments

N41m0r picture N41m0r  ·  5Comments