web-dev-qa-db-ja.com

「無効なトークン」エラーを与えるAsp.NET Identity 2

Asp.Net-Identity-2を使用しており、以下の方法を使用して電子メール検証コードを検証しようとしています。しかし、"Invalid Token"エラーメッセージが表示されます。

  • 私のアプリケーションのユーザーマネージャーは次のようなものです。

    public class AppUserManager : UserManager<AppUser>
    {
        public AppUserManager(IUserStore<AppUser> store) : base(store) { }
    
        public static AppUserManager Create(IdentityFactoryOptions<AppUserManager> options, IOwinContext context)
        {
            AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
            AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
    
            manager.PasswordValidator = new PasswordValidator { 
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = true,
                RequireUppercase = true
            };
    
            manager.UserValidator = new UserValidator<AppUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = true,
                RequireUniqueEmail = true
            };
    
            var dataProtectionProvider = options.DataProtectionProvider;
    
            //token life span is 3 hours
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider =
                   new DataProtectorTokenProvider<AppUser>
                      (dataProtectionProvider.Create("ConfirmationToken"))
                   {
                       TokenLifespan = TimeSpan.FromHours(3)
                   };
            }
    
            manager.EmailService = new EmailService();
    
            return manager;
        } //Create
      } //class
    } //namespace
    
  • トークンを生成するためのアクションは次のとおりです(ここでトークンをチェックしても、「Invalid token」メッセージが表示されます)。

    [AllowAnonymous]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult ForgotPassword(string email)
    {
        if (ModelState.IsValid)
        {
            AppUser user = UserManager.FindByEmail(email);
            if (user == null || !(UserManager.IsEmailConfirmed(user.Id)))
            {
                // Returning without warning anything wrong...
                return View("../Home/Index");
    
            } //if
    
            string code = UserManager.GeneratePasswordResetToken(user.Id);
            string callbackUrl = Url.Action("ResetPassword", "Admin", new { Id = user.Id, code = HttpUtility.UrlEncode(code) }, protocol: Request.Url.Scheme);
    
            UserManager.SendEmail(user.Id, "Reset password Link", "Use the following  link to reset your password: <a href=\"" + callbackUrl + "\">link</a>");
    
            //This 2 lines I use tho debugger propose. The result is: "Invalid token" (???)
            IdentityResult result;
            result = UserManager.ConfirmEmail(user.Id, code);
        }
    
        // If we got this far, something failed, redisplay form
        return View();
    
    } //ForgotPassword
    
  • トークンをチェックする私のアクションは次のとおりです(ここでは、結果をチェックするときに常に「Invalid Token」を取得します):

    [AllowAnonymous]
    public async Task<ActionResult> ResetPassword(string id, string code)
    {
    
        if (id == null || code == null)
        {
            return View("Error", new string[] { "Invalid params to reset password." });
        }
    
        IdentityResult result;
    
        try
        {
            result = await UserManager.ConfirmEmailAsync(id, code);
        }
        catch (InvalidOperationException ioe)
        {
            // ConfirmEmailAsync throws when the id is not found.
            return View("Error", new string[] { "Error to reset password:<br/><br/><li>" + ioe.Message + "</li>" });
        }
    
        if (result.Succeeded)
        {
            AppUser objUser = await UserManager.FindByIdAsync(id);
            ResetPasswordModel model = new ResetPasswordModel();
    
            model.Id = objUser.Id;
            model.Name = objUser.UserName;
            model.Email = objUser.Email;
    
            return View(model);
        }
    
        // If we got this far, something failed.
        string strErrorMsg = "";
        foreach(string strError in result.Errors)
        {
            strErrorMsg += "<li>" + strError + "</li>";
        } //foreach
    
        return View("Error", new string[] { strErrorMsg });
    
    } //ForgotPasswordConfirmation
    

何が欠けているのか、何が間違っているのかわからない...

55
Julio Schurt

ここでパスワードリセット用のトークンを生成しているため:

string code = UserManager.GeneratePasswordResetToken(user.Id);

しかし、実際にはメールのトークンを検証しようとしています:

result = await UserManager.ConfirmEmailAsync(id, code);

これらは2つの異なるトークンです。

あなたの質問では、電子メールを確認しようとしていると言いますが、コードはパスワードのリセット用です。どっちをしているの?

メールによる確認が必要な場合は、経由でトークンを生成します

var emailConfirmationCode = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);

確認して

var confirmResult = await UserManager.ConfirmEmailAsync(userId, code);

パスワードのリセットが必要な場合は、次のようなトークンを生成します。

var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);

次のように確認します。

var resetResult = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
55
trailmax

この問題に遭遇し、解決しました。考えられる理由はいくつかあります。

1. URLエンコードの問題(問題が「ランダムに」発生する場合)

これがランダムに発生する場合、URLエンコードの問題が発生している可能性があります。不明な理由により、トークンはURLセーフ用に設計されていません。つまり、URLを通過するときに無効な文字が含まれる可能性があります(たとえば、電子メールで送信される場合)。

この場合、HttpUtility.UrlEncode(token)HttpUtility.UrlDecode(token)を使用する必要があります。

OãoPereiraが彼のコメントで述べたように、UrlDecodeは必要ではありません(または時々必要ではありませんか?)。両方試してください。ありがとう。

2.一致しない方法(電子メールとパスワードトークン)

例えば:

    var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);

そして

    var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);

Email-token-provideによって生成されたトークンは、reset-password-token-providerによって確認できません。

しかし、これが起こる理由の根本的な原因がわかります。

3.トークンプロバイダーの異なるインスタンス

使用している場合でも:

var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);

に加えて

var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);

それでもエラーが発生する可能性があります。

私の古いコードは理由を示しています:

public class AccountController : Controller
{
    private readonly UserManager _userManager = UserManager.CreateUserManager(); 

    [AllowAnonymous]
    [HttpPost]
    public async Task<ActionResult> ForgotPassword(FormCollection collection)
    {
        var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { area = "", UserId = user.Id, token = HttpUtility.UrlEncode(token) }, Request.Url.Scheme);

        Mail.Send(...);
    }

そして:

public class UserManager : UserManager<IdentityUser>
{
    private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
    private static readonly UserManager Instance = new UserManager();

    private UserManager()
        : base(UserStore)
    {
    }

    public static UserManager CreateUserManager()
    {
        var dataProtectionProvider = new DpapiDataProtectionProvider();
        Instance.UserTokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());

        return Instance;
    }

このコードでは、UserManagerが作成される(またはnew- ed)たびに、新しいdataProtectionProviderも生成されることに注意してください。そのため、ユーザーがメールを受信して​​リンクをクリックすると:

public class AccountController : Controller
{
    private readonly UserManager _userManager = UserManager.CreateUserManager();
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> ResetPassword(string userId, string token, FormCollection collection)
    {
        var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
        if (result != IdentityResult.Success)
            return Content(result.Errors.Aggregate("", (current, error) => current + error + "\r\n"));
        return RedirectToAction("Login");
    }

AccountControllerは古いものではなく、_userManagerとそのトークンプロバイダー。そのため、新しいトークンプロバイダーはメモリにトークンがないため失敗します。

したがって、トークンプロバイダーに単一のインスタンスを使用する必要があります。ここに私の新しいコードがあり、それはうまく機能します:

public class UserManager : UserManager<IdentityUser>
{
    private static readonly UserStore<IdentityUser> UserStore = new UserStore<IdentityUser>();
    private static readonly UserManager Instance = new UserManager();

    private UserManager()
        : base(UserStore)
    {
    }

    public static UserManager CreateUserManager()
    {
        //...
        Instance.UserTokenProvider = TokenProvider.Provider;

        return Instance;
    }

そして:

public static class TokenProvider
{
    [UsedImplicitly] private static DataProtectorTokenProvider<IdentityUser> _tokenProvider;

    public static DataProtectorTokenProvider<IdentityUser> Provider
    {
        get
        {

            if (_tokenProvider != null)
                return _tokenProvider;
            var dataProtectionProvider = new DpapiDataProtectionProvider();
            _tokenProvider = new DataProtectorTokenProvider<IdentityUser>(dataProtectionProvider.Create());
            return _tokenProvider;
        }
    }
}

エレガントなソリューションとは言えませんが、根本にぶつかって問題を解決しました。

79
cheny

次のようなコードでも「無効なトークン」エラーが表示されました。

_var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);
_

私の場合、問題はユーザーを手動で作成し、UserManager.Create(...)メソッドを使用せずにユーザーをデータベースに追加していたであることが判明しました。ユーザーはデータベースに存在しましたが、セキュリティスタンプはありませんでした。

GenerateEmailConfirmationTokenがセキュリティスタンプの欠如について文句を言わずにトークンを返したのは興味深いですが、そのトークンを検証することはできませんでした。

34
mendel

それ以外は、エンコードされていない場合、コード自体が失敗するのを見てきました。

私は最近、次の方法でエンコードを開始しました。

string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);

そして、読み返す準備ができたら:

string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);

正直に言うと、そもそも適切にエンコードされていないことに驚いています。

19
LTMOD

私の場合、AngularJSアプリはすべてのプラス記号(+)を空のスペース( "")に変換したため、トークンは返されたときに実際に無効でした。

この問題を解決するために、AccountControllerのResetPasswordメソッドで、パスワードを更新する前に置換を追加しました。

code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);

これが、Web APIとAngularJSでIdentityを使用する他の人に役立つことを願っています。

13
user3812699
string code = _userManager.GeneratePasswordResetToken(user.Id);

                code = HttpUtility.UrlEncode(code);

//残りのメールを送信


コードをデコードしません

var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password); 
7
Tebogo Johannes

私がやったことは次のとおりです:URL用にエンコードした後にトークンをデコードします(要するに)

最初に、生成されたユーザーGenerateEmailConfirmationTokenをエンコードする必要がありました。 (上記の標準アドバイス)

    var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
    var encodedToken = HttpUtility.UrlEncode(token);

そして、コントローラーの「確認」アクションで、トークンを検証する前にデコードする必要がありました。

    var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
    var result = await userManager.ConfirmEmailAsync(user,decodedCode);
4
Damion

tl; dr:使用するカスタムトークンプロバイダーをaspnet core 2.2に登録するMachineKey保護ではなくAES暗号化、Gist: https://Gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b

トークンプロバイダーのインスタンスは同じである必要があるとchenyが指摘したように、私は_aspnet core 2.2_で同じ問題に遭遇しました。これは私にはうまくいきません

  • トークンを生成し、トークンを受け取ってパスワードをリセットする_different API-projects_を取得しました
  • aPIは仮想マシンの_different instances_で実行される可能性があるため、マシンキーは同じではありません
  • aPIはrestartであり、トークンは_same instance_ではないため無効になります。

services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path"))を使用してトークンをファイルシステムに保存し、再起動と複数インスタンスの共有の問題を回避できましたが、各プロジェクトが独自のファイルを生成するため、複数のプロジェクトで問題を回避できませんでした。

私にとっての解決策は、MachineKeyデータ保護ロジックを_AES then HMAC_を使用する独自のロジックで置き換え、マシン、インスタンス、プロジェクト間で共有できる独自の設定のキーでトークンを対称暗号化することです C#で文字列を暗号化および復号化しますか? (要点: https://Gist.github.com/jbtule/4336842#file-aesthenhmac-cs )およびカスタムTokenProviderを実装しました:

_    public class AesDataProtectorTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
    {
        public AesDataProtectorTokenProvider(IOptions<DataProtectionTokenProviderOptions> options, ISettingSupplier settingSupplier)
            : base(new AesProtectionProvider(settingSupplier.Supply()), options)
        {
            var settingsLifetime = settingSupplier.Supply().Encryption.PasswordResetLifetime;

            if (settingsLifetime.TotalSeconds > 1)
            {
                Options.TokenLifespan = settingsLifetime;
            }
        }
    }
_
_    public class AesProtectionProvider : IDataProtectionProvider
    {
        private readonly SystemSettings _settings;

        public AesProtectionProvider(SystemSettings settings)
        {
            _settings = settings;

            if(string.IsNullOrEmpty(_settings.Encryption.AESPasswordResetKey))
                throw new ArgumentNullException("AESPasswordResetKey must be set");
        }

        public IDataProtector CreateProtector(string purpose)
        {
            return new AesDataProtector(purpose, _settings.Encryption.AESPasswordResetKey);
        }
    }
_
_    public class AesDataProtector : IDataProtector
    {
        private readonly string _purpose;
        private readonly SymmetricSecurityKey _key;
        private readonly Encoding _encoding = Encoding.UTF8;

        public AesDataProtector(string purpose, string key)
        {
            _purpose = purpose;
            _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        }

        public byte[] Protect(byte[] userData)
        {
            return AESThenHMAC.SimpleEncryptWithPassword(userData, _encoding.GetString(_key.Key));
        }

        public byte[] Unprotect(byte[] protectedData)
        {
            return AESThenHMAC.SimpleDecryptWithPassword(protectedData, _encoding.GetString(_key.Key));
        }

        public IDataProtector CreateProtector(string purpose)
        {
            throw new NotSupportedException();
        }
    }
_

設定サプライヤは、私のプロジェクトで使用して設定を提供します

_    public interface ISettingSupplier
    {
        SystemSettings Supply();
    }

    public class SettingSupplier : ISettingSupplier
    {
        private IConfiguration Configuration { get; }

        public SettingSupplier(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public SystemSettings Supply()
        {
            var settings = new SystemSettings();
            Configuration.Bind("SystemSettings", settings);

            return settings;
        }
    }

    public class SystemSettings
    {
        public EncryptionSettings Encryption { get; set; } = new EncryptionSettings();
    }

    public class EncryptionSettings
    {
        public string AESPasswordResetKey { get; set; }
        public TimeSpan PasswordResetLifetime { get; set; } = new TimeSpan(3, 0, 0, 0);
    }
_

最後に、スタートアップでプロバイダーを登録します。

_ services
     .AddIdentity<AppUser, AppRole>()
     .AddEntityFrameworkStores<AppDbContext>()
     .AddDefaultTokenProviders()
     .AddTokenProvider<AesDataProtectorTokenProvider<AppUser>>(TokenOptions.DefaultProvider);


 services.AddScoped(typeof(ISettingSupplier), typeof(SettingSupplier));
_
_//AESThenHMAC.cs: See https://Gist.github.com/jbtule/4336842#file-aesthenhmac-cs
_
2
cyptus

私たちは、すべて正常に機能していた一連のユーザーでこの状況に遭遇しました。シマンテックの電子メール保護システムに隔離し、ユーザーへの電子メール内のリンクを、検証のためにサイトにアクセスする安全なリンクに置き換え、ユーザーを送信した元のリンクにリダイレクトします。

問題は、デコードを導入していることです...生成されたリンクでURLエンコードを実行して、リンクをサイトのクエリパラメーターとして埋め込みますが、ユーザーがクリックしてclicksafe.symantec.comがURLをデコードするとエンコードに必要な最初の部分をデコードしますが、クエリ文字列のコンテンツもブラウザがリダイレクトされるURLもデコードされ、特殊文字がコードビハインドのクエリ文字列処理を台無しにする状態に戻ります。

1
user9296906

生成するときは、必ず以下を使用してください。

GeneratePasswordResetTokenAsync(user.Id)

そして、あなたが使用していることを確認してください:

ResetPasswordAsync(user.Id, model.Code, model.Password)

一致する方法を使用していることを確認しても、それでも機能しない場合は、user.Idは両方のメソッドで同じです。 (レジストリなどに同じメールを使用できるため、ロジックが正しくない場合があります。)

1
Grey Wolf

ここでも同じ問題がありますが、私の場合、カスタムアカウントクラスにIdプロパティが再宣言されてオーバーライドされているという事実により、無効なトークンエラーが発生することがわかりました。

そのように:

 public class Account : IdentityUser
 {
    [ScaffoldColumn(false)]
    public override string Id { get; set; } 
    //Other properties ....
 }

それを修正するために、そのプロパティを削除し、念のためにデータベーススキーマを再生成しました。

これを削除すると問題が解決します。

1
Diego Garcia

生成するトークンがすぐに期限切れにならないようにしてください。テストのためにトークンを10秒に変更したため、常にエラーが返されます。

    if (dataProtectionProvider != null) {
        manager.UserTokenProvider =
           new DataProtectorTokenProvider<AppUser>
              (dataProtectionProvider.Create("ConfirmationToken")) {
               TokenLifespan = TimeSpan.FromHours(3)
               //TokenLifespan = TimeSpan.FromSeconds(10);
           };
    }
1
Mathemats

これは古いスレッドかもしれませんが、念のため、このエラーがランダムに発生することで頭をかきました。私はすべてのスレッドについてすべてのスレッドをチェックし、各提案を検証しましたが、「ランダムに見える」コードの一部が「無効なトークン」として返されました。ユーザーデータベースへのいくつかのクエリの後、スペースまたはユーザー名の他の非英数字に直接関連する「無効なトークン」エラーがようやく見つかりました。解決策は簡単に見つかりました。ユーザー名にこれらの文字を許可するようにUserManagerを設定するだけです。これは、ユーザーマネージャーがイベントを作成した直後に行うことができ、対応するプロパティをfalseに設定する新しいUserValidatorを次のように追加します。

 public static UserManager<User> Create(IdentityFactoryOptions<UserManager<User>> options, IOwinContext context)
    {
        var userManager = new UserManager<User>(new UserStore());

        // this is the key 
        userManager.UserValidator = new UserValidator<User>(userManager) { AllowOnlyAlphanumericUserNames = false };


        // other settings here
        userManager.UserLockoutEnabledByDefault = true;
        userManager.MaxFailedAccessAttemptsBeforeLockout = 5;
        userManager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(1);

        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"))
            {
                TokenLifespan = TimeSpan.FromDays(5)
            };
        }

        return userManager;
    }

これが私のような「到着後期」に役立つことを願っています!

1
Josep Alacid

私の問題は、<input asp-for="Input.Code" type="hidden" /> [パスワードのリセット]フォームのコントロール

<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />
0
Ajit Goel

私の問題は、ConfirmationTokenを含むメールにタイプミスがあったことです。

<p>Please confirm your account by <a [email protected]'>clicking here</a>.</p>

これは、ConfirmationTokenの末尾に余分なアポストロフィが追加されたことを意味します。

ど!

0
Nacht

私の場合、メールを送信する前にHttpUtility.UrlEncodeを実行するだけです。リセット中にHttpUtility.UrlDecodeはありません。

0
sailen