Skip to content
This repository was archived by the owner on Oct 17, 2018. It is now read-only.

Commit fe83e69

Browse files
author
Nate McMaster
committed
Add a startup filter which initializes the key ring before the server starts
1 parent 285b973 commit fe83e69

File tree

5 files changed

+173
-1
lines changed

5 files changed

+173
-1
lines changed

src/Microsoft.AspNetCore.DataProtection/DataProtectionServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.AspNetCore.DataProtection.KeyManagement;
1010
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
1111
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
12+
using Microsoft.AspNetCore.Hosting;
1213
using Microsoft.Extensions.DependencyInjection.Extensions;
1314
using Microsoft.Extensions.Logging;
1415
using Microsoft.Extensions.Options;
@@ -77,6 +78,7 @@ private static void AddDataProtectionServices(IServiceCollection services)
7778

7879
services.TryAddSingleton<IKeyManager, XmlKeyManager>();
7980
services.TryAddSingleton<IApplicationDiscriminator, HostingApplicationDiscriminator>();
81+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, DataProtectionStartupFilter>());
8082

8183
// Internal services
8284
services.TryAddSingleton<IDefaultKeyResolver, DefaultKeyResolver>();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Microsoft.AspNetCore.DataProtection.Internal
11+
{
12+
internal class DataProtectionStartupFilter : IStartupFilter
13+
{
14+
private readonly IKeyRingProvider _keyRingProvider;
15+
private readonly ILogger<DataProtectionStartupFilter> _logger;
16+
17+
public DataProtectionStartupFilter(IKeyRingProvider keyRingProvider, ILoggerFactory loggerFactory)
18+
{
19+
_keyRingProvider = keyRingProvider;
20+
_logger = loggerFactory.CreateLogger<DataProtectionStartupFilter>();
21+
}
22+
23+
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
24+
{
25+
try
26+
{
27+
// It doesn't look like much, but this preloads the key ring,
28+
// which in turn may load data from remote stores like Redis or Azure.
29+
var keyRing = _keyRingProvider.GetCurrentKeyRing();
30+
31+
_logger.KeyRingWasLoadedOnStartup(keyRing.DefaultKeyId);
32+
}
33+
catch (Exception ex)
34+
{
35+
// This should be non-fatal, so swallow, log, and allow server startup to continue.
36+
// The KeyRingProvider may be able to try again on the first request.
37+
_logger.KeyRingFailedToLoadOnStartup(ex);
38+
}
39+
40+
return next;
41+
}
42+
}
43+
}

src/Microsoft.AspNetCore.DataProtection/LoggingExtensions.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ internal static class LoggingExtensions
129129

130130
private static Action<ILogger, Exception> _policyResolutionStatesThatANewKeyShouldBeAddedToTheKeyRing;
131131

132+
private static Action<ILogger, Guid, Exception> _keyRingWasLoadedOnStartup;
133+
134+
private static Action<ILogger, Exception> _keyRingFailedToLoadOnStartup;
135+
132136
private static Action<ILogger, Exception> _usingEphemeralKeyRepository;
133137

134138
private static Action<ILogger, string, Exception> _usingRegistryAsKeyRepositoryWithDPAPI;
@@ -388,6 +392,14 @@ static LoggingExtensions()
388392
_usingAzureAsKeyRepository = LoggerMessage.Define<string>(eventId: 0,
389393
logLevel: LogLevel.Information,
390394
formatString: "Azure Web Sites environment detected. Using '{FullName}' as key repository; keys will not be encrypted at rest.");
395+
_keyRingWasLoadedOnStartup = LoggerMessage.Define<Guid>(
396+
eventId: 0,
397+
logLevel: LogLevel.Debug,
398+
formatString: "Key ring with default key {KeyId:B} was loaded during application startup.");
399+
_keyRingFailedToLoadOnStartup = LoggerMessage.Define(
400+
eventId: 0,
401+
logLevel: LogLevel.Information,
402+
formatString: "Key ring failed to load during application startup.");
391403
}
392404

393405
/// <summary>
@@ -760,5 +772,15 @@ public static void UsingAzureAsKeyRepository(this ILogger logger, string fullNam
760772
{
761773
_usingAzureAsKeyRepository(logger, fullName, null);
762774
}
775+
776+
public static void KeyRingWasLoadedOnStartup(this ILogger logger, Guid defaultKeyId)
777+
{
778+
_keyRingWasLoadedOnStartup(logger, defaultKeyId, null);
779+
}
780+
781+
public static void KeyRingFailedToLoadOnStartup(this ILogger logger, Exception innerException)
782+
{
783+
_keyRingFailedToLoadOnStartup(logger, innerException);
784+
}
763785
}
764-
}
786+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.Hosting.Server;
11+
using Microsoft.AspNetCore.Http.Features;
12+
using Microsoft.AspNetCore.Testing;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.DependencyInjection.Extensions;
15+
using Moq;
16+
using Xunit;
17+
18+
namespace Microsoft.AspNetCore.DataProtection.Test
19+
{
20+
public class HostingTests
21+
{
22+
[Fact]
23+
public async Task LoadsKeyRingBeforeServerStarts()
24+
{
25+
var tcs = new TaskCompletionSource<object>();
26+
var mockKeyRing = new Mock<IKeyRingProvider>();
27+
mockKeyRing.Setup(m => m.GetCurrentKeyRing())
28+
.Returns(Mock.Of<IKeyRing>())
29+
.Callback(() => tcs.TrySetResult(null));
30+
31+
var builder = new WebHostBuilder()
32+
.UseStartup<TestStartup>()
33+
.ConfigureServices(s =>
34+
s.AddDataProtection()
35+
.Services
36+
.Replace(ServiceDescriptor.Singleton(mockKeyRing.Object))
37+
.AddSingleton<IServer>(
38+
new FakeServer(onStart: () => tcs.TrySetException(new InvalidOperationException("Server was started before key ring was initialized")))));
39+
40+
using (var host = builder.Build())
41+
{
42+
await host.StartAsync();
43+
}
44+
45+
await tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
46+
mockKeyRing.VerifyAll();
47+
}
48+
49+
[Fact]
50+
public async Task StartupContinuesOnFailureToLoadKey()
51+
{
52+
var mockKeyRing = new Mock<IKeyRingProvider>();
53+
mockKeyRing.Setup(m => m.GetCurrentKeyRing())
54+
.Throws(new NotSupportedException("This mock doesn't actually work, but shouldn't kill the server"))
55+
.Verifiable();
56+
57+
var builder = new WebHostBuilder()
58+
.UseStartup<TestStartup>()
59+
.ConfigureServices(s =>
60+
s.AddDataProtection()
61+
.Services
62+
.Replace(ServiceDescriptor.Singleton(mockKeyRing.Object))
63+
.AddSingleton(Mock.Of<IServer>()));
64+
65+
using (var host = builder.Build())
66+
{
67+
await host.StartAsync();
68+
}
69+
70+
mockKeyRing.VerifyAll();
71+
}
72+
73+
private class TestStartup
74+
{
75+
public void Configure(IApplicationBuilder app)
76+
{
77+
}
78+
}
79+
80+
public class FakeServer : IServer
81+
{
82+
private readonly Action _onStart;
83+
84+
public FakeServer(Action onStart)
85+
{
86+
_onStart = onStart;
87+
}
88+
89+
public IFeatureCollection Features => new FeatureCollection();
90+
91+
public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
92+
{
93+
_onStart();
94+
return Task.CompletedTask;
95+
}
96+
97+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
98+
99+
public void Dispose()
100+
{
101+
}
102+
}
103+
}
104+
}

test/Microsoft.AspNetCore.DataProtection.Test/Microsoft.AspNetCore.DataProtection.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
</ItemGroup>
1818

1919
<ItemGroup>
20+
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
2021
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" />
2122
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
2223
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />

0 commit comments

Comments
 (0)