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
何が欠けているのか、何が間違っているのかわからない...
ここでパスワードリセット用のトークンを生成しているため:
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);
この問題に遭遇し、解決しました。考えられる理由はいくつかあります。
これがランダムに発生する場合、URLエンコードの問題が発生している可能性があります。不明な理由により、トークンはURLセーフ用に設計されていません。つまり、URLを通過するときに無効な文字が含まれる可能性があります(たとえば、電子メールで送信される場合)。
この場合、HttpUtility.UrlEncode(token)
とHttpUtility.UrlDecode(token)
を使用する必要があります。
OãoPereiraが彼のコメントで述べたように、UrlDecode
は必要ではありません(または時々必要ではありませんか?)。両方試してください。ありがとう。
例えば:
var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
そして
var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
Email-token-provideによって生成されたトークンは、reset-password-token-providerによって確認できません。
しかし、これが起こる理由の根本的な原因がわかります。
使用している場合でも:
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;
}
}
}
エレガントなソリューションとは言えませんが、根本にぶつかって問題を解決しました。
次のようなコードでも「無効なトークン」エラーが表示されました。
_var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);
_
私の場合、問題はユーザーを手動で作成し、UserManager.Create(...)
メソッドを使用せずにユーザーをデータベースに追加していたであることが判明しました。ユーザーはデータベースに存在しましたが、セキュリティスタンプはありませんでした。
GenerateEmailConfirmationToken
がセキュリティスタンプの欠如について文句を言わずにトークンを返したのは興味深いですが、そのトークンを検証することはできませんでした。
それ以外は、エンコードされていない場合、コード自体が失敗するのを見てきました。
私は最近、次の方法でエンコードを開始しました。
string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
そして、読み返す準備ができたら:
string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);
正直に言うと、そもそも適切にエンコードされていないことに驚いています。
私の場合、AngularJSアプリはすべてのプラス記号(+)を空のスペース( "")に変換したため、トークンは返されたときに実際に無効でした。
この問題を解決するために、AccountControllerのResetPasswordメソッドで、パスワードを更新する前に置換を追加しました。
code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);
これが、Web APIとAngularJSでIdentityを使用する他の人に役立つことを願っています。
string code = _userManager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
//残りのメールを送信
コードをデコードしません
var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
私がやったことは次のとおりです: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);
tl; dr:使用するカスタムトークンプロバイダーをaspnet core 2.2に登録するMachineKey保護ではなくAES暗号化、Gist: https://Gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b
トークンプロバイダーのインスタンスは同じである必要があるとchenyが指摘したように、私は_aspnet core 2.2
_で同じ問題に遭遇しました。これは私にはうまくいきません
different API-projects
_を取得しましたdifferent instances
_で実行される可能性があるため、マシンキーは同じではありません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
_
私たちは、すべて正常に機能していた一連のユーザーでこの状況に遭遇しました。シマンテックの電子メール保護システムに隔離し、ユーザーへの電子メール内のリンクを、検証のためにサイトにアクセスする安全なリンクに置き換え、ユーザーを送信した元のリンクにリダイレクトします。
問題は、デコードを導入していることです...生成されたリンクでURLエンコードを実行して、リンクをサイトのクエリパラメーターとして埋め込みますが、ユーザーがクリックしてclicksafe.symantec.comがURLをデコードするとエンコードに必要な最初の部分をデコードしますが、クエリ文字列のコンテンツもブラウザがリダイレクトされるURLもデコードされ、特殊文字がコードビハインドのクエリ文字列処理を台無しにする状態に戻ります。
生成するときは、必ず以下を使用してください。
GeneratePasswordResetTokenAsync(user.Id)
そして、あなたが使用していることを確認してください:
ResetPasswordAsync(user.Id, model.Code, model.Password)
一致する方法を使用していることを確認しても、それでも機能しない場合は、user.Id
は両方のメソッドで同じです。 (レジストリなどに同じメールを使用できるため、ロジックが正しくない場合があります。)
ここでも同じ問題がありますが、私の場合、カスタムアカウントクラスにIdプロパティが再宣言されてオーバーライドされているという事実により、無効なトークンエラーが発生することがわかりました。
そのように:
public class Account : IdentityUser
{
[ScaffoldColumn(false)]
public override string Id { get; set; }
//Other properties ....
}
それを修正するために、そのプロパティを削除し、念のためにデータベーススキーマを再生成しました。
これを削除すると問題が解決します。
生成するトークンがすぐに期限切れにならないようにしてください。テストのためにトークンを10秒に変更したため、常にエラーが返されます。
if (dataProtectionProvider != null) {
manager.UserTokenProvider =
new DataProtectorTokenProvider<AppUser>
(dataProtectionProvider.Create("ConfirmationToken")) {
TokenLifespan = TimeSpan.FromHours(3)
//TokenLifespan = TimeSpan.FromSeconds(10);
};
}
これは古いスレッドかもしれませんが、念のため、このエラーがランダムに発生することで頭をかきました。私はすべてのスレッドについてすべてのスレッドをチェックし、各提案を検証しましたが、「ランダムに見える」コードの一部が「無効なトークン」として返されました。ユーザーデータベースへのいくつかのクエリの後、スペースまたはユーザー名の他の非英数字に直接関連する「無効なトークン」エラーがようやく見つかりました。解決策は簡単に見つかりました。ユーザー名にこれらの文字を許可するように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;
}
これが私のような「到着後期」に役立つことを願っています!
私の問題は、<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" />
私の問題は、ConfirmationTokenを含むメールにタイプミスがあったことです。
<p>Please confirm your account by <a [email protected]'>clicking here</a>.</p>
これは、ConfirmationTokenの末尾に余分なアポストロフィが追加されたことを意味します。
ど!
私の場合、メールを送信する前にHttpUtility.UrlEncodeを実行するだけです。リセット中にHttpUtility.UrlDecodeはありません。