24
votes

This is a very similar question to this aspnet identity invalid token on confirmation email but the solutions are not valid because I am using the new ASP.NET Core 1.0 that includes ASP.NET Core Identity.

My scenario is as follows:

  1. In the back end (ASP.NET Core) I have a function that sends a password reset email with a link. In order to generate that link I have to generate a code using Identity. Something like this.

    public async Task SendPasswordResetEmailAsync(string email)
    {
        //_userManager is an instance of UserManager<User>
        var userEntity = await _userManager.FindByNameAsync(email);
        var tokenGenerated = await _userManager.GeneratePasswordResetTokenAsync(userEntity);
        var link = Url.Action("MyAction", "MyController", new { email = email, code = tokenGenerated }, protocol: HttpContext.Request.Scheme);
         //this is my service that sends an email to the user containing the generated password reset link
         await _emailService.SendPasswordResetEmailAsync(userEntity , link);
    }
    

    this would generate an email with a link to:

    http://myapp:8080/passwordreset?code=CfDJ8JBnWaVj6h1PtqlmlJaH57r9TRA5j7Ij1BVyeBUpqX+5Cq1msu9zgkuI32Iz9x/5uE1B9fKFp4tZFFy6lBTseDFTHSJxwtGu+jHX5cajptUBiVqIChiwoTODh7ei4+MOkX7rdNVBMhG4jOZWqqtZ5J30gXr/JmltbYxqOp4JLs8V05BeKDbbVO/Fsq5+jebokKkR5HEJU+mQ5MLvNURsJKRBbI3qIllj1RByXt9mufGRE3wmQf2fgKBkAL6VsNgB8w==

  2. Then my AngularJs application would present a view with a form to enter and confirm the new password, and would PUT a JSON object with the new password and the code that got from the query parameter in the URL.

  3. Finally my back end would get the PUT request, grab the code and validate it using Identity like this:

    [HttpPut]
    [AllowAnonymous]
    [Route("api/password/{email}")]
    public async Task<IActionResult> SendPasswordEmailResetRequestAsync(string email, [FromBody] PasswordReset passwordReset)
    {
        //some irrelevant validatoins here
        await _myIdentityWrapperService.ResetPasswordAsync(email, passwordReset.Password, passwordReset.Code);
        return Ok();
    }
    

The problem is that Identity responds with an

Invalid token

error. I have found that the problem is that the codes don't match and the above code would be received back in the JSON object in the PUT request as follows:

CfDJ8JBnWaVj6h1PtqlmlJaH57r9TRA5j7Ij1BVyeBUpqX 5Cq1msu9zgkuI32Iz9x/5uE1B9fKFp4tZFFy6lBTseDFTHSJxwtGu jHX5cajptUBiVqIChiwoTODh7ei4 MOkX7rdNVBMhG4jOZWqqtZ5J30gXr/JmltbYxqOp4JLs8V05BeKDbbVO/Fsq5 jebokKkR5HEJU mQ5MLvNURsJKRBbI3qIllj1RByXt9mufGRE3wmQf2fgKBkAL6VsNgB8w==

Notice that where there was + symbols now there are spaces symbols and obviously that causes Identity to think the tokens are different. For some reason Angular is decoding the URL query parameter in a different way that was encoded.

How to resolve this?

9
In my case (Asp.Net Core 3.0) it seems that the scaffolded pages introduced this error. See my answer here.jhhwilliams

9 Answers

30
votes

This answer https://stackoverflow.com/a/31297879/2948212 pointed me in the right direction. But as I said it was for a different version and now it is slightly different solution.

The answer is still the same: encode the token in base 64 url, and then decode it in base 64 url. That way both Angular and ASP.NET Core will retrieve the very same code.

I needed to install another dependency to Microsoft.AspNetCore.WebUtilities;

Now the code would be something like this:

public async Task SendPasswordResetEmailAsync(string email)
{
    //_userManager is an instance of UserManager<User>
    var userEntity = await _userManager.FindByNameAsync(email);
    var tokenGenerated = await _userManager.GeneratePasswordResetTokenAsync(userEntity);
    byte[] tokenGeneratedBytes = Encoding.UTF8.GetBytes(tokenGenerated);
    var codeEncoded = WebEncoders.Base64UrlEncode(tokenGeneratedBytes);
    var link = Url.Action("MyAction", "MyController", new { email = email, code = codeEncoded }, protocol: HttpContext.Request.Scheme);
     //this is my service that sends an email to the user containing the generated password reset link
     await _emailService.SendPasswordResetEmailAsync(userEntity , link);
}

and when receiving back the code during the PUT request

[HttpPut]
[AllowAnonymous]
[Route("api/password/{email}")]
public async Task<IActionResult> SendPasswordEmailResetRequestAsync(string email, [FromBody] PasswordReset passwordReset)
{
    //some irrelevant validatoins here
    await _myIdentityWrapperService.ResetPasswordAsync(email, passwordReset.Password, passwordReset.Code);
    return Ok();
}

//in MyIdentityWrapperService
public async Task ResetPasswordAsync(string email, string password, string code)
{
    var userEntity = await _userManager.FindByNameAsync(email);
    var codeDecodedBytes = WebEncoders.Base64UrlDecode(code);
    var codeDecoded = Encoding.UTF8.GetString(codeDecodedBytes);
    await _userManager.ResetPasswordAsync(userEntity, codeDecoded, password);
}
8
votes

I had a similar issue and I was encoding my token but it kept on failing validation and the problem turned out to be this : options.LowercaseQueryStrings = true; Do not set true on options.LowercaseQueryStrings this alters the validation token's integrity and you will get Invalid Token Error.

// This allows routes to be in lowercase
services.AddRouting(options =>
{
     options.LowercaseUrls = true;
      options.LowercaseQueryStrings = false;
});

7
votes

I have tried the answers above, but this guide has helped me. Basically, you would need to encode the code, otherwise, you would encounter some weird bugs. To summarise you would need to do this:

string code = HttpUtility.UrlEncode(UserManager.GenerateEmailConfirmationToken(userID));

After this, if it is applicable to you, decode code:

string decoded = HttpUtility.UrlDecode(code)
6
votes

After scaffolding the ConfirmEmail page in my Asp.Net Core 3.0 project I ran into the same problem.

Removing the following line from the OnGetAsync method in ConfirmEmail.cshtml.cs fixed the problem:

code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));

In the scaffolded Login page, the code is added to the callbackUrl which is then URL encoded using HtmlEncoder.Default.Encode(callbackUrl). When the link is clicked the decoding is automatically done and the code is like it should be to confirm the email.

UPDATE:

I noticed that during the Forgot Password process the code is Base64 encoded before being put in the callbackUrl which then means that the Base64 decode IS necessary.

A better solution would thus be to add the following line to wherever the code is generated before adding it to the callbackUrl.

code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

Here is a link to the issue which has been fixed.

1
votes

I had the same issue while hosting my website using Cloud Run in GCP, and none of the solutions here worked for me.

In my case the problem was with the data protection keys being stored locally in the instance. The following entry in the logs hinted the problem:

Storing keys in a directory '/home/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.

Because of this, the tokens were only valid to the instance that issued them, which was already destroyed by the time the users clicked the link sent to their email.

The solution was to use a distributed storage for the keys, for instance, a database via EntityFramework:

using Microsoft.AspNetCore.DataProtection;
...
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToDbContext<DbContext>();

    services.AddIdentity<User, IdentityRole>()
                .AddEntityFrameworkStores<AnotherDbContext>()
                .AddDefaultTokenProviders();
}

The details of the different types of storages can be found here.

0
votes

(according this post: https://stackoverflow.com/a/27943434/9869427)

For the resetPasswordAsync (identity manager) "token invalid" problem... because "+" become space in url... use Uri.EscapeUriString

exemple: in my sendResetPasswordByMailAsync

var token = "Aa+Bb Cc";
var encodedToken = Uri.EscapeDataString(token); 

encodedToken = "Aa%20Bb2B%Cc"

var url = $"http://localhost:4200/account/reset-password?email={email}&token={encodedToken}";
var mailContent= $"Please reset your password by <a href='{url}'>clicking here</a>.";

Now u can click on your link, and you will go to the good url with "+" (encode by %2B)... your token won't be invalid...

0
votes

Had a similar issue for ASP Core 2.1, and was scratching my head, because any encoding/decoding for code(token) did not work for me. And I always had an Invalid token error for userManager.ConfirmEmailAsync(user, code)


SOLUTION: Turned out that the issue was that the user was created not with UserManager but using dbcontext like _dbContext.Users.AddAsync after replacing this creation method with _userManager.CreateAsync everything worked fine for me even without any encoding/decoding for code(token).

0
votes

In my case, this problem was due to OnPostAsync method in RegisterModel encoding the callback url:

var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { userId = user.Id, code = code },
                    protocol: Request.Scheme);

await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

This encoding (the application of HtmlEncoder.Default.Encode() to callbackUrl), made the url '&' become '&', thus invalidating the whole link.

0
votes

You can also use regex when verifying the token in the reset put.

var decode = token.Replace(" ", "+");

await _userManager.ResetPasswordAsync(user, decode, Password);