URL encoding password reset or email confirmation tokens

  • asp.net, identity
  • Asp.Net Identity 2.0 makes it really easy to validate an email address by using the GenerateEmailConfirmationTokenAsync method. However I recently hit an issue where the simplest path, from the same controller, in the same web app, on the same server could not round trip a validation token to confirm the email address.

    Using nancy endpoint to send a code, and then validate the code.

    public class ConfirmEmailModule : NancyModule
    {
    private readonly IdentityUserManager _userManager;

    public ConfirmEmailModule(IdentityUserManager userManager) : base("confirmEmail")
    {
    _userManager = userManager;
    Get["/", true] = async (x, ct) =>
    {
    var validateAttempt = this.Bind<ValidateEmail>();
    var code = HttpUtility.UrlDecode(validateAttempt.Code);
    var result = await _userManager.ConfirmEmailAsync(Guid.Parse(validateAttempt.UserId), code);

    if (result.Succeeded)
    {
    ViewBag.ReturnUrl = validateAttempt.ReturnUrl;
    return View["EmailValidated"];
    }

    ViewBag.Errors = result.Errors;
    return View["ErrorValidatingEmail"];
    };

    Post["/{userId}", true] = async (x, ct) =>
    {
    var userId = Guid.Parse(x.userId);
    string code = await _userManager.GenerateEmailConfirmationTokenAsync(userId);
    var baseUrl = Context.Request.Url.SiteBase;
    var encodedCode = HttpUtility.UrlEncode(code);

    var callbackUrl = $"{baseUrl}/confirmEmail?userid={userId}&code={encodedCode}";

    await _userManager.SendEmailAsync(userId,
    "Confirm your email address",
    "Please confirm your email address by clicking <a href=\"" + callbackUrl + "\">here</a>");

    return HttpStatusCode.OK;
    };
    }
    }

    Using Postman to post a userId to the endpoint to send a confirmation URL created the following confimation code

    frsv1AuYKgDTxcomdt27b/xLiGbzg/pv1k9e6X91B8enQUOIQn4sQ6w/vyZZ4QmIqHVHqMtuFXwbtSJoXKByKkTf7pM1EAftRwwJ2z3BZ/g61NLAhJQ5f8y8NcS+b1vR+/4Cs+yni5GiNElTqqtZVj99c1SY+J7Zsp+Ke/YjkVU3Q75eoghFWlZaCnyUAr29

    And the URL encoded version to send in the email link becomes

    frsv1AuYKgDTxcomdt27b%2FxLiGbzg%2Fpv1k9e6X91B8enQUOIQn4sQ6w%2FvyZZ4QmIqHVHqMtuFXwbtSJoXKByKkTf7pM1EAftRwwJ2z3BZ%2Fg61NLAhJQ5f8y8NcS%2Bb1vR%2B%2F4Cs%2Byni5GiNElTqqtZVj99c1SY%2BJ7Zsp%2BKe%2FYjkVU3Q75eoghFWlZaCnyUAr29

    Now after clicking the emailed link, and then URL Decoding the validation code the code that is used for verification becomes

    frsv1AuYKgDTxcomdt27b/xLiGbzg/pv1k9e6X91B8enQUOIQn4sQ6w/vyZZ4QmIqHVHqMtuFXwbtSJoXKByKkTf7pM1EAftRwwJ2z3BZ/g61NLAhJQ5f8y8NcS b1vR /4Cs yni5GiNElTqqtZVj99c1SY J7Zsp Ke/YjkVU3Q75eoghFWlZaCnyUAr29

    Where all '+' chars in the original code have been replaced with spaces. Therefore there is an issue with directly URL encoding the Base64 string into a percent-encoded URL safe string. Hoever the wikipedia article on Base64 states that there is a URL safe variant of Base64, which does not use the percent encoding. A solution to creating URL safe Base64 string has been provided on Stack Overflow

    Using this sample, we can create a utility to make the Base64 codes URL safe, wihtout needing to generically URL encode a string.

    public static class Base64Utility
    {
    static readonly char[] padding = { '=' };

    public static string UrlSafeEncode(this string base64String)
    {
    return base64String.TrimEnd(padding).Replace('+', '-').Replace('/', '_');
    }

    public static string UrlSafeDecode(this string urlSafeBase64String)
    {
    string base64String = urlSafeBase64String.Replace('_', '/').Replace('-', '+');
    switch (urlSafeBase64String.Length % 4)
    {
    case 2: base64String += "=="; break;
    case 3: base64String += "="; break;
    }
    return base64String;
    }
    }

    By using this encode utility method, the encoded code that is URL safe for the activation link becomes

    frsv1AuYKgDTxcomdt27b_xLiGbzg_pv1k9e6X91B8enQUOIQn4sQ6w_vyZZ4QmIqHVHqMtuFXwbtSJoXKByKkTf7pM1EAftRwwJ2z3BZ_g61NLAhJQ5f8y8NcS-b1vR-_4Cs-yni5GiNElTqqtZVj99c1SY-J7Zsp-Ke_YjkVU3Q75eoghFWlZaCnyUAr29

    and successfully decodes back to the original code generated by Asp.Net Identiy.

    * flickr photo by Gruenewiese86 https://flickr.com/photos/gruenewiese/27682943560 shared under a Creative Commons (BY-ND) license

    Comments

    Post a Comment