[SOLVED] Invalid Token using GenerateEmailConfirmationTokenAsync Outside Controller in MVC

Issue

I’ve been stuck on this for days.

I am using GenerateEmailConfirmationTokenAsync to create a token outside the Controller (it’s working fine), but somehow my token is longer than the ones created within the Controller using the GenerateEmailConfirmationTokenAsync and therefore the ConfirmEmail action rejects the token. (Error: Invalid Token).
I have tried Machinekey on web.config, HttpUtility.UrlEncode, but I am still stuck.

How to sort out the Invalid Token error on Controller ConfirmEmail?

Here is my Code:

RegisterUser (outside Controller)

public async Task RegisterUserAsync()
{
    var store = new UserStore<ApplicationUser>(db);
    var UserManager = new ApplicationUserManager(store);

    var query = from c in db.Customer
                where !(from o in db.Users
                        select o.customer_pk)
                    .Contains(c.customer_pk)
                select c;
    var model = query.ToList();

    if (query != null)
    {
        foreach (var item in model)
        {
            var user = new ApplicationUser { UserName = item.email, Email = item.email, customerId = item.customerId};
            var result = await UserManager.CreateAsync(user);
            if (result.Succeeded)
            {
                string callbackUrl = await SendEmailConfirmationTokenAsync(user.Id);
                SmtpClient client = new SmtpClient();
                MailMessage message = new MailMessage
                {
                    IsBodyHtml = true
                };
                message.Subject = "Confirm Email";
                message.To.Add(item.email1);
                message.Body = "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>";

                client.SendAsync(message, "userToken");

                //Assign Role User Here
                await UserManager.AddToRoleAsync(user.Id, "Client");
            }
        }
    }

}

SendEmailConfirmation method (outside Controller)

public async Task<string> SendEmailConfirmationTokenAsync(string userID)
{
    var store = new UserStore<ApplicationUser>(db);
    var UserManager = new ApplicationUserManager(store);
    var url = new UrlHelper();
    var provider = new DpapiDataProtectionProvider("MyApp");
    UserManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
        provider.Create("EmailConfirmation"));
    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userID);
    string encodedCode = HttpUtility.UrlEncode(code);

    string callbackUrl = "http://localhost/Accounts/ConfirmEmail?userId=" + userID + "&code=" + encodedCode;
    return callbackUrl;
}

where db is

ApplicationdDbContext db = new ApplicationdDbContext();

ConfirmEmail within the Identity Controller (Accounts Controller) – I’ve created Accounts instead of Account controller but it’s working fine.

//
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }

    var confirmed = await UserManager.IsEmailConfirmedAsync(userId);
    if (confirmed)
    {
        return RedirectToLocal(userId);
    }
    var result = await UserManager.ConfirmEmailAsync(userId, code); //Here I get the error (Token Invlaid, despite the token and userId being displayed)
    if (result.Succeeded)
    {
        ViewBag.userId = userId;
        ViewBag.code = code;
    }

    return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(SetPasswordViewModel model, string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }

    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var result = await UserManager.AddPasswordAsync(userId, model.NewPassword);
    if (result.Succeeded)
    {
        var user = await UserManager.FindByIdAsync(userId);
        if (user != null)
        {
            await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
        }

        return RedirectToLocal(userId);
    }

    ViewBag.userId = userId;
    ViewBag.code = code;

    AddErrors(result);

    return View(model);
}

I have worked for hours in this code but until now I can’t sort it out.
Thanks for any comments or solution. The reason for this approach is that I have to use task scheduler (I’m using fluentscheduler, which is working fine).

Solution

Your problem is in this line:

var provider = new DpapiDataProtectionProvider("MyApp");
UserManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
    provider.Create("EmailConfirmation"));

DpapiDataProtectionProvider here is just not the same as what Identity uses while running under IIS. As far as I remember instead of "MyApp" it uses internal name for IIS web-site. Also it does some magic with registering it through delegates and as a singleton.

You can try to save a static reference to data protection provider and use it in your scheduler code. In Startup.Auth.cs class do this:

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

    public void ConfigureAuth(IAppBuilder app)
    {
        DataProtectionProvider = app.GetDataProtectionProvider();
        // other stuff.
    }
}

Then in your UserManager access for that reference like this:

public class UserManager : UserManager<ApplicationUser>
{
    public UserManager() : base(new UserStore<ApplicationUser>(new MyDbContext()))
    {
        var dataProtectionProvider = Startup.DataProtectionProvider;
        this.UserTokenProvider = 
                new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));

        // do other configuration
    }
}

However I’m not familiar wiht details of FluentScheduler and it might not let you access this static variable if it starts processes in separate AppDomain. But give it a try and see how it works.

Answered By – trailmax

Answer Checked By – Pedro (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *