当确认邮件通过后台作业发送时,机器钥匙数据保护无效链接

时间:2021-06-21 18:27:23

I've been pulling my hair out over this. Anytime a user registration email is sent out via my windows service (background task), I get an "Invalid link".

我把头发都扯下来了。当用户注册邮件通过我的windows服务(后台任务)发送出去时,我得到一个“无效链接”。

My setup

我的设置

I'm using Hangfire as a windows service on our development server. This is where the problematic GenerateEmailConfirmationToken call is happening. It's in a completely different context, outside of the ASP.NET pipeline. So I have setup machineKey values to correspond with that in the web.config of the MVC application:

我在开发服务器上使用Hangfire作为windows服务。这就是出现问题的GenerateEmailConfirmationToken调用的地方。它处于一个完全不同的上下文中,在ASP之外。净管道。所以我设置了machineKey值来对应web上的值。MVC应用的配置:

In the app.config of the Windows Service Console project, which transforms to MyApp.exe.config, I have a machineKey element

在Windows服务控制台项目的app.config中,该项目将转换为MyApp.exe。配置,我有一个机器关键元素。

In the MVC 5 project - I have a machineKey element that matches the MyApp.exe.config machineKey element.

在MVC 5项目中——我有一个与MyApp.exe匹配的machineKey元素。配置machineKey元素。

I've verified that BOTH of these have the same machine key element data.

我已经验证了这两个都有相同的machine key element数据。

The Problem

这个问题

When I generate a user using the ASP.NET MVC context and pipeline (IE without going through the Hangfire Background job processing), the link works fine.

当我使用ASP生成用户时。NET MVC上下文和管道(即不需要经过Hangfire后台作业处理),链接工作正常。

When I use the background job processor, I always get invalid link. I'm all out of ideas here.

当我使用后台作业处理器时,我总是会得到无效的链接。我完全没有办法了。

Why is this happening? Is it because the token is being generated in a different thread? How do I get around this?

为什么会这样?是因为该令牌是在另一个线程中生成的吗?我该怎么解决这个问题呢?

Relevant code for the various projects

各种项目的相关代码

IoC Bootstrapping

国际奥委会引导

Gets called by both applications (Windows Service and MVC Web App)

被两个应用程序调用(Windows服务和MVC Web应用程序)

container.Register<IUserTokenProvider<AppUser, int>>(() => DataProtector.TokenProvider, defaultAppLifeStyle);

DataProtector.cs

DataProtector.cs

public class DataProtector
    {
        public static IDataProtectionProvider DataProtectionProvider { get; set; }
        public static DataProtectorTokenProvider<AppUser, int> TokenProvider { get; set; } 

        static DataProtector()
        {
            DataProtectionProvider = new MachineKeyProtectionProvider();
            TokenProvider = new DataProtectorTokenProvider<AppUser, int>(DataProtectionProvider.Create("Confirmation", "ResetPassword"));
        }
    }

Things I've Tried

我已经试过

Using a DpapiDataProtectionProvider

使用DpapiDataProtectionProvider

Custom MachineKeyProtectionProvider from Generating reset password token does not work in Azure Website

自定义MachineKeyProtectionProvider从生成重置密码令牌并不在Azure网站中工作

The MachineKeyProtectionProvider.cs code is exactly as the linked post above.

MachineKeyProtectionProvider。cs代码与上面链接的文章完全一样。

I've also tried other purposes like "YourMom" and "AllYourTokensAreBelongToMe" to no avail. Single purposes, multiple purposes - it doesn't matter - none work.

我还尝试了其他的一些目的,比如“你妈妈”和“你自己的归属”。单目的,多目的——没关系——都没用。

I'm also calling HttpUtility.UrlEncode(code) on the code that gets generated in both places (Controller and Background Job).

我还把HttpUtility.UrlEncode(代码)调用在两个位置(控制器和后台作业)生成的代码中。

Solution

解决方案

igor got it right, except it was not a code issue. It was because of a rogue service picking up the job, which had a different machine key. I had been staring at the problem so long that I did not see a second service running.

igor说对了,除了它不是代码问题。这是因为一个流氓服务接手了这个任务,它有一个不同的机器密钥。我盯着这个问题看了这么久,以至于没有看到第二个服务在运行。

1 个解决方案

#1


1  

As I understand your problem there are 2 possible places where failure could occur.

正如我理解你的问题,有两个地方可能会发生失败。


1. MachineKey

It could be that the MachineKey itself is not producing a consistent value between your 2 applications. This can happen if your machineKey in the .config file is not the same in both applications (I did read that you checked it but a simple type-o, added space, added to the wrong parent element, etc. could lead to this behavior.). This can be easily tested to rule it out as a point of failure. Also the behavior might be different depending on the referenced .net framework, MachineKey.Protect

可能是机器键本身并没有在您的两个应用程序之间产生一致的值。如果在.config文件中的machineKey在两个应用程序中都不相同(我确实读过您检查过它,但是一个简单的类型-o、添加的空格、添加到错误的父元素等可能导致这种行为),这可能会发生。可以很容易地测试它,将其排除为故障点。另外,根据所引用的。net框架MachineKey.Protect的不同,行为也可能不同

The configuration settings that are required for the MachineKeyCompatibilityMode.Framework45 option are required for this method even if the MachineKeySection.CompatibilityMode property is not set to the Framework45 option.

机器键盘兼容模式所需的配置设置。这个方法需要Framework45选项,即使是MachineKeySection。相容模式属性没有设置为Framework45选项。

I created a random key pair for testing and using this key I generated a test value I assigned to variable validValue below in the code. If you copy/paste the following section into your web.config and app.config the Unprotect of that keyvalue will work.

我创建了一个用于测试的随机密钥对,并使用这个密钥生成了一个测试值,我将它分配给代码下面的变量validValue。如果您将以下部分复制/粘贴到您的web中。config和app.config对该键值的不保护将起作用。

web.config / app.config

网络。配置/ app.config

<system.web>
  <httpRuntime targetFramework="4.6.1"/>
    <machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209"  validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>

Service Application Test

服务应用程序测试

class Program
{
    static void Main(string[] args)
    {
        // should evaluate to SomeTestString
        const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
        var unprotected2 = MachineWrapper.Unprotect(validValue);
    }
}

Mvc Controller (or Web Api controller) Test

Mvc控制器(或Web Api控制器)测试。

public class WebTestController : Controller
{
    // GET: WebTest
    public ActionResult Index()
    {
        // should evaluate to SomeTestString
        const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
        var unprotected2 = MachineWrapper.Unprotect(validValue);

        return View(unprotected2);
    }
}

Common Code

通用代码

using System;
using System.Linq;
using System.Text;
using System.Web.Security;

namespace Common
{
    public class MachineWrapper
    {
        public static string Protect()
        {
            var testData = "SomeTestString";
            return BytesToString(MachineKey.Protect(System.Text.Encoding.UTF8.GetBytes(testData), "PasswordSafe"));
        }

        public static string Unprotect(string data)
        {
            var bytes = StringToBytes(data);
            var result = MachineKey.Unprotect(bytes, "PasswordSafe");
            return System.Text.Encoding.UTF8.GetString(result);
        }

        public static byte[] StringToBytes(string hex)
        {
            return Enumerable.Range(0, hex.Length)
                .Where(x => x % 2 == 0)
                .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                .ToArray();
        }
        public static string BytesToString(byte[] bytes)
        {
            var hex = new StringBuilder(bytes.Length * 2);
            foreach (byte b in bytes)
                hex.AppendFormat("{0:x2}", b);
            return hex.ToString().ToUpper();
        }
    }
}

If this passes both Console and the Web Application will get the same value and not throw a CryptographicException message Error occurred during a cryptographic operation. If you want to test with your own keys just run Protect from the common MachineWrapper class and record the value and re-execute for both apps.

如果这通过了控制台和Web应用程序,则会得到相同的值,而不会抛出加密操作期间发生的密码异常消息错误。如果您想使用自己的密钥进行测试,只需运行普通的MachineWrapper类的Protect,并记录下两个应用程序的值并重新执行。


2. UserManager uses Wrong Type

I would start with the previous section BUT the other failure point is that your custom machine key provider is not being used by the Microsoft.AspNet.Identity.UserManager. So here are some questions/action items that can help you figure out why this is happening:

我将从上一节开始,但是另一个失败点是您的自定义机器密钥提供程序没有被Microsoft.AspNet.Identity.UserManager使用。这里有一些问题/行动项目可以帮助你理解为什么会这样:

  1. Is container.Register the Unity IoC framework or are you using another framework?
  2. 是容器。注册Unity IoC框架还是使用其他框架?
  3. Are you sure that your Di framework is also injecting that instance in the Microsoft.AspNet.Identity.UserManager in both the Service application as well as the Web application?
  4. 确定您的Di框架也将该实例注入到Microsoft.AspNet.Identity中。服务应用程序和Web应用程序中的UserManager ?
  5. Have put a break point in public byte[] Protect of your MachineKeyDataProtector class to see if this is called in both the Service application as well as the Web application?
  6. 是否在您的machinekeydataprotection类的公共字节[]中设置了断点,以查看在服务应用程序和Web应用程序中是否都调用了它?

From examples I have seen so far (including the one you posted with the custom MachineKey solution) you need to manually bootstrap the type during application startup but then again I have not ever tried to hook into the Identity framework to replace this component using DI.

从我到目前为止看到的示例(包括您使用定制的MachineKey解决方案发布的示例)中,您需要在应用程序启动期间手动引导类型,但同样地,我从未尝试过使用DI连接到标识框架来替换此组件。

If you look at the default Visual Studio template code that is provided when you create a new MVC application the code file App_Start\IdentityConfig.cs would be the place to add this new provider.

如果您查看默认的Visual Studio模板代码,当您创建一个新的MVC应用程序时,代码文件App_Start\IdentityConfig。cs将是添加这个新的提供者的地方。

Method:

方法:

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)

Replace

取代

var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}

With this

用这个

var provider = new MachineKeyProtectionProvider();
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(provider.Create("ResetPasswordPurpose"));

And this has to be configured for both applications if you are not using a common library where this is configured.

如果您不使用公共库(配置公共库),这两个应用程序都必须进行配置。

#1


1  

As I understand your problem there are 2 possible places where failure could occur.

正如我理解你的问题,有两个地方可能会发生失败。


1. MachineKey

It could be that the MachineKey itself is not producing a consistent value between your 2 applications. This can happen if your machineKey in the .config file is not the same in both applications (I did read that you checked it but a simple type-o, added space, added to the wrong parent element, etc. could lead to this behavior.). This can be easily tested to rule it out as a point of failure. Also the behavior might be different depending on the referenced .net framework, MachineKey.Protect

可能是机器键本身并没有在您的两个应用程序之间产生一致的值。如果在.config文件中的machineKey在两个应用程序中都不相同(我确实读过您检查过它,但是一个简单的类型-o、添加的空格、添加到错误的父元素等可能导致这种行为),这可能会发生。可以很容易地测试它,将其排除为故障点。另外,根据所引用的。net框架MachineKey.Protect的不同,行为也可能不同

The configuration settings that are required for the MachineKeyCompatibilityMode.Framework45 option are required for this method even if the MachineKeySection.CompatibilityMode property is not set to the Framework45 option.

机器键盘兼容模式所需的配置设置。这个方法需要Framework45选项,即使是MachineKeySection。相容模式属性没有设置为Framework45选项。

I created a random key pair for testing and using this key I generated a test value I assigned to variable validValue below in the code. If you copy/paste the following section into your web.config and app.config the Unprotect of that keyvalue will work.

我创建了一个用于测试的随机密钥对,并使用这个密钥生成了一个测试值,我将它分配给代码下面的变量validValue。如果您将以下部分复制/粘贴到您的web中。config和app.config对该键值的不保护将起作用。

web.config / app.config

网络。配置/ app.config

<system.web>
  <httpRuntime targetFramework="4.6.1"/>
    <machineKey decryption="AES" decryptionKey="9ADCFD68D2089D79A941F9B8D06170E4F6C96E9CE996449C931F7976EF3DD209"  validation="HMACSHA256" validationKey="98D92CC1E5688DB544A1A5EF98474F3758C6819A93CC97E8684FFC7ED163C445852628E36465DB4E93BB1F8E12D69D0A99ED55639938B259D0216BD2DF4F9E73" />
</system.web>

Service Application Test

服务应用程序测试

class Program
{
    static void Main(string[] args)
    {
        // should evaluate to SomeTestString
        const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
        var unprotected2 = MachineWrapper.Unprotect(validValue);
    }
}

Mvc Controller (or Web Api controller) Test

Mvc控制器(或Web Api控制器)测试。

public class WebTestController : Controller
{
    // GET: WebTest
    public ActionResult Index()
    {
        // should evaluate to SomeTestString
        const string validValue = "03AD03E75A76CF13FDDA57425E9D362BA0FF852C4A052FD94F641B73CEBD3AC8B2F253BB45550379E44A4938371264BFA590F9E68E59DB57A9A4EB5B8B1CCC59";
        var unprotected2 = MachineWrapper.Unprotect(validValue);

        return View(unprotected2);
    }
}

Common Code

通用代码

using System;
using System.Linq;
using System.Text;
using System.Web.Security;

namespace Common
{
    public class MachineWrapper
    {
        public static string Protect()
        {
            var testData = "SomeTestString";
            return BytesToString(MachineKey.Protect(System.Text.Encoding.UTF8.GetBytes(testData), "PasswordSafe"));
        }

        public static string Unprotect(string data)
        {
            var bytes = StringToBytes(data);
            var result = MachineKey.Unprotect(bytes, "PasswordSafe");
            return System.Text.Encoding.UTF8.GetString(result);
        }

        public static byte[] StringToBytes(string hex)
        {
            return Enumerable.Range(0, hex.Length)
                .Where(x => x % 2 == 0)
                .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                .ToArray();
        }
        public static string BytesToString(byte[] bytes)
        {
            var hex = new StringBuilder(bytes.Length * 2);
            foreach (byte b in bytes)
                hex.AppendFormat("{0:x2}", b);
            return hex.ToString().ToUpper();
        }
    }
}

If this passes both Console and the Web Application will get the same value and not throw a CryptographicException message Error occurred during a cryptographic operation. If you want to test with your own keys just run Protect from the common MachineWrapper class and record the value and re-execute for both apps.

如果这通过了控制台和Web应用程序,则会得到相同的值,而不会抛出加密操作期间发生的密码异常消息错误。如果您想使用自己的密钥进行测试,只需运行普通的MachineWrapper类的Protect,并记录下两个应用程序的值并重新执行。


2. UserManager uses Wrong Type

I would start with the previous section BUT the other failure point is that your custom machine key provider is not being used by the Microsoft.AspNet.Identity.UserManager. So here are some questions/action items that can help you figure out why this is happening:

我将从上一节开始,但是另一个失败点是您的自定义机器密钥提供程序没有被Microsoft.AspNet.Identity.UserManager使用。这里有一些问题/行动项目可以帮助你理解为什么会这样:

  1. Is container.Register the Unity IoC framework or are you using another framework?
  2. 是容器。注册Unity IoC框架还是使用其他框架?
  3. Are you sure that your Di framework is also injecting that instance in the Microsoft.AspNet.Identity.UserManager in both the Service application as well as the Web application?
  4. 确定您的Di框架也将该实例注入到Microsoft.AspNet.Identity中。服务应用程序和Web应用程序中的UserManager ?
  5. Have put a break point in public byte[] Protect of your MachineKeyDataProtector class to see if this is called in both the Service application as well as the Web application?
  6. 是否在您的machinekeydataprotection类的公共字节[]中设置了断点,以查看在服务应用程序和Web应用程序中是否都调用了它?

From examples I have seen so far (including the one you posted with the custom MachineKey solution) you need to manually bootstrap the type during application startup but then again I have not ever tried to hook into the Identity framework to replace this component using DI.

从我到目前为止看到的示例(包括您使用定制的MachineKey解决方案发布的示例)中,您需要在应用程序启动期间手动引导类型,但同样地,我从未尝试过使用DI连接到标识框架来替换此组件。

If you look at the default Visual Studio template code that is provided when you create a new MVC application the code file App_Start\IdentityConfig.cs would be the place to add this new provider.

如果您查看默认的Visual Studio模板代码,当您创建一个新的MVC应用程序时,代码文件App_Start\IdentityConfig。cs将是添加这个新的提供者的地方。

Method:

方法:

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)

Replace

取代

var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}

With this

用这个

var provider = new MachineKeyProtectionProvider();
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(provider.Create("ResetPasswordPurpose"));

And this has to be configured for both applications if you are not using a common library where this is configured.

如果您不使用公共库(配置公共库),这两个应用程序都必须进行配置。