3
votes

I'm adding confirmation email feature to my ASP.NET WebAPI project. The server can send email fine, however, the confirmation link always return "Invalid token".

I checked some reasons as pointed out here http://tech.trailmax.info/2015/05/asp-net-identity-invalid-token-for-password-reset-or-email-confirmation/ but it seems that none of them is the root cause

Below is my code:

public async Task<IHttpActionResult> Register(RegisterBindingModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        IdentityResult result;           
            result = await UserManager.CreateAsync(user, model.Password);

            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            try
            {
                await userManager.AddToRoleAsync(user.Id, "Player");

                //Generate email confirmation token                                        
                //var provider = new DpapiDataProtectionProvider("GSEP");
                var provider = new MachineKeyProtectionProvider();
                userManager.UserTokenProvider = new DataProtectorTokenProvider<GSEPUser>(provider.Create("EmailConfirmation"));
                var code = await userManager.GenerateEmailConfirmationTokenAsync(user.Id);
                code = System.Web.HttpUtility.UrlEncode(code);

                EmailHelper emailHelper = new EmailHelper();
                string callBackUrl = emailHelper.GetCallBackUrl(user, code);
                EmailMessage message = new EmailMessage();
                message.Body = callBackUrl;
                message.Destination = user.Email;
                message.Subject = "GSEP Account confirmation";
                emailHelper.sendMail(message);
            }
            catch (Exception e)
            {
                return Ok(GSEPWebAPI.App_Start.Constants.ErrorException(e));
            }         
    }

And now is EmailHelper

public class EmailHelper
{
    public string GetCallBackUrl(GSEPUser user, string code)
    {

        var newRouteValues = new RouteValueDictionary(new { userId = user.Id, code = code });

        newRouteValues.Add("httproute", true);            
        UrlHelper urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext, RouteTable.Routes);
        string callbackUrl = urlHelper.Action(
                    "ConfirmEmail",
                    "Account",
                    newRouteValues,
                    HttpContext.Current.Request.Url.Scheme
                    );
        return callbackUrl;
    }

    public void sendMail(EmailMessage message)
    {
        #region formatter
        string text = string.Format("Please click on this link to {0}: {1}", message.Subject, message.Body);
        string html = "Please confirm your account by clicking this link: <a href=\"" + message.Body + "\">link</a><br/>";

        html += HttpUtility.HtmlEncode(@"Or click on the copy the following link on the browser:" + message.Body);
        #endregion

        MailMessage msg = new MailMessage();
        msg.From = new MailAddress("[email protected]");
        msg.To.Add(new MailAddress(message.Destination));
        msg.Subject = message.Subject;
        msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
        msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));

        SmtpClient smtpClient = new SmtpClient("smtp-mail.outlook.com", Convert.ToInt32(587));
        System.Net.NetworkCredential credentials = new System.Net.NetworkCredential("[email protected]", "mypassword!");
        smtpClient.Credentials = credentials;
        smtpClient.EnableSsl = true;
        smtpClient.Send(msg);
    }
}

And 2 MachineKey class

public class MachineKeyProtectionProvider : IDataProtectionProvider
{
    public IDataProtector Create(params string[] purposes)
    {
        return new MachineKeyDataProtector(purposes);
    }
}

public class MachineKeyDataProtector : IDataProtector
{
    private readonly string[] _purposes;

    public MachineKeyDataProtector(string[] purposes)
    {
        _purposes = purposes;
    }

    public byte[] Protect(byte[] userData)
    {
        return MachineKey.Protect(userData, _purposes);
    }

    public byte[] Unprotect(byte[] protectedData)
    {
        return MachineKey.Unprotect(protectedData, _purposes);
    }
}

I also added machineKey tag in Web.config as some instruction pointed out. And finally is my confirmation email API

    [AllowAnonymous]
    [HttpGet]
    public async Task<IHttpActionResult> ConfirmEmail(string userId, string code)
    {
        if (userId == null || code == null)
        {
            return Ok("Confirm error");
        }
        IdentityResult result;
        try
        {
            result = await UserManager.ConfirmEmailAsync(userId, code);
        }
        catch (InvalidOperationException ioe)
        {
            // ConfirmEmailAsync throws when the userId is not found.
            return Ok("UserID not found");
        }

        if (result.Succeeded)
        {
            return Ok("Confirmation succesfully");
        }
        else
        {
            return Ok(result.Errors);
        }
    }

Please show me where am I go wrong

1

1 Answers

0
votes

I know this is an old thread. But I though of adding the answer as it could help others.

You are using the below code

string callbackUrl = urlHelper.Action(
                    "ConfirmEmail",
                    "Account",
                    newRouteValues,
                    HttpContext.Current.Request.Url.Scheme
                    );

and the UrlHelper.Action already does the url encoding for you in the latest MVC versions. So here in your code you are doing the encoding twice (one inside the Register and another inside GetCallBackUrl using urlHelper.Action) and that is why you are getting the invalid token error.