确认电子邮件中的ASP.NET核心标识无效令牌



这是一个与此非常相似的问题确认电子邮件上的aspnet标识无效令牌但是这些解决方案无效,因为我使用的是包含ASP.NET Core Identity的新ASP.NET Core 1.0。

我的场景如下:

  1. 在后端(ASP.NET Core)中,我有一个函数,可以发送一封带有链接的密码重置电子邮件。为了生成该链接,我必须使用Identity生成一个代码。像这样的东西。

    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);
    }
    

    这将生成一封带有链接的电子邮件:

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

  2. 然后,我的AngularJs应用程序将显示一个带有表单的视图,用于输入和确认新密码,并将带有新密码和从URL中的查询参数获得的代码的JSON对象放入URL中。

  3. 最后,我的后端将获得PUT请求,获取代码并使用以下标识进行验证:

    [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();
    }
    

问题是Identity使用进行响应

无效的令牌

错误。我发现问题是代码不匹配,上面的代码会在PUT请求中的JSON对象中接收回来,如下所示:

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

请注意,以前有+符号的地方现在有空格符号,这显然会导致Identity认为这些符号不同。出于某种原因,Angular正在以不同的编码方式对URL查询参数进行解码。

如何解决此问题?

这个答案https://stackoverflow.com/a/31297879/2948212为我指明了正确的方向。但正如我所说,这是一个不同的版本,现在它是一个略有不同的解决方案。

答案仍然是一样的:在base64url中对令牌进行编码,然后在base64URL中对其进行解码。这样,Angular和ASP.NET Core都将检索到完全相同的代码。

我需要安装另一个对options.LowercaseQueryStrings = true; 的依赖项

现在代码应该是这样的:

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);
}

以及当在PUT请求期间接收回代码时

[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);
}

我遇到了类似的问题,我正在编码我的令牌,但它一直未能通过验证,问题是:true不要在OnGetAsync上设置options.LowercaseQueryStrings,这会更改验证令牌的完整性,您将收到无效令牌错误。

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

我已经尝试了上面的答案,但本指南对我有所帮助。基本上,你需要对代码进行编码,否则,你会遇到一些奇怪的错误。总之,您需要这样做:

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

在这之后,如果它适用于你,解码代码:

string decoded = HttpUtility.UrlDecode(code)

在我的Asp.Net Core 3.0项目中搭建ConfirmEmail页面后,我遇到了同样的问题。

code中的ConfirmEmail.cshtml.cs方法中删除以下行修复了问题:

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

在脚手架登录页面中,callbackUrl被添加到HtmlEncoder.Default.Encode(callbackUrl),然后使用code对其进行URL编码。当点击链接时,解码会自动完成,code就像确认电子邮件一样。

更新:

我注意到,在忘记密码过程中,callbackUrl在放入code之前是Base64编码的,这意味着Base64解码is是必要的。

因此,更好的解决方案是在将代码添加到callbackUrl之前,将以下行添加到生成代码的任何位置。

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

这是一个已修复问题的链接。

(根据本文:https://stackoverflow.com/a/27943434/9869427)

对于resetPasswordAsync(身份管理器)"令牌无效";问题因为"+"成为url中的空间。。。使用Uri.EscapeUriString

示例:在我的sendResetPasswordByMailAsync 中

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

encodeToken=";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>.";

现在你可以点击你的链接,你会用"(按%2B编码)。。。您的代币不会无效。。。

我在GCP中使用Cloud Run托管我的网站时遇到了同样的问题,这里的解决方案都不适合我。

在我的案例中,问题在于实例中本地存储的数据保护密钥。日志中的以下条目暗示了问题:

将密钥存储在目录"/home/.aspnet/DataProtection keys"中,该目录可能不会在容器外部持久化。当容器被销毁时,受保护的数据将不可用。

因此,令牌仅对发出令牌的实例有效,当用户单击发送到其电子邮件的链接时,该实例已被销毁。

解决方案是使用密钥的分布式存储,例如,通过EntityFramework:的数据库

using Microsoft.AspNetCore.DataProtection;
...
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection()
        .PersistKeysToDbContext<DbContext>();
    services.AddIdentity<User, IdentityRole>()
                .AddEntityFrameworkStores<AnotherDbContext>()
                .AddDefaultTokenProviders();
}

不同类型的存储器的详细信息可以在这里找到。

ASP Core 2.1也有类似的问题,我很头疼,因为tokenuserManager.ConfirmEmailAsync(user, code))的任何编码/解码都不适用于我。而且UserManager 总是出现无效令牌错误


解决方案:事实证明,问题是用户不是用_dbContext.Users.AddAsync创建的,而是像_userManager.CreateAsync一样使用dbcontext创建的。在用code替换了这种创建方法后,即使没有对token(CCD_23)进行任何编码/解码,对我来说一切都很好。

在我的案例中,这个问题是由于RegisterModel中的OnPostAsync方法对回调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>.");

此编码(HtmlEncoder.Default.Encode()对callbackUrl的应用程序)使url为"&"变成"&",从而使整个链路无效。

您也可以在验证重置put中的令牌时使用regex。

var decode = token.Replace(" ", "+");
await _userManager.ResetPasswordAsync(user, decode, Password);

最新更新