diff --git a/Directory.Packages.props b/Directory.Packages.props index 62322f922..568b995c9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -98,7 +98,6 @@ - diff --git a/samples/AppConfig/AppConfigFramework/Web.config b/samples/AppConfig/AppConfigFramework/Web.config index aaf3035c4..1ec4f8dcf 100644 --- a/samples/AppConfig/AppConfigFramework/Web.config +++ b/samples/AppConfig/AppConfigFramework/Web.config @@ -23,7 +23,6 @@ - @@ -154,4 +153,4 @@ - \ No newline at end of file + diff --git a/samples/AuthRemoteIdentity/AuthRemoteIdentityFramework/Web.config b/samples/AuthRemoteIdentity/AuthRemoteIdentityFramework/Web.config index 0bb3f0c6d..5956cb031 100644 --- a/samples/AuthRemoteIdentity/AuthRemoteIdentityFramework/Web.config +++ b/samples/AuthRemoteIdentity/AuthRemoteIdentityFramework/Web.config @@ -28,7 +28,6 @@ - diff --git a/samples/MachineKey/MachineKeyFramework/Global.asax.cs b/samples/MachineKey/MachineKeyFramework/Global.asax.cs index dbdc90089..47762a063 100644 --- a/samples/MachineKey/MachineKeyFramework/Global.asax.cs +++ b/samples/MachineKey/MachineKeyFramework/Global.asax.cs @@ -14,6 +14,7 @@ protected void Application_Start() { HttpApplicationHost.RegisterHost(builder => { + builder.AddSystemWebDependencyInjection(); builder.AddServiceDefaults(); builder.AddDataProtection() .SetApplicationName(MachineKeyExampleHandler.AppName) diff --git a/samples/MachineKey/MachineKeyFramework/MachineKeyFramework.csproj b/samples/MachineKey/MachineKeyFramework/MachineKeyFramework.csproj index 1b6276f64..1f71bca8d 100644 --- a/samples/MachineKey/MachineKeyFramework/MachineKeyFramework.csproj +++ b/samples/MachineKey/MachineKeyFramework/MachineKeyFramework.csproj @@ -2,9 +2,6 @@ net481 - - - diff --git a/samples/MachineKey/MachineKeyFramework/Web.config b/samples/MachineKey/MachineKeyFramework/Web.config index 0ef39e937..aace065ca 100644 --- a/samples/MachineKey/MachineKeyFramework/Web.config +++ b/samples/MachineKey/MachineKeyFramework/Web.config @@ -7,7 +7,6 @@ - diff --git a/samples/SessionRemote/SessionRemoteFramework/Web.config b/samples/SessionRemote/SessionRemoteFramework/Web.config index 62820678e..4b0b1eb8e 100644 --- a/samples/SessionRemote/SessionRemoteFramework/Web.config +++ b/samples/SessionRemote/SessionRemoteFramework/Web.config @@ -15,7 +15,6 @@ - @@ -146,4 +145,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/CompatibilityDataProtector.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/CompatibilityDataProtector.cs new file mode 100644 index 000000000..0d440dffe --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/CompatibilityDataProtector.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Configuration; +using System.Security.Cryptography; +using System.Web; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.SystemWebAdapters.Hosting; + +namespace Microsoft.AspNetCore.DataProtection.SystemWeb; + +[EditorBrowsable(EditorBrowsableState.Never)] +public class CompatibilityDataProtector : DataProtector +{ + [ThreadStatic] + private static bool _suppressPrimaryPurpose; + + private readonly Lazy _lazyProtector; + private readonly Lazy _lazyProtectorSuppressedPrimaryPurpose; + + public CompatibilityDataProtector(string applicationName, string primaryPurpose, string[] specificPurposes) + : base("application-name", "primary-purpose", null) // we feed dummy values to the base ctor + { + // We don't want to evaluate the IDataProtectionProvider factory quite yet, + // as we'd rather defer failures to the call to Protect so that we can bubble + // up a good error message to the developer. + + _lazyProtector = new Lazy(() => GetDataProtectionProvider().CreateProtector(primaryPurpose, specificPurposes)); + + // System.Web always provides "User.MachineKey.Protect" as the primary purpose for calls + // to MachineKey.Protect. Only in this case should we allow suppressing the primary + // purpose, as then we can easily map calls to MachineKey.Protect(userData, purposes) + // into calls to provider.GetProtector(purposes).Protect(userData). + if (primaryPurpose == "User.MachineKey.Protect") + { + _lazyProtectorSuppressedPrimaryPurpose = new Lazy(() => GetDataProtectionProvider().CreateProtector(specificPurposes)); + } + else + { + _lazyProtectorSuppressedPrimaryPurpose = _lazyProtector; + } + } + + // We take care of flowing purposes ourselves. + protected override bool PrependHashedPurposeToPlaintext => false; + + // Retrieves the appropriate protector (potentially with a suppressed primary purpose) for this operation. + private IDataProtector Protector => ((_suppressPrimaryPurpose) ? _lazyProtectorSuppressedPrimaryPurpose : _lazyProtector).Value; + + protected virtual IDataProtectionProvider GetDataProtectionProvider() + => HttpApplicationHost.Current.Services.GetDataProtectionProvider(); + + public override bool IsReprotectRequired(byte[] encryptedData) + { + // Nobody ever calls this. + return false; + } + + protected override byte[] ProviderProtect(byte[] userData) + { + try + { + return Protector.Protect(userData); + } + catch (Exception ex) + { + // System.Web special-cases ConfigurationException errors and allows them to bubble + // up to the developer without being homogenized. Since a call to Protect should + // never fail, any exceptions here really do imply a misconfiguration. + +#pragma warning disable CS0618 // Type or member is obsolete + throw new ConfigurationException("DataProtection failed to protect", ex); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + protected override byte[] ProviderUnprotect(byte[] encryptedData) + { + return Protector.Unprotect(encryptedData); + } + + /// + /// Invokes a delegate where calls to + /// and will ignore the primary + /// purpose and instead use only the sub-purposes. + /// + public static byte[] RunWithSuppressedPrimaryPurpose(Func callback, object state, byte[] input) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + if (_suppressPrimaryPurpose) + { + return callback(state, input); // already suppressed - just forward call + } + + try + { + try + { + _suppressPrimaryPurpose = true; + return callback(state, input); + } + finally + { + _suppressPrimaryPurpose = false; + } + } + catch + { + // defeat exception filters + throw; + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices.csproj b/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices.csproj index afaf8e5a8..cd2073677 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices.csproj +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices.csproj @@ -27,7 +27,6 @@ - diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/SystemWebDataProtectionExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/SystemWebDataProtectionExtensions.cs index ab494d9f8..edbeaa387 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/SystemWebDataProtectionExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/SystemWebDataProtectionExtensions.cs @@ -2,80 +2,204 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Configuration; +using System.Reflection; +using System.Security.Cryptography; using System.Web.Configuration; +using System.Web.Hosting; +using System.Web.Security; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.DataProtection.Infrastructure; using Microsoft.AspNetCore.DataProtection.SystemWeb; using Microsoft.AspNetCore.SystemWebAdapters.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace System.Web; -public static class SystemWebDataProtectionExtensions +public static partial class SystemWebDataProtectionExtensions { - private const string StartupTypeKey = "aspnet:dataProtectionStartupType"; - - public static IDataProtectionBuilder AddDataProtection(this HttpApplicationHostBuilder builder) + /// + /// Adds to the and enables integration. + /// + /// The . + /// The setup action for data protection + public static IDataProtectionBuilder AddDataProtection(this HttpApplicationHostBuilder builder, Action setupAction) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } - if (!IsMachineKeyOverriden()) + if (setupAction is null) { - const string ExpectedSetup = """ - - """; - throw new InvalidOperationException($"Must configure machine key in web.config to use data protection: {ExpectedSetup}"); + throw new ArgumentNullException(nameof(setupAction)); } - if (!TrySetStartupType()) + builder.Services.AddMachineKey(); + + return builder.Services.AddDataProtection(setupAction); + } + + /// + /// Adds to the and enables integration. + /// + /// The . + public static IDataProtectionBuilder AddDataProtection(this HttpApplicationHostBuilder builder) + { + if (builder is null) { - throw new InvalidOperationException($"Must not manually set the '{StartupTypeKey}' app setting when using HttpApplicationHostBuilder.AddDataProtection."); + throw new ArgumentNullException(nameof(builder)); } + builder.Services.AddMachineKey(); + return builder.Services.AddDataProtection(); } - private static bool TrySetStartupType() + private static void AddMachineKey(this IServiceCollection services) { - var startupType = typeof(MachineKeyImpl).AssemblyQualifiedName; - - var current = ConfigurationManager.AppSettings[StartupTypeKey]; - - if (current != startupType && !string.IsNullOrEmpty(current)) + if (ConfigurationManager.GetSection("system.web/machineKey") is MachineKeySection section) { - return false; + if (!string.IsNullOrEmpty(section.DataProtectorType)) + { + throw new InvalidOperationException("Could not set up DataProtection for use with MachineKey because 'system.web/machineKey' already has 'dataProtectorType' configured. Remove or clear the 'dataProtectorType' attribute (and 'compatibilityMode' if it was previously set for older MachineKey/DataProtection integration), or do not call AddDataProtection if you want to keep the existing machineKey configuration. AddDataProtection now configures MachineKey integration automatically."); + } } - ConfigurationManager.AppSettings[StartupTypeKey] = startupType; - - return true; + // We use this to auto-start it ASAP to ensure the dataprotector is set up before anyone else tries to do anything + services.AddHostedService(); + services.TryAddSingleton(); } - private static bool IsMachineKeyOverriden() + /// + /// This is used to initialized the data protection infrastructure early in the set up for . Optionally, will run + /// a runtime diagnostic to verify it is set up correctly. + /// + private sealed partial class MachineKeySetup(IServiceProvider sp, IHostEnvironment env, ILogger logger) : IHostedService { - if (ConfigurationManager.GetSection("system.web/machineKey") is MachineKeySection section) + private static readonly FieldInfo _configField = GetRequiredField("s_config"); + private static readonly MethodInfo _getApplicationConfig = GetRequiredMethod("GetApplicationConfig"); + + [LoggerMessage(LogLevel.Trace, EventId = 0, Message = "Initializing MachineKey infrastructure to use IDataProtection")] + private static partial void LogInitializing(ILogger logger); + + [LoggerMessage(LogLevel.Trace, EventId = 1, Message = "Running test to validate IDataProtection")] + private static partial void LogRunningTest(ILogger logger); + + [LoggerMessage(LogLevel.Trace, EventId = 2, Message = "Initialized MachineKey infrastructure to use IDataProtection")] + private static partial void LogInitialized(ILogger logger); + + public Task StartAsync(CancellationToken cancellationToken) { - if (section.CompatibilityMode != MachineKeyCompatibilityMode.Framework45) + LogInitializing(logger); + + Initialize(); + + if (env.IsDevelopment()) { - return false; + LogRunningTest(logger); + ValidateMachineKey(); } - if (section.DataProtectorType is { } typeString && Type.GetType(typeString) is { } type && type == typeof(CompatibilityDataProtector)) + LogInitialized(logger); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// This method initializes the infrastructure such that it will use the . + /// Since this uses reflection, there are a few checks to make sure things are set up correctly. .NET Framework is not being changed at + /// this point that much, so there's little risk in relying on some of these internals. + /// + private static void Initialize() + { + var existing = GetApplicationConfig(); + var updated = new MachineKeySection() + { + ApplicationName = existing.ApplicationName, + CompatibilityMode = MachineKeyCompatibilityMode.Framework45, + DataProtectorType = typeof(CompatibilityDataProtector).AssemblyQualifiedName, + Decryption = existing.Decryption, + DecryptionKey = existing.DecryptionKey, + }; + + Value = updated; + + // Force MachineKey to start up data protection + _ = MachineKey.Protect([]); + } + + /// + /// This is a mini test in production when ran in development mode to verify that things are setup correctly + /// + private void ValidateMachineKey() + { + using var rng = RandomNumberGenerator.Create(); + + // Arrange + var dp = sp.GetDataProtector("User.MachineKey.Protect"); + var bytes = new byte[10]; + rng.GetBytes(bytes); + + // Act + var dpProtected = dp.Protect(bytes); + var mProtected = MachineKey.Protect(bytes); + + // Assert + var unprotected1 = MachineKey.Unprotect(dpProtected); + var unprotected2 = dp.Unprotect(mProtected); + + if (!bytes.SequenceEqual(unprotected1) || !bytes.SequenceEqual(unprotected2)) { - return true; + throw new InvalidOperationException("DataProtection was not setup correctly for MachineKey"); } } - return false; + private static MachineKeySection Value + { + get => (MachineKeySection)_configField.GetValue(null); + set => _configField.SetValue(null, value); + } + + private static MachineKeySection GetApplicationConfig() => (MachineKeySection)_getApplicationConfig.Invoke(null, []); + + private static FieldInfo GetRequiredField(string name) + { + var field = typeof(MachineKeySection).GetField(name, BindingFlags.NonPublic | BindingFlags.Static); + + return field ?? throw new NotSupportedException($"The required MachineKeySection field '{name}' could not be found. The current System.Web implementation is not supported."); + } + + private static MethodInfo GetRequiredMethod(string name) + { + var method = typeof(MachineKeySection).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static); + + return method ?? throw new NotSupportedException($"The required MachineKeySection method '{name}' could not be found. The current System.Web implementation is not supported."); + } } - private sealed class MachineKeyImpl : DataProtectionStartup + private sealed class SystemWebApplicationDiscriminator : IApplicationDiscriminator { - public override IDataProtectionProvider CreateDataProtectionProvider(IServiceProvider services) + public string? Discriminator { get; } = GetAppDiscriminatorCore(); + + private static string GetAppDiscriminatorCore() { - return base.CreateDataProtectionProvider(HttpApplicationHost.Current.Services); + // Try reading the discriminator from defined + // at the web app root. If the value was set explicitly (even if the value is empty), + // honor it as the discriminator. + var machineKeySection = (MachineKeySection)WebConfigurationManager.GetWebApplicationSection("system.web/machineKey"); + if (machineKeySection.ElementInformation.Properties["applicationName"].ValueOrigin != PropertyValueOrigin.Default) + { + return machineKeySection.ApplicationName; + } + else + { + return HttpRuntime.AppDomainAppId; + } } } }