Skip to content

Commit

Permalink
Periodically reload Apple public keys
Browse files Browse the repository at this point in the history
Reload the cached Apple public keys, with a default reload period of at least every 15 minutes after first load.
Resolves aspnet-contrib#383.
  • Loading branch information
martincostello committed Feb 14, 2020
1 parent 451d8b9 commit b5a4c7f
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public AppleAuthenticationOptions()
/// </summary>
public string KeyId { get; set; }

/// <summary>
/// Gets or sets the default period of time to cache the Apple public key(s)
/// retreived from the endpoint specified by <see cref="PublicKeyEndpoint"/> for.
/// </summary>
public TimeSpan PublicKeyCacheLifetime { get; set; } = TimeSpan.FromMinutes(15);

/// <summary>
/// Gets or sets the URI the middleware will access to obtain the public key for
/// validating tokens if <see cref="ValidateTokens"/> is <see langword="true"/>.
Expand Down
19 changes: 16 additions & 3 deletions src/AspNet.Security.OAuth.Apple/Internal/DefaultAppleKeyStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@
using System.Net.Http;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;

namespace AspNet.Security.OAuth.Apple.Internal
{
internal sealed class DefaultAppleKeyStore : AppleKeyStore
{
private readonly ISystemClock _clock;
private readonly ILogger _logger;

private byte[] _publicKey;
private DateTimeOffset _reloadKeysAfter;

public DefaultAppleKeyStore(
[NotNull] ISystemClock clock,
[NotNull] ILogger<DefaultAppleKeyStore> logger)
{
_clock = clock;
_logger = logger;
}

Expand All @@ -40,18 +45,26 @@ public override async Task<byte[]> LoadPrivateKeyAsync([NotNull] AppleGenerateCl
/// <inheritdoc />
public override async Task<byte[]> LoadPublicKeysAsync([NotNull] AppleValidateIdTokenContext context)
{
if (_publicKey == null)
var utcNow = _clock.UtcNow;

if (_publicKey == null || _reloadKeysAfter < utcNow)
{
_logger.LogInformation("Loading Apple public keys from {PublicKeyEndpoint}.", context.Options.PublicKeyEndpoint);

_publicKey = await LoadApplePublicKeysAsync(context);
_reloadKeysAfter = utcNow.Add(context.Options.PublicKeyCacheLifetime);

_logger.LogInformation(
"Loaded Apple public keys from {PublicKeyEndpoint}. Keys will be reloaded at or after {ReloadKeysAfter}.",
context.Options.PublicKeyEndpoint,
_reloadKeysAfter);
}

return _publicKey;
}

private async Task<byte[]> LoadApplePublicKeysAsync([NotNull] AppleValidateIdTokenContext context)
{
_logger.LogInformation("Loading Apple public keys from {PublicKeyEndpoint}.", context.Options.PublicKeyEndpoint);

var response = await context.Options.Backchannel.GetAsync(context.Options.PublicKeyEndpoint, context.HttpContext.RequestAborted);

if (!response.IsSuccessStatusCode)
Expand Down
81 changes: 81 additions & 0 deletions test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;
using Shouldly;
Expand Down Expand Up @@ -316,6 +318,85 @@ static void ConfigureServices(IServiceCollection services)
exception.InnerException.Message.ShouldBe("No Apple ID token was returned in the OAuth token response.");
}

[Fact]
public async Task Apple_Public_Keys_Are_Cached()
{
// Arrange
static void ConfigureServices(IServiceCollection services)
{
services.PostConfigureAll<AppleAuthenticationOptions>((options) =>
{
options.PublicKeyCacheLifetime = TimeSpan.FromMinutes(1);
});
}

using var server = CreateTestServer(ConfigureServices);

var keyStore = server.Services.GetRequiredService<AppleKeyStore>();
var options = server.Services.GetRequiredService<IOptions<AppleAuthenticationOptions>>().Value;

var context = new AppleValidateIdTokenContext(
new DefaultHttpContext(),
new AuthenticationScheme("apple", "Apple", typeof(AppleAuthenticationHandler)),
options,
"my-token");

// Act
byte[] actual1 = await keyStore.LoadPublicKeysAsync(context);
byte[] actual2 = await keyStore.LoadPublicKeysAsync(context);

// Assert
actual1.ShouldNotBeNull();
actual1.ShouldNotBeEmpty();
actual1.ShouldBeSameAs(actual2);
}

[Fact]
public async Task Apple_Public_Keys_Are_Reloaded_Once_Cache_Lieftime_Expires()
{
// Arrange
static void ConfigureServices(IServiceCollection services)
{
services.PostConfigureAll<AppleAuthenticationOptions>((options) =>
{
options.PublicKeyCacheLifetime = TimeSpan.FromSeconds(0.25);
});
}

using var server = CreateTestServer(ConfigureServices);

var keyStore = server.Services.GetRequiredService<AppleKeyStore>();
var options = server.Services.GetRequiredService<IOptions<AppleAuthenticationOptions>>().Value;

var context = new AppleValidateIdTokenContext(
new DefaultHttpContext(),
new AuthenticationScheme("apple", "Apple", typeof(AppleAuthenticationHandler)),
options,
"my-token");

// Act
byte[] actual1 = await keyStore.LoadPublicKeysAsync(context);
byte[] actual2 = await keyStore.LoadPublicKeysAsync(context);

// Assert
actual1.ShouldNotBeNull();
actual1.ShouldNotBeEmpty();
actual1.ShouldBeSameAs(actual2);

// Arrange
await Task.Delay(TimeSpan.FromSeconds(1));

// Act
actual2 = await keyStore.LoadPublicKeysAsync(context);

// Assert
actual1.ShouldNotBeNull();
actual1.ShouldNotBeEmpty();
actual2.ShouldNotBeNull();
actual2.ShouldNotBeEmpty();
actual1.ShouldNotBeSameAs(actual2);
}

private sealed class FrozenJwtSecurityTokenHandler : JwtSecurityTokenHandler
{
protected override void ValidateLifetime(DateTime? notBefore, DateTime? expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
Expand Down

0 comments on commit b5a4c7f

Please sign in to comment.