Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Identity 2.0 Invalid Token Randomly

Sometimes users get Invalid Token when clicking on their email confirmation link. I can't figure out why, it's purely random.

Here is the code that creates the user:

IdentityResult result = manager.Create(user, "Password134567");
if (result.Succeeded)
{
    var provider = new DpapiDataProtectionProvider("WebApp2015");
    UserManager<User> userManager = new UserManager<User>(new UserStore<User>());
    userManager.UserTokenProvider = new DataProtectorTokenProvider<User>(provider.Create(user.Id));
    manager.UserTokenProvider = new DataProtectorTokenProvider<User>(provider.Create("ConfirmUser"));

    var emailInfo = new Email();

    string code = HttpUtility.UrlEncode(Context.GetOwinContext().GetUserManager<ApplicationUserManager>().GenerateEmailConfirmationToken(user.Id));
    string callbackUrl = IdentityHelper.GetUserConfirmationRedirectUrl(code, user.Id, Request);

    if (email.IndexOf("@") != -1)
    {
        if (assignedId == 0)
        {
            lblError.Text = "There was an error adding this user";
            return;
        }
        string emailcontent = emailInfo.GetActivationEmailContent(assignedId, callbackUrl, userRole);
        string subject = emailInfo.Subject;
        if (string.IsNullOrEmpty(subject))
        {
            subject = "Your Membership";
        }
        Context.GetOwinContext()
               .GetUserManager<ApplicationUserManager>()
               .SendEmail(user.Id, subject, emailcontent);

        if (user.EmailConfirmed)
        {
            IdentityModels.IdentityHelper.SignIn(manager, user, isPersistent: false);
            IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
        }
        else
        {
            ErrorMessage.ForeColor = Color.Green;
            ErrorMessage.Text = "An email has been sent to the user, once they verify their email they are ready to login.";
        }
    }
    else
    {
        ErrorMessage.ForeColor = System.Drawing.Color.Green;
        ErrorMessage.Text = "User has been created.";
    }

    var ra = new RoleActions();
    ra.AddUserToRoll(txtEmail.Text, txtEmail.Text, userRole);
}
else
{
    ErrorMessage.Text = result.Errors.FirstOrDefault();
}

Here is the confirmation page that gives the 'invalid token' error

protected void Page_Load(object sender, EventArgs e)
{
    var code = IdentityHelper.GetCodeFromRequest(Request);
    var userId = IdentityHelper.GetUserIdFromRequest(Request);
    if (code != null && userId != null)
    {
        var manager = Context.GetOwinContext()
                             .GetUserManager<ApplicationUserManager>();
        var confirmId = manager.FindById(userId);
        if (confirmId != null)
        {
            var result = manager.ConfirmEmail(userId, HttpUtility.UrlDecode(code));
            if (result.Succeeded)
            {
                return;
            }
            else
            {
                lblError.Text = result.Errors.FirstOrDefault();
                txtNewPassword.TextMode= TextBoxMode.SingleLine;
                txtNewPassword.Text = "Error contact support";
                txtNewPassword2.TextMode= TextBoxMode.SingleLine;
                txtNewPassword2.Text = result.Errors.FirstOrDefault();
                txtNewPassword.Enabled = false;
                txtNewPassword2.Enabled = false;
                imageButton1.Enabled = false;
            }
        }
        else
        {
            lblError.Text = "Account Does Not Exist";
            imageButton1.Enabled = false;
        }
    }
}
like image 717
prospector Avatar asked Mar 06 '15 01:03

prospector


3 Answers

Live Demo Project

I've created a pared-down demo project for you. It's hosted on GitHub here and is live on Azure here. It works as designed (see edits about Azure Websites) and uses a similar but not identical approach as you used.

It started with this tutorial, and then I removed the cruft that came with this NuGet demo code:

Install-Package -Prerelease Microsoft.AspNet.Identity.Samples 

For your purposes, my demo code is more relevant than the NuGet sample is, because it focuses just on token creation and validation. In particular, take a look at these two files:

Startup.Auth.cs.

We're instantiating the IDataProtectionProvider only once per application start.

public partial class Startup
{
    public static IDataProtectionProvider DataProtectionProvider 
    { 
        get; 
        private set; 
    }

    public void ConfigureAuth(IAppBuilder app)
    {
        DataProtectionProvider = 
            new DpapiDataProtectionProvider("WebApp2015");

        // other code removed
    }
}

AccountController.cs.

Then within the AccountController, we're using the static provider instead of creating a new one.

userManager.UserTokenProvider = 
    new DataProtectorTokenProvider<User>(
        Startup.DataProtectionProvider.Create("UserToken"));

Just doing that might remove the bug you're seeing. Here are some questions for you to consider while troubleshooting further.

Are you using two different UserTokenProvider purposes?

The DataProtectorTokenProvider.Create(string[] purposes) method takes a purposes argument. Here is what MSDN has to say about that:

purposes. Additional entropy used to ensure protected data may only be unprotected for the correct purposes.

When you create the user code, you're using (at least) two different purposes:

  1. user.Id
  2. "ConfirmUser" and
  3. the purpose of the ApplicationUserManager that you retrieve with GetOwinContext()....

Here's your code as a snippet.

userManager.UserTokenProvider = 
    new DataProtectorTokenProvider<User>(provider.Create(user.Id));

manager.UserTokenProvider = 
    new DataProtectorTokenProvider<User>(provider.Create("ConfirmUser"));

string code = Context
    .GetOwinContext()
    .GetUserManager<ApplicationUserManager ()      
    .GenerateEmailConfirmationToken(user.Id)

When you validate the code, you might be using the wrong purpose. Where do you assign the UserTokenProvider for the ApplicationUserManager that you use to confirm the email? It's purposes argument must be the same!

var manager = Context.GetOwinContext()
                  .GetUserManager<ApplicationUserManager>();

var result = manager.ConfirmEmail(userId, HttpUtility.UrlDecode(code));

There is a strong chance that the token is invalid because you're sometimes using a different UserTokenProvider purpose for creation than you're using for validation.

Why would this be sometimes? Do a thorough search of your code to find all the places that assign to UserTokenProvider. Maybe you override it somewhere unexpected (such as in a property or in the IdentityConfig.cs file) so that it seems random.

Has the TokenLifespan expired?

You've mentioned that the Invalid Token message occurs randomly. It might be that the token has expired. This tutorial notes that the default lifespan is one day. You can change it like this:

manager.UserTokenProvider = 
    new DataProtectorTokenProvider<ApplicationUser>
      (dataProtectionProvider.Create("WebApp2015"))
      {                    
         TokenLifespan = TimeSpan.FromHours(3000)
      };

Why three UserManager instances?

Here are some comments on the code that creates the confirmation token. It seems that you're using a three separate UserManager instances including a derived ApplicationUserManager type. What's that about?

  1. What is the type of manager here?
  2. Why create a userManager instead of using the existing manager?
  3. Why use manager.UserTokenProvider not userManager.UserTokenProvider?
  4. Why are you getting a third UserManager instance from the Context?

Note that I have removed a lot of code to focus on just your token creation.

// 1. 
IdentityResult result = manager.Create(user, "Password134567");

if (result.Succeeded)
{
    var provider = new DpapiDataProtectionProvider("WebApp2015");

    // 2. 
    UserManager<User> userManager = 
        new UserManager<User>(new UserStore<User>());

    userManager.UserTokenProvider = 
        new DataProtectorTokenProvider<User>(provider.Create(user.Id));

    // 3.
    manager.UserTokenProvider = 
        new DataProtectorTokenProvider<User>(provider.Create("ConfirmUser"));

    // 4. 
    string raw = Context.GetOwinContext()
                 .GetUserManager<ApplicationUserManager>()
                 .GenerateEmailConfirmationToken(user.Id)

    // remaining code removed
}

I wonder whether we could simplify the above to use just one UserManager instance as follows.

// 1. 
IdentityResult result = manager.Create(user, "Password134567");

if (result.Succeeded)
{
    var provider = new DpapiDataProtectionProvider("WebApp2015");

    manager.UserTokenProvider = 
        new DataProtectorTokenProvider<User>(provider.Create(user.Id));

    // 3.
    var provider = provider.Create("ConfirmUser");
    manager.UserTokenProvider = 
        new DataProtectorTokenProvider<User>(provider);

    // 4. 
    string raw = manager.GenerateEmailConfirmationToken(user.Id);

    // remaining code removed
}

If you use this approach, be sure to use the same "ConfirmUser" purposes argument during confirmation of the email.

What's inside IdentityHelper?

Since the error is happening randomly, it occurs to me that the IdentityHelper methods might be doing something funky to the code that mucks up things. What's inside each of these methods?

  • IdentityHelper.GetUserConfirmationRedirectUrl()
  • IdentityHelper.RedirectToReturnUrl()
  • IdentityHelper.GetCodeFromRequest()
  • IdentityHelper.GetUserIdFromRequest()

I might write some tests to ensure that the raw code that your process creates always matches the raw code that your process retrieves from the Request. In pseudo-code:

var code01 = CreateCode();
var code02 = UrlEncode(code01);
var request = CreateTheRequest(code02);
var response = GetTheResponse();
var code03 = GetTheCode(response);
var code04 = UrlDecode(code03);
Assert.AreEquals(code01, code04);

Run the above 10,000 times to ensure that no problems exist.

Conclusion

It's my strong suspicion that the problem lies in using one purposes argument during token creation and another during confirmation. Use one purpose only and you might be fine.

Making this work on Azure Websites

  1. Use SqlCompact instead of localdb.
  2. Use app.GetDataProtectionProvider() not DpapiDataProtectionProvider because Dpapi does not work with web farms.
like image 90
Shaun Luttin Avatar answered Nov 19 '22 13:11

Shaun Luttin


Is the site hosted on multiple web servers? If so, you cannot use DPAPI here. It is machine-specific. You'll need to use another data protection provider.

like image 2
Jon Tirjan Avatar answered Nov 19 '22 13:11

Jon Tirjan


There might be some url-invalid characters in the code (token). Thus we need to use HttpUtility.UrlEncode(token) and HttpUtility.UrlDecode(token) when it appears in any url.

See something details here: Identity password reset token is invalid

like image 1
cheny Avatar answered Nov 19 '22 14:11

cheny