74

I'm using Asp.Net-Identity-2 and I'm trying to verify email verification code using the below method. But I am getting an "Invalid Token" error message.

  • My Application's User Manager is like this:

    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
    
  • My Action to generate the token is (and even if I check the token here, I get "Invalid token" message):

    [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
    
  • My Action to check the token is (here, I always get "Invalid Token" when I check the result):

    [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
    

I don't know what could be missing or what's wrong...

Jeroen
  • 53,290
  • 30
  • 172
  • 279
Julio Schurt
  • 1,426
  • 2
  • 12
  • 19

21 Answers21

96

I encountered this problem and resolved it. There are several possible reasons.

1. URL-Encoding issues (if problem occurring "randomly")

If this happens randomly, you might be running into url-encoding problems. For unknown reasons, the token is not designed for url-safe, which means it might contain invalid characters when being passed through a url (for example, if sent via an e-mail).

In this case, HttpUtility.UrlEncode(token) and HttpUtility.UrlDecode(token) should be used.

As oão Pereira said in his comments, UrlDecode is not (or sometimes not?) required. Try both please. Thanks.

2. Non-matching methods (email vs password tokens)

For example:

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

and

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

The token generated by the email-token-provide cannot be confirmed by the reset-password-token-provider.

But we will see the root cause of why this happens.

3. Different instances of token providers

Even if you are using:

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

along with

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

the error still could happen.

My old code shows why:

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

and:

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

Pay attention that in this code, every time when a UserManager is created (or new-ed), a new dataProtectionProvider is generated as well. So when a user receives the email and clicks the link:

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

The AccountController is no longer the old one, and neither are the _userManager and its token provider. So the new token provider will fail because it has no that token in it's memory.

Thus we need to use a single instance for the token provider. Here is my new code and it works fine:

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

and:

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

It could not be called an elegant solution, but it hit the root and solved my problem.

Jeroen
  • 53,290
  • 30
  • 172
  • 279
cheny
  • 1,999
  • 18
  • 24
  • 10
    Just one note to this awesome answer! :) The token MUST be `UrlEncoded`, but it shouldn't be `UrlDecoded`, at least in MVC when received as a method parameter, since it is automatically decoded. If we decode it again, we invalidate the token since the `+` character gets replaced with a white space. – João Pereira Jun 19 '16 at 22:32
  • this solved not my problem bc i need to use the tokens `across projects, instances and computers`. i implemented a custom `AES encryption` for this, see my answer for details: https://stackoverflow.com/a/56355282/1216595 – cyptus May 29 '19 at 08:20
  • Solved the issue in an Asp Net Core 3.1 application using the solution #3 – Krusty Apr 22 '20 at 23:42
  • I hit this answer again. The last time I solved it using a single instance of the UserManager registering the service that consumes the UserManager as singleton. In this other projcet instead if I do the same it throws an exception saying I can't register that service as singleton because UserManager requires a Transient scope. Your solution above doesn't compile (lot of issues I can report). So what might be a valid fix for this? The problem is clearly the #3 (different instances of token providers) – Krusty Jul 15 '20 at 08:20
  • I fixed it again. The issue was caused by the lack of SecurityStamp column in the User table. I had removed it but without that column it doesn't work – Krusty Jul 15 '20 at 09:06
72

Because you are generating token for password reset here:

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

But actually trying to validate token for email:

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

These are 2 different tokens.

In your question you say that you are trying to verify email, but your code is for password reset. Which one are you doing?

If you need email confirmation, then generate token via

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

and confirm it via

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

If you need password reset, generate token like this:

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

and confirm it like this:

var resetResult = await userManager.ResetPasswordAsync(user.Id, code, newPassword);
Bakudan
  • 17,636
  • 9
  • 48
  • 69
trailmax
  • 31,605
  • 20
  • 126
  • 225
  • How to make ConfirmEmailAsync return _failed_ instead of success, if that token is already being used once. Like user tries to revisits the link from his/her email address? – user2904995 Dec 17 '19 at 15:19
  • @user2904995 to make token invalid, you need to change `SecurityStamp` field. This will invalidate all the previously valid tokens, including those that have been used in the past. – trailmax Dec 17 '19 at 22:49
43

I was getting the "Invalid Token" error even with code like this:

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

In my case the problem turned out to be that I was creating the user manually and adding him to the database without using the UserManager.Create(...) method. The user existed in the database but without a security stamp.

It's interesting that the GenerateEmailConfirmationToken returned a token without complaining about the lack of security stamp, but that token could never be validated.

Jeroen
  • 53,290
  • 30
  • 172
  • 279
mendel
  • 1,331
  • 14
  • 18
  • 7
    In my case the users had been migrated from an old database so had null Security Stamps, I ran this to fix it: `UPDATE AspNetUsers SET SecurityStamp = NewID()` – user1069816 Jul 04 '16 at 21:36
  • 1
    I suggest using `UPDATE AspNetUsers SET SecurityStamp = NewID() WHERE SecurityStamp is null` . In my case, SecurityStamp of some users are fine, I prefer to don't mess with them. – TNT Oct 01 '17 at 16:07
  • One thing to keep in mind is that Identity, left to its own devices, generates guids in lowercase, whereas NewID() returns an uppercase guid (at least in SSMS). Consider using LOWER(NewID()) – Christopher Berman Nov 01 '19 at 15:01
  • For me it was actually on checking the token. I pulled user by my repo instead of UserManager, so called with my repo user the ResetPasswordAsync. Same issue basically – Yeronimo Nov 09 '19 at 16:59
24

Other than that, I've seen the code itself fail if it's not encoded.

I've recently started encoding mine in the following fashion:

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

And then when I'm ready to read it back:

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

To be quite honest, I'm surprised that it isn't being properly encoded in the first place.

Alex T
  • 679
  • 7
  • 15
  • 5
    It only needs to be encoded when it is used as a query string value for a reset link. It's possible to use it without encoding if you are providing a password reset form inside of an application where the code gets passed as a hidden value or something similar. – Eric Carlson Jul 17 '15 at 22:18
  • Hey Sr. Thanks by your light! I did it in the following way in ASP.NET Core 5.0: Encoding.UTF8.GetString(Convert.FromBase64String(code)); – Victor HD Mar 11 '21 at 20:01
16

In my case, our AngularJS app converted all plus signs (+) to empty spaces (" ") so the token was indeed invalid when it was passed back.

To resolve the issue, in our ResetPassword method in the AccountController, I simply added a replace prior to updating the password:

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

I hope this helps anyone else working with Identity in a Web API and AngularJS.

Razvan Dumitru
  • 8,710
  • 4
  • 29
  • 50
user3812699
  • 191
  • 1
  • 6
  • 3
    For a more formal approach, I would recommend `var callbackUrl = new Uri(Request.RequestUri, RequestContext.VirtualPathRoot).AbsoluteUri + $"#/resetPassword?username={WebUtility.UrlEncode(user.UserName)}&code={WebUtility.UrlEncode(code)}";` to correctly url encode username and code to a client page (for example Angular) to let the user set password and finalize request – Victor Jan 26 '16 at 19:53
  • The default token is base64 encoded, which is not URL safe and requires URL encoding. You can override or wrap the token provider, and return base64url encoded tokens instead, avoiding the special characters like you did already. – Bart Verkoeijen Mar 22 '17 at 06:53
9

tl;dr: Register custom token provider in aspnet core 2.2 to use AES encryption instead of MachineKey protection, gist: https://gist.github.com/cyptus/dd9b2f90c190aaed4e807177c45c3c8b

i ran into the same issue with aspnet core 2.2, as cheny pointed out the instances of the token provider needs to be the same. this does not work for me because

  • i got different API-projects which does generate the token and receive the token to reset password
  • the APIs may run on different instances of virtual machines, so the machine key would not be the same
  • the API may restart and the token would be invalid because it is not the same instance any more

i could use services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("path")) to save the token to the file system and avoid restart and multiple instance sharing issues, but could not get around the issue with multiple projects, as each project generates a own file.

the solution for me is to replace the MachineKey data protection logic with an own logic which does use AES then HMAC to symmetric encrypt the token with a key from my own settings which i can share across machines, instances and projects. I took the encryption logic from Encrypt and decrypt a string in C#? (Gist: https://gist.github.com/jbtule/4336842#file-aesthenhmac-cs) and implemented a custom 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();
        }
    }

and the SettingsSupplier i use in my project to supply my settings

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

finally register the provider in 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
cyptus
  • 1,737
  • 2
  • 27
  • 47
7
string code = _userManager.GeneratePasswordResetToken(user.Id);

                code = HttpUtility.UrlEncode(code);

//send rest email


do not decode the code

var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password); 
Prudhvi
  • 2,108
  • 7
  • 31
  • 51
Tebogo Johannes
  • 281
  • 3
  • 4
  • Your comment to **not** decode the code does not work for me. Only decoding the code will result in success. – Aaron Hudon Nov 15 '15 at 19:17
  • @AaronHudon Probably dependent on whether you're sending it through the url string or in the request body (post). – Alternatex May 27 '17 at 12:05
  • 1
    It seems to depend on if your using WebAPI or MVC controllers. The model Binder on the MVC controller URL Decodes it by default! – Choco Sep 07 '18 at 01:20
4

Here is what I did: Decode Token after encoding it for URL (in short)

First I had to Encode the User GenerateEmailConfirmationToken that was generated. (Standard above advice)

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

and in your controller's "Confirm" Action I had to decode the Token before I validated it.

    var decodedCode = HttpUtility.UrlDecode(mViewModel.Token);
    var result = await userManager.ConfirmEmailAsync(user,decodedCode);
Damion
  • 63
  • 7
2

Make sure when generate, you use:

GeneratePasswordResetTokenAsync(user.Id)

And confirm you use:

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

If you make sure you are using the matching methods, but it still doesn't work, please verify that user.Id is the same in both methods. (Sometimes your logic may not be correct because you allow using same email for registry, etc.)

Scott Weldon
  • 8,032
  • 6
  • 43
  • 60
Grey Wolf
  • 5,801
  • 2
  • 25
  • 25
2

Here I've the same problem but after a lot of time I found that in my case the invalid token error was raised by the fact that my custom Account class has the Id property re-declared and overridden.

Like that:

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

So to fix it I've just removed that property and generated again the database schema just to be sure.

Removing this solves the problem.

Cyber Progs
  • 2,964
  • 3
  • 23
  • 32
Diego Garcia
  • 1,026
  • 1
  • 11
  • 24
2

The following solution helped me in WebApi:

Registration

var result = await _userManager.CreateAsync(user, model.Password);

if (result.Succeeded) {
EmailService emailService = new EmailService();
var url = _configuration["ServiceName"];
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var encodedToken = HttpUtility.UrlEncode(token);

// .Net Core 2.1, Url.Action return null
// Url.Action("confirm", "account", new { userId = user.Id, code = token }, protocol: HttpContext.Request.Scheme);
var callbackUrl = _configuration["ServiceAddress"] + $"/account/confirm?userId={user.Id}&code={encodedToken}";
var message = emailService.GetRegisterMailTemplate(callbackUrl, url);

await emailService.SendEmailAsync( model.Email, $"please confirm your registration {url}", message );
}

Confirm

[Route("account/confirm")]
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> ConfirmEmail(string userId, string code) {
  if (userId == null || code == null) {
    return Content(JsonConvert.SerializeObject( new { result = "false", message = "data is incorrect" }), "application/json");
  }

  var user = await _userManager.FindByIdAsync(userId);
  if (user == null) {
    return Content(JsonConvert.SerializeObject(new { result = "false", message = "user not found" }), "application/json");
  }

  //var decodedCode = HttpUtility.UrlDecode(code);
  //var result = await _userManager.ConfirmEmailAsync(user, decodedCode);

  var result = await _userManager.ConfirmEmailAsync(user, code);

  if (result.Succeeded)
    return Content(JsonConvert.SerializeObject(new { result = "true", message = "ок", token = code }), "application/json");
  else
    return Content(JsonConvert.SerializeObject(new { result = "false", message = "confirm error" }), "application/json");
}
1

Maybe this is an old thread but, just for the case, I've been scratching my head with the random occurrence of this error. I've been checking all threads about and verifying each suggestion but -randomly seemed- some of the codes where returned as "invalid token". After some queries to the user database I've finally found that those "invalid token" errors where directly related with spaces or other non alphanumerical characters in user names. Solution was easy to find then. Just configure the UserManager to allow those characters in user's names. This can be done just after the user manager create event, adding a new UserValidator setting to false the corresponding property this way:

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

Hope this could help "late arrivals" like me!

JoeCool
  • 695
  • 1
  • 7
  • 23
  • Concerning the encoding/decoding to avoid spaces and other simbols interference I'm using this proposal that works like a charm: http://stackoverflow.com/questions/27535233/aspnet-identity-invalid-token-on-confirmation-email/31297879#31297879 – JoeCool Jan 14 '16 at 10:58
1

Make sure that the token that you generate doesn't expire rapidly - I had changed it to 10 seconds for testing and it would always return the error.

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

We have run into this situation with a set of users where it was all working fine. We have isolated it down to Symantec's email protection system which replaces links in our emails to users with safe links that go to their site for validation and then redirects the user to the original link we sent.

The problem is that they are introducing a decode... they appear to do a URL Encode on the generated link to embed our link as a query parameter to their site but then when the user clicks and clicksafe.symantec.com decodes the url it decodes the first part they needed to encode but also the content of our query string and then the URL that the browser gets redirected to has been decoded and we are back in the state where the special characters mess up the query string handling in the code behind.

1

Hit this issue with asp.net core and after a lot of digging I realised I'd turned this option on in Startup:

services.Configure<RouteOptions>(options =>
{
    options.LowercaseQueryStrings = true;
});

This of course invalidated the token that was in the query string.

Matt
  • 9,685
  • 4
  • 37
  • 36
1

Insipired by the soluion #3 posted by @cheny, I realized that if you use the same UserManager instance the generated code is accepted. But in a real scenario, the validation code happens in a second API call after the user clicks on the email link. It means that a new instance of the UserManager is created and it is unable to verify the code generated by the first instance of the first call. The only way to make it work is to be sure to have the SecurityStamp column in the database user table. Registering the class that's using the UserManager as singleton throws an exception at the application startup because the UserManager class is automatically registered with a Scoped lifetime

Krusty
  • 413
  • 4
  • 18
0

In my case, I just need to do HttpUtility.UrlEncode before sending an email. No HttpUtility.UrlDecode during reset.

sailen
  • 1
  • 1
0

Related to chenny's 3. Different instances of token providers .

In my case I was passing IDataProtectionProvider.Create a new guid every time it got called, which prevented existing codes from being recognized in subsequent web api calls (each request creates its own user manager).

Making the string static solved it for me.

private static string m_tokenProviderId = "MyApp_" + Guid.NewGuid().ToString();
...
manager.UserTokenProvider =
  new DataProtectorTokenProvider<User>(
  dataProtectionProvider.Create(new string[1] { m_tokenProviderId } ))
  {
      TokenLifespan = TimeSpan.FromMinutes(accessTokenLifespan)
  };
0

In case anyone runs into this, it turns out that the token was not URL-friendly, and so I had to wrap it in a HttpUtility.UrlEncode() like so:

var callback = Url.Content($"{this.Request.Scheme}://{this.Request.Host}{this.Request.PathBase}/reset-password?token={HttpUtility.UrlEncode(token)}&email={user.Email}");
San Jaisy
  • 9,586
  • 15
  • 93
  • 162
-1

My problem was that there was a typo in the email containing the ConfirmationToken:

<p>Please confirm your account by <a href=@ViewBag.CallbackUrl'>clicking here</a>.</p>

This meant the extra apostrophe was appended to the end of the ConfirmationToken.

D'oh!

Nacht
  • 2,912
  • 3
  • 22
  • 37
-1

My issue was that I was missing a <input asp-for="Input.Code" type="hidden" /> control in my Reset Password form

<form role="form" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Input.Code" type="hidden" />
Ajit Goel
  • 3,264
  • 3
  • 42
  • 77