60

I am implementing reset password functionality on my site by using the in-built UserManager class that comes with ASP.NET 5.

Everything works fine in my dev environment. However, once I try it in the production site that is running as an Azure website, I get the following exception:

System.Security.Cryptography.CryptographicException: The data protection operation was unsuccessful. This may have been caused by not having the user profile loaded for the current thread's user context, which may be the case when the thread is impersonating.

This is how I setup the UserManager instance:

var provider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider(SiteConfig.SiteName);
UserManager.UserTokenProvider = new Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider<User>(provider.Create(ResetPasswordPurpose));

Then, I generate the token thusly (to be sent to the user in an email so that they can verify that they do indeed want to reset their password):

string token = UserManager.GeneratePasswordResetToken(user.Id);

Unfortunately, when this runs on Azure, I get the exception above.

I've Googled around and found this possible solution. However, it didn't work at all and I still get the same exception.

According to the link, it has something to do with session tokens not working on a web farm like Azure.

Jeroen
  • 60,696
  • 40
  • 206
  • 339
Andrew
  • 11,068
  • 17
  • 52
  • 62

4 Answers4

92

The DpapiDataProtectionProvider utilizes DPAPI which will not work properly in a web farm/cloud environment since encrypted data can only be decrypted by the machine that encypted it. What you need is a way to encrypt data such that it can be decrypted by any machine in your environment. Unfortunately, ASP.NET Identity 2.0 does not include any other implementation of IProtectionProvider other than DpapiDataProtectionProvider. However, it's not too difficult to roll your own.

One option is to utilize the MachineKey class as follows:

public class MachineKeyProtectionProvider : IDataProtectionProvider
{
    public IDataProtector Create(params string[] purposes)
    {
        return new MachineKeyDataProtector(purposes);
    }
}

public class MachineKeyDataProtector : IDataProtector
{
    private readonly string[] _purposes;

    public MachineKeyDataProtector(string[] purposes)
    {
        _purposes = purposes;
    }

    public byte[] Protect(byte[] userData)
    {
        return MachineKey.Protect(userData, _purposes);
    }

    public byte[] Unprotect(byte[] protectedData)
    {
        return MachineKey.Unprotect(protectedData, _purposes);
    }
}

In order to use this option, there are a couple of steps that you would need to follow.

Step 1

Modify your code to use the MachineKeyProtectionProvider.

using Microsoft.AspNet.Identity.Owin;
// ...

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

Step 2

Synchronize the MachineKey value across all the machines in your web farm/cloud environment. This sounds scary, but it's the same step that we've performed countless times before in order to get ViewState validation to work properly in a web farm (it also uses DPAPI).

dav_i
  • 27,509
  • 17
  • 104
  • 136
Chris Staley
  • 2,370
  • 1
  • 20
  • 21
  • Reported [here too](http://tech.trailmax.info/2014/06/asp-net-identity-and-cryptographicexception-when-running-your-site-on-microsoft-azure-web-sites/#comment-1640502238). This is great feed back for asp.net team's [UserVoice](https://aspnet.uservoice.com/forums/41199-general-asp-net) and [Codeplex Issues](https://aspnetidentity.codeplex.com/WorkItem/Create?ProjectName=aspnetidentity) Will you raise an issue? – OzBob Oct 20 '14 at 02:49
  • Is that a bug? `ResetPasswordPurpose` should be a string right? Like `"ResetPasswordPurpose"`? – Serj Sagan Nov 11 '14 at 18:40
  • @yqit You can put it wherever you'd like, but It needs to be executed when the application starts. e.g. In an MVC 5 app, it should be executed by `MvcApplication.Application_Start()`. – Chris Staley Dec 08 '15 at 18:45
  • I've used this now for a long time with success, but now we added a .NET Core project. How would I need to do this now? Considering this documentation: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/compatibility/replacing-machinekey and this for storing in Azure Blob storage: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers – CularBytes Feb 19 '18 at 14:14
  • @CularBytes First off, please note that my answer is nearly 4 years old, so there's been a lot of change in APIs and the underlying platform since it was written. One of the other up-voted answers may be more helpful at this point. With that said, it looks like the only change in your situation would be in Step 2, since you want to store your machine key in Azure Blob Storage. There is a sample in the documentation that you linked to that points out how to accomplish this: https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers#azure-and-redis – Chris Staley Feb 20 '18 at 15:07
  • Worked. I didn't need to do step 2. Best answer. Thank you – Nour Lababidi Oct 16 '18 at 01:27
  • you literally saved my day, thank you very much! I didn't need step 2 and I was managing to have a WIF application hosted on Plesk Obsidian 18 – MAXE Jul 16 '21 at 08:31
47

Consider using IAppBuilder.GetDataProtectionProvider() instead of declaring a new DpapiDataProtectionProvider.

Similar to you, I had introduced this issue by configuring my UserManager like this, from a code sample I found:

public class UserManager : UserManager<ApplicationUser>
{
    public UserManager() : base(new UserStore<ApplicationUser>(new MyDbContext()))
    {
        // this does not work on azure!!!
        var provider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider("ASP.NET IDENTITY");
        this.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(provider.Create("EmailConfirmation"))
        {
            TokenLifespan = TimeSpan.FromHours(24),
        };
    }
}

The CodePlex issue linked to above actually references a blog post which has been updated with a simpler solution to the problem. It recommends saving a static reference to the IDataProtector...

public partial class Startup
{
    internal static IDataProtectionProvider DataProtectionProvider { get; private set; }

    public void ConfigureAuth(IAppBuilder app)
    {
        DataProtectionProvider = app.GetDataProtectionProvider();
        // other stuff.
    }
}

...and then referencing it from within the UserManager

public class UserManager : UserManager<ApplicationUser>
{
    public UserManager() : base(new UserStore<ApplicationUser>(new MyDbContext()))
    {
        var dataProtectionProvider = Startup.DataProtectionProvider;
        this.UserTokenProvider = 
                new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));

        // do other configuration
    }
}

The answer from johnso also provides a good example of how to wire this up using Autofac.

Loren Paulsen
  • 8,960
  • 1
  • 28
  • 38
  • Does this resolve the issue where DPAPI doesn't work in a web farm as mentioned in the top answer? – BenCr Jun 11 '15 at 09:07
  • No, but it allows ASP.NET Identity 2.0 to work in Azure Websites (Web Apps), which I believe is the intent of the question. The top answer has its own issues with Azure, as setting the MachineKey brings its own challenges: http://stackoverflow.com/a/29765371/94853 – Loren Paulsen Jul 28 '15 at 00:29
  • Thanks @Loren, works well also when you have a custom implementation of Identity – dalcam Aug 25 '15 at 03:20
  • this worked for me. thank you very much @LorenPaulsen, you saved my day. – Syed Ali Taqi Oct 03 '16 at 11:07
  • There is indeed a difference between webfarms or VMs on Azure (where you can set/sync the machine keys) and an Azure App Service (Azure Website) where you can't set the machine key. This answer will fix the last. The other answer the former. – Peter Jun 04 '21 at 20:05
25

I had the same issues except I was hosting on amazon ec2.
I was able to resolve it by going to the application pool in IIS and (under advanced settings after a right click) setting process model - load user profile = true.

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
boone
  • 503
  • 1
  • 6
  • 7
12

I was having the same issue (Owin.Security.DataProtection.DpapiDataProtectionProvider failing when ran on Azure), and Staley is correct, you cannot use DpapiDataProtectionProvider.

If you're using OWIN Startup Classes you can avoid rolling your own IDataProtectionProvider, instead use the GetDataProtectionProvider method of IAppBuilder.

For instance, with Autofac:

internal static IDataProtectionProvider DataProtectionProvider;    

public void ConfigureAuth(IAppBuilder app)
{
    // ...

    DataProtectionProvider = app.GetDataProtectionProvider();

    builder.Register<IDataProtectionProvider>(c => DataProtectionProvider)
        .InstancePerLifetimeScope();

    // ...
}
Dr Rob Lang
  • 6,659
  • 5
  • 40
  • 60
Aaron B
  • 859
  • 11
  • 17
  • Thanks for keeping this answer up despite someone's downvote. It works nicely for me. – Rob Lyndon Aug 15 '15 at 18:34
  • Wouldn't `builder.RegisterInstance(app.GetDataProtectionProvider());` be a better choice? – Marc L. Mar 22 '16 at 19:10
  • 1
    Adding the WEBSITE_LOAD_USER_PROFILE to the web.config does not work, adding the setting in Azure works just fine, but requesting the DataProtectionProvider using app.GetDataProtectionProvider() is the best solution I've found. Thank you! – Vincent Apr 22 '18 at 19:34