5

I'm using Asp.Net Identity. I systematically have an Invalid token error when I want to confirm new users with an email confirmation token.

Here's my WebApi user controller:

public class UsersController : ApiController
{
    private MyContext _db;
    private MyUserManager _userManager;
    private MyRoleManager _roleManager;


public UsersController()
{
    _db = new MyContext();
    _userManager = new MyUserManager(new UserStore<MyUser>(_db));
    _roleManager = new MyRoleManager(new RoleStore<IdentityRole>(_db));
}

//New user method
[HttpPost]
public async Task<HttpResponseMessage> Register([FromBody]PostUserModel userModel)

{
//New user code
...

var token = await _userManager.GenerateEmailConfirmationTokenAsync(user.Id);
            var message = new IdentityMessage();
            message.Body = string.Format("Hi {0} !\r\nFollow this link to set your password : \r\nhttps://www.mywebsite.com/admin/users/{1}/reset?token={2}", user.UserName, user.Id, HttpUtility.UrlEncode(token));
            message.Subject = "Confirm e-mail";
            message.Destination = user.Email;
            await _userManager.SendEmailAsync(user.Id, message.Subject, message.Body);

            return Request.CreateResponse(HttpStatusCode.OK, res);                     }
    }

To confirm the email, I'm just doing :

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

I'm not doing an HttpUtility.UrlDecode because WebApi does it by itself.

And my custom user manager class :

public class MyUserManager : UserManager<MyUser>
    {
        public MyUserManager(IUserStore<MyUser> store)
            : base(store)
        {            
            UserValidator = new UserValidator<MyUser>(this)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };
            // Configure validation logic for passwords
            PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = true,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
            };
            // Configure user lockout defaults
            UserLockoutEnabledByDefault = false;

            EmailService = new EmailService();
            SmsService = new SmsService();

            var dataProtectionProvider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider("MyApp");

            UserTokenProvider = new Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider<MyUser>(dataProtectionProvider.Create("UserToken"));            
        }
    }
}

Any idea ? Thank you very much

Clément Picou
  • 4,011
  • 2
  • 15
  • 18
  • I had a similar issue on my site. Basically DPADI cannot encrypt/decrypt across multiple machines. I had to use a Machine Key to accomplish what I wanted. You can read more about it here http://stackoverflow.com/a/23661872/1341538 – Andres Castro Sep 16 '15 at 20:34
  • Thank you for your answer. I've read this, but unfortunately, I've got the issue in localhost too. – Clément Picou Sep 17 '15 at 07:29
  • There could be a stack of reasons. I've written about this before: http://tech.trailmax.info/2015/05/asp-net-identity-invalid-token-for-password-reset-or-email-confirmation/ – trailmax Sep 17 '15 at 08:29
  • I've made a test : if I call the confirmation method immediately after having generated the token it works. So even if I generate the token and confirm it on the same computer, I've tried to implement the MachineKeyProtectionProvider instead of the DpapiDataProtectionProvider and it works. I don't know why, because everything is on localhost. – Clément Picou Sep 17 '15 at 09:47
  • Check out this page. https://tech.trailmax.info/2015/05/asp-net-identity-invalid-token-for-password-reset-or-email-confirmation/ – Daniel Botero Correa Feb 15 '18 at 09:46

1 Answers1

1

I've had the same issue and I managed to solve it without implementing custom provider based on machine keys.

I've had the UserTokenProvider which is set up in the UserManager class wired up by DI as per-request scope. So with each request, I was creating new UserTokenProvider.

And when I checked the source code, I noticed that it's using DpapiDataProtectionProvider somewhere inside by default. And the constructor of DpapiDataProtectionProvider used is:

public DpapiDataProtectionProvider()
  : this(Guid.NewGuid().ToString())
{
}

So each time DpapiDataProtectionProvider is created, it generates a new GUID that is used as the app name.

There are two solutions to solve this:

  1. Wire it up as a singleton.
  2. Make it use some fixed app name.

I decided to use the second one, since the singleton solution would break again on app pool recycles etc.

So now my DI registration (using Simple Injector) looks like this (still using per-request scope even though it might not be necessary):

container.Register<IUserTokenProvider<ApplicationUser, string>>(() =>
    new DataProtectorTokenProvider<ApplicationUser>
        (new DpapiDataProtectionProvider("MY APPLICATION NAME").Create("ASP.NET Identity"))
        {
            // all user tokens are only valid for 3 hours
            TokenLifespan = TimeSpan.FromHours(3)
        }, Lifestyle.Scoped);

Hope this helps somebody.

Tom Pažourek
  • 9,582
  • 8
  • 66
  • 107