【问题标题】:ASP.NET Identity verify if ResetPassword token has expiredASP.NET Identity 验证 ResetPassword 令牌是否已过期
【发布时间】:2018-07-25 12:39:55
【问题描述】:

在我的 API 中,我有 2 个端点,第一个生成电子邮件以重置密码表单(我使用 UserManager.GeneratePasswordResetTokenAsync 生成令牌)。
第二个端点用于实际的密码重置(我使用UserManager.ResetPasswordAsync)。

我的要求是验证密码重置所需的令牌是否未过期。

在 GitHub 上搜索,我找到了 this issue,据我发现这是设计不可能的。

但我发现UserManager.ResetPasswordAsync 内部使用ValidateAsync 来自Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider

有了这个,我创建了这个扩展方法:

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

所以现在我可以添加此检查:

if (UserManager.IsResetPasswordTokenValid(user, model.Code) == TokenValidity.INVALID_EXPIRED)
{
    return this.BadRequest("errorResetingPassword", "Link expired");
}

我的问题是:

1.有没有更简单的方法?
我的目的是向用户显示电子邮件中的链接已过期的信息,因为现在他只能看到重置密码有问题。

2.如果没有内置方法这样做,那么潜在的安全漏洞是什么?我使用我的扩展方法作为附加检查。如果我的方法返回 true,我仍然使用 ResetPasswordAsync

【问题讨论】:

    标签: c# asp.net asp.net-web-api asp.net-identity-2


    【解决方案1】:

    UserManager 具有您可以使用的 VerifyUserTokenAsync 和 VerifyUserToken 方法。

    更多详情请参阅Wouter's answer to the question "How can I check if a password reset token is expired?"

    所以你可以使用类似的东西

    if (!UserManager.VerifyUserToken(userId, "ResetPassword", model.code)){
      return this.BadRequest("errorResetingPassword", "Link expired");
    }
    

    【讨论】:

    • 我试过这个,但问题是它检查了所有的东西:creationDate、userId、目的和安全标记。调用VerifyUserToken后你会知道token是无效的,你不会知道为什么它是无效的。我想明确知道 userId 有效,目的有效,安全标记有效但令牌已过期。希望这是有道理的:)
    • 我已经稍微更新了我的代码,现在我在检查令牌到期之前检查用户 ID、用途和安全标记。
    • 嘿@Misiu,你的扩展似乎符合我的目的。我读到你更新了代码:原始帖子中的代码是否是最新的,可以检查用户 ID、用途和印章?
    • @j0w 是的,我认为它是最新的 :) 我已经在几个项目中使用过,并且没有任何问题。我只使用它向用户显示正确的消息 - 我显示 the link is expiredthe link is invalid。我不是安全专家,所以总是使用内置方法来验证令牌 - 首先我调用我的方法,如果它返回 VALID 然后我再次调用内置方法(以确保它是有效的)
    猜你喜欢
    • 2018-12-28
    • 2014-09-18
    • 2022-10-06
    • 2021-04-02
    • 1970-01-01
    • 2021-12-21
    • 1970-01-01
    • 2014-12-19
    • 2016-12-29
    相关资源
    最近更新 更多