web-dev-qa-db-fra.com

Asp.NET Identity 2 générant l'erreur "Jeton non valide"

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 ...

44
Julio Schurt

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);
49
trailmax

J'ai rencontré ce problème et l'ai résolu. Il y a plusieurs raisons possibles.

1. Problèmes de codage d'URL (si le problème survient "de manière aléatoire")

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.

2. Méthodes ne correspondant pas (email vs mot de passe)

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.

3. Différentes instances de fournisseurs de jetons

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.

65
cheny

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é.

31
mendel

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é. 

18
LTMOD

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.

13
user3812699
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); 
7
Tebogo Johannes

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);
2
Damion

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 . 

1
user9296906

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.)

1
Grey Wolf

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.

1
Diego Garcia

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

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!

1
Josep Alacid

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" />
0
Ajit Goel

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!

0
Nacht

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

  • j'ai obtenu different API-projects qui génère le jeton et le reçoit pour réinitialiser le mot de passe
  • les API peuvent s'exécuter sur different instances des machines virtuelles, la clé d'ordinateur ne serait donc pas la même
  • l'API peut restart 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
0
cyptus

Dans mon cas, je dois juste faire HttpUtility.UrlEncode avant d’envoyer un email. Pas de HttpUtility.UrlDecode lors de la réinitialisation.

0
sailen