J'utilise Asp.Net-Identity-2 et j'essaie de vérifier le code de vérification de l'e-mail en utilisant la méthode ci-dessous. Mais je reçois un "Jeton non valide" un message d'erreur.
Le gestionnaire d'utilisateurs de mon application est comme ceci:
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
Mon action pour générer le jeton est (et même si je vérifie le jeton ici, j'obtiens le message "Jeton non valide"):
[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
Mon action pour vérifier le jeton est (ici, je reçois toujours un "jeton invalide" quand je vérifie le résultat):
[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
Je ne sais pas ce qui pourrait manquer ou ce qui ne va pas ...
Parce que vous générez un jeton pour la réinitialisation du mot de passe ici:
string code = UserManager.GeneratePasswordResetToken(user.Id);
Mais essayons en réalité de valider le jeton pour le courrier électronique:
result = await UserManager.ConfirmEmailAsync(id, code);
Ce sont 2 jetons différents.
Dans votre question, vous dites que vous essayez de vérifier votre courrier électronique, mais votre code est destiné à la réinitialisation du mot de passe. Lequel faites-vous?
Si vous avez besoin d'une confirmation par e-mail, générez un jeton via
var emailConfirmationCode = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
et confirmez-le via
var confirmResult = await UserManager.ConfirmEmailAsync(userId, code);
Si vous avez besoin de réinitialiser le mot de passe, générez un jeton comme ceci:
var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
et confirmez comme ceci:
var resetResult = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
J'ai rencontré ce problème et l'ai résolu. Il y a plusieurs raisons possibles.
Si cela se produit de manière aléatoire, vous pouvez être confronté à des problèmes de codage d'URL . Pour des raisons inconnues, le jeton n'est pas conçu pour url-safe, ce qui signifie qu'il peut contenir des caractères non valides lorsqu'il est transmis via une URL (par exemple, si envoyé par e-mail).
Dans ce cas, HttpUtility.UrlEncode(token)
et HttpUtility.UrlDecode(token)
doivent être utilisés.
Comme l'a dit oão Pereira dans ses commentaires, UrlDecode
n'est pas (ou parfois pas?) Requis. Essayez les deux s'il vous plait. Merci.
Par exemple:
var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
et
var result = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
Le jeton généré par email-token-supply ne peut pas être confirmé par le fournisseur reset-password-token-token.
Mais nous verrons pourquoi cela se produit.
Même si vous utilisez:
var token = await _userManager.GeneratePasswordResetTokenAsync(user.Id);
de même que
var result = await _userManager.ResetPasswordAsync(user.Id, HttpUtility.UrlDecode(token), newPassword);
l'erreur peut encore arriver.
Mon ancien code montre pourquoi:
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(...);
}
et:
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;
}
Faites attention à ce que dans ce code, chaque fois que vous créez une UserManager
(ou new
- ed), une nouvelle dataProtectionProvider
est également générée. Ainsi, lorsqu'un utilisateur reçoit le courrier électronique et clique sur le lien:
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");
}
La AccountController
n'est plus l'ancienne, ni le _userManager
ni son fournisseur de jetons. Ainsi, le nouveau fournisseur de jetons échouera car il n'a pas de jeton dans sa mémoire.
Nous devons donc utiliser une seule instance pour le fournisseur de jetons. Voici mon nouveau code et cela fonctionne bien:
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;
}
et:
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;
}
}
}
On ne pouvait pas appeler ça une solution élégante, mais ça a touché la racine et résolu mon problème.
J'obtenais l'erreur "Invalid Token" même avec un code comme celui-ci:
var emailCode = UserManager.GenerateEmailConfirmationToken(id);
var result = UserManager.ConfirmEmail(id, emailCode);
Dans mon cas, le problème était que je créais l’utilisateur manuellement et je l’ajoutais à la base de données sans utiliser la méthode UserManager.Create(...)
. L'utilisateur existait dans la base de données mais sans timbre de sécurité.
Il est intéressant de noter que la variable GenerateEmailConfirmationToken
a renvoyé un jeton sans se plaindre de l'absence de tampon de sécurité, mais ce jeton n'a jamais pu être validé.
Autre que cela, j'ai vu le code lui-même échouer s'il n'est pas codé.
J'ai récemment commencé à encoder le mien de la manière suivante:
string code = manager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
Et puis quand je suis prêt à le relire:
string code = IdentityHelper.GetCodeFromRequest(Request);
code = HttpUtility.UrlDecode(code);
Pour être tout à fait honnête, je suis surpris que ce ne soit pas correctement encodé.
Dans mon cas, notre application AngularJS a converti tous les signes plus (+) en espaces vides (""), de sorte que le jeton était en effet invalide lors de son renvoi.
Pour résoudre le problème, dans notre méthode ResetPassword dans AccountController, j'ai simplement ajouté un remplacement avant la mise à jour du mot de passe:
code = code.Replace(" ", "+");
IdentityResult result = await AppUserManager.ResetPasswordAsync(user.Id, code, newPassword);
J'espère que cela aidera toute autre personne travaillant avec Identity dans une API Web et AngularJS.
string code = _userManager.GeneratePasswordResetToken(user.Id);
code = HttpUtility.UrlEncode(code);
// envoyer un email de repos
ne pas décoder le code
var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
Voici ce que j'ai fait: Décoder Token après l'avoir encodé pour une URL (en bref)
J'ai d'abord dû encoder l'utilisateur GenerateEmailConfirmationToken qui a été généré. (Standard ci-dessus conseil)
var token = await userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token);
et dans l'action "Confirmer" de votre contrôleur, j'ai dû décoder le jeton avant de le valider.
var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
var result = await userManager.ConfirmEmailAsync(user,decodedCode);
Nous avons rencontré cette situation avec un ensemble d’utilisateurs où tout fonctionnait bien. Nous l'avons isolé du système de protection de la messagerie de Symantec, qui remplace les liens de nos e-mails destinés aux utilisateurs par des liens sécurisés menant à leur site pour validation, puis redirige l'utilisateur vers le lien d'origine que nous avons envoyé.
Le problème est qu'ils introduisent un décodage ... ils semblent créer un URL Encoder sur le lien généré pour incorporer notre lien en tant que paramètre de requête sur leur site, mais lorsque l'utilisateur clique et clicksafe.symantec.com décode l'url. décode la première partie dont ils avaient besoin pour encoder, mais aussi le contenu de notre chaîne de requête, puis l'URL vers laquelle le navigateur est redirigé a été décodée et nous sommes de nouveau dans l'état où les caractères spéciaux gâchent la gestion de la chaîne de requête dans le code situé derrière .
Assurez-vous que lorsque vous générez, vous utilisez:
GeneratePasswordResetTokenAsync(user.Id)
Et confirmez que vous utilisez:
ResetPasswordAsync(user.Id, model.Code, model.Password)
Si vous vous assurez que vous utilisez les méthodes correspondantes, mais que cela ne fonctionne toujours pas, veuillez vérifier que user.Id
est identique dans les deux méthodes. (Parfois, votre logique peut ne pas être correcte car vous autorisez l'utilisation du même courrier électronique pour le registre, etc.)
Ici, j'ai le même problème, mais après un laps de temps assez long, j'ai constaté que dans mon cas, l'erreur de jeton non valide était générée par le fait que ma classe Account personnalisée a la propriété Id redéclarée et remplacée.
Comme ça:
public class Account : IdentityUser
{
[ScaffoldColumn(false)]
public override string Id { get; set; }
//Other properties ....
}
Donc, pour résoudre ce problème, je viens de supprimer cette propriété et de générer à nouveau le schéma de base de données pour en être sûr.
Supprimer cela résout le problème.
Assurez-vous que le jeton que vous générez n'expire pas rapidement - je l'avais changé en 10 secondes pour les tests et il retournerait toujours l'erreur.
if (dataProtectionProvider != null) {
manager.UserTokenProvider =
new DataProtectorTokenProvider<AppUser>
(dataProtectionProvider.Create("ConfirmationToken")) {
TokenLifespan = TimeSpan.FromHours(3)
//TokenLifespan = TimeSpan.FromSeconds(10);
};
}
C’est peut-être un vieux fil de discussion mais, juste pour le cas, je me suis gratifié la tête avec l’occurrence aléatoire de cette erreur. J'ai consulté toutes les discussions et vérifié chaque suggestion, mais il me semblait que certains des codes étaient renvoyés sous forme de "jeton non valide" . Après quelques requêtes dans la base de données des utilisateurs, j'ai finalement constaté que ces "jetons non valides" les erreurs étaient directement liées aux espaces ou à d’autres caractères non alphanumériques dans les noms d’utilisateur .. La solution était alors facile à trouver. Configurez simplement UserManager pour autoriser ces caractères dans les noms d’utilisateur . Cela peut être fait juste après la création de l’événement par le gestionnaire d’utilisateur, en ajoutant un nouveau paramètre UserValidator sur false pour la propriété correspondante:
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;
}
J'espère que cela pourrait aider les "arrivées tardives" comme moi!
Mon problème était qu'il me manquait un contrôle <input asp-for="Input.Code" type="hidden" />
dans mon formulaire de réinitialisation de mot de passe
<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />
Mon problème était qu'il y avait une faute de frappe dans l'email contenant le ConfirmationToken:
<p>Please confirm your account by <a [email protected]'>clicking here</a>.</p>
Cela signifiait que l'apostrophe supplémentaire était ajoutée à la fin de ConfirmationToken.
D'oh!
tl; dr: Enregistrez le fournisseur de jetons personnalisé dans aspnet core 2.2 à utiliser. Cryptage AES au lieu de la protection MachineKey, Gist: https://Gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b
j'ai rencontré le même problème avec aspnet core 2.2
, comme l'a souligné cheny, les instances du fournisseur de jetons doivent être identiques. cela ne marche pas pour moi car
different API-projects
qui génère le jeton et le reçoit pour réinitialiser le mot de passedifferent instances
des machines virtuelles, la clé d'ordinateur ne serait donc pas la mêmerestart
et le jeton serait invalide car il ne s'agit plus du same instance
je pourrais utiliser services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path"))
pour enregistrer le jeton dans le système de fichiers et éviter les problèmes de redémarrage et de partage d'instances multiples, mais je ne pouvais pas résoudre le problème avec plusieurs projets, car chaque projet génère un fichier propre.
la solution pour moi est de remplacer la logique de protection des données MachineKey par une logique propre qui utilise AES then HMAC
pour chiffrer de manière symétrique le jeton avec une clé de mes propres paramètres que je peux partager sur des machines, des instances et des projets. J'ai pris la logique de cryptage de Crypter et décrypter une chaîne en C #? (Gist: https://Gist.github.com/jbtule/4336842#file-aesthenhmac-cs ) et implémenté un TokenProvider personnalisé:
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();
}
}
et le SettingsSupplier que j'utilise dans mon projet pour fournir mes paramètres
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);
}
enfin enregistrer le fournisseur dans Startup:
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
Dans mon cas, je dois juste faire HttpUtility.UrlEncode avant d’envoyer un email. Pas de HttpUtility.UrlDecode lors de la réinitialisation.