MachineKeyDataProtector - Invalid link when confirmation email sent through background job

筅森魡賤 提交于 2021-02-10 12:28:12

问题


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".

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:

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

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

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

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.

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)

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

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

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

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

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).

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.


回答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

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.

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.

web.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

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.


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:

  1. Is container.Register the Unity IoC framework or are you using another framework?
  2. 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?
  3. 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?

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.

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.

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.



来源:https://stackoverflow.com/questions/35735021/machinekeydataprotector-invalid-link-when-confirmation-email-sent-through-back

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!