Skip to content

Commit

Permalink
Use ConfigurationManager in Apple provider
Browse files Browse the repository at this point in the history
Switch to using the ConfigurationManager and Apple's OpenID Connect discovery endpoint instead of the custom implementation.
Also changes PrivateKeyBytes to accept a CancellationToken.
Resolves #421.
  • Loading branch information
martincostello committed Jun 7, 2021
1 parent 6ac6acc commit 15523d2
Show file tree
Hide file tree
Showing 19 changed files with 184 additions and 414 deletions.
6 changes: 2 additions & 4 deletions docs/sign-in-with-apple.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,12 @@ Below are links to a number of other documentation sources, blog posts and sampl
|:--|:--|:--|:--|
| `ClientSecretExpiresAfter` | `TimeSpan` | The period of time after which generated client secrets expire if `GenerateClientSecret` is set to `true`. | 6 months |
| `ClientSecretGenerator` | `AppleClientSecretGenerator` | A service that generates client secrets for Sign In with Apple. | _An internal implementation_ |
| `ConfigurationManager` | `IConfigurationManager<OpenIdConnectConfiguration>?` | The configuration manager to use for the OpenID configuration. | `null` |
| `GenerateClientSecret` | `bool` | Whether to automatically generate a client secret. | `false` |
| `JwtSecurityTokenHandler` | `JwtSecurityTokenHandler` | The handler to use to validate JSON Web Keys. | `new JwtSecurityTokenHandler()` |
| `KeyId` | `string?` | The optional ID for your Sign in with Apple private key. | `null` |
| `KeyStore` | `AppleKeyStore` | A service that loads private keys to use with Sign In with Apple. | _An internal implementation_ |
| `PublicKeyCacheLifetime` | `TimeSpan` | The default period of time to cache Apple public key(s) for. | `TimeSpan.FromMinutes(15)` |
| `PublicKeyEndpoint` | `string` | The URI to use to retrieve the Apple public keys. | `AppleAuthenticationDefaults.PublicKeyEndpoint` |
| `PrivateKeyBytes` | `Func<string, Task<byte[]>>?` | An optional delegate to use to get the raw bytes of the client's private key in PKCS #8 format. | `null` |
| `TeamId` | `string` | The Team ID for your Apple Developer account. | `""` |
| `TokenAudience` | `string` | The audience used for tokens. | `AppleAuthenticationConstants.Audience` |
| `TokenValidator` | `AppleIdTokenValidator` | A service that validates Apple ID tokens. | `An internal implementation` |
| `TokenValidationParameters` | `TokenValidationParameters` | The JSON Web Token validation parameters to use. | `new TokenValidationParameters()` |
| `ValidateTokens` | `bool` | Whether to validate tokens using Apple's public key. | `true` |
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public static class AppleAuthenticationDefaults
public const string AuthorizationEndpoint = "https://appleid.apple.com/auth/authorize";

/// <summary>
/// Default value for the endpoint to get the Apple public key to verify ID token signatures.
/// Default value for <see cref="AppleAuthenticationOptions.MetadataEndpoint"/>.
/// </summary>
public const string PublicKeyEndpoint = "https://appleid.apple.com/auth/keys";
public const string MetadataEndpoint = "https://appleid.apple.com/.well-known/openid-configuration";

/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
Expand Down
3 changes: 2 additions & 1 deletion src/AspNet.Security.OAuth.Apple/AppleAuthenticationEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public virtual async Task GenerateClientSecret([NotNull] AppleGenerateClientSecr
/// <returns>
/// A <see cref="Task"/> representing the completed operation.
/// </returns>
public virtual async Task ValidateIdToken([NotNull] AppleValidateIdTokenContext context) => await OnValidateIdToken(context);
public virtual async Task ValidateIdToken([NotNull] AppleValidateIdTokenContext context) =>
await OnValidateIdToken(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ protected virtual IEnumerable<Claim> ExtractClaimsFromToken([NotNull] string tok
{
try
{
var securityToken = Options.JwtSecurityTokenHandler.ReadJwtToken(token);
var securityToken = Options.SecurityTokenHandler.ReadJsonWebToken(token);

return new List<Claim>(securityToken.Claims)
{
Expand Down
53 changes: 27 additions & 26 deletions src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
*/

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace AspNet.Security.OAuth.Apple
{
Expand All @@ -29,6 +34,7 @@ public AppleAuthenticationOptions()

Events = new AppleAuthenticationEvents();

Scope.Add("openid");
Scope.Add("name");
Scope.Add("email");

Expand All @@ -47,6 +53,13 @@ public AppleAuthenticationOptions()
/// </remarks>
public TimeSpan ClientSecretExpiresAfter { get; set; } = TimeSpan.FromSeconds(15777000); // 6 months in seconds

/// <summary>
/// Gets or sets the configuration manager responsible for retrieving, caching, and refreshing the
/// OpenID configuration from metadata. If not provided, then one will be created using the <see cref="MetadataEndpoint"/>
/// and <see cref="RemoteAuthenticationOptions.Backchannel"/> properties.
/// </summary>
public IConfigurationManager<OpenIdConnectConfiguration>? ConfigurationManager { get; set; }

/// <summary>
/// Gets or sets the <see cref="AppleAuthenticationEvents"/> used to handle authentication events.
/// </summary>
Expand All @@ -67,28 +80,19 @@ public AppleAuthenticationOptions()
public string? KeyId { get; set; }

/// <summary>
/// Gets or sets the default period of time to cache the Apple public key(s)
/// retrieved from the endpoint specified by <see cref="PublicKeyEndpoint"/>.
/// </summary>
/// <remarks>
/// The default public key cache lifetime is 15 minutes.
/// </remarks>
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"/>.
/// Gets or sets the URI the middleware uses to obtain the OpenID Connect configuration.
/// </summary>
public string PublicKeyEndpoint { get; set; } = AppleAuthenticationDefaults.PublicKeyEndpoint;
public string MetadataEndpoint { get; set; } = AppleAuthenticationDefaults.MetadataEndpoint;

/// <summary>
/// Gets or sets an optional delegate to get the raw bytes of the client's private key
/// which is passed the value of the <see cref="KeyId"/> property.
/// which is passed the value of the <see cref="KeyId"/> property and the <see cref="CancellationToken"/>
/// associated with the current HTTP request.
/// </summary>
/// <remarks>
/// The private key should be in PKCS #8 (<c>.p8</c>) format.
/// </remarks>
public Func<string, Task<byte[]>>? PrivateKeyBytes { get; set; }
public Func<string, CancellationToken, Task<byte[]>>? PrivateKeyBytes { get; set; }

/// <summary>
/// Gets or sets the Team ID for your Apple Developer account.
Expand All @@ -111,19 +115,19 @@ public AppleAuthenticationOptions()
public AppleClientSecretGenerator ClientSecretGenerator { get; set; } = default!;

/// <summary>
/// Gets or sets the <see cref="AppleKeyStore"/> to use.
/// Gets or sets the <see cref="AppleIdTokenValidator"/> to use.
/// </summary>
public AppleKeyStore KeyStore { get; set; } = default!;
public AppleIdTokenValidator TokenValidator { get; set; } = default!;

/// <summary>
/// Gets or sets the <see cref="AppleIdTokenValidator"/> to use.
/// Gets or sets the optional <see cref="JsonWebTokenHandler"/> to use.
/// </summary>
public AppleIdTokenValidator TokenValidator { get; set; } = default!;
public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!;

/// <summary>
/// Gets or sets the optional <see cref="JwtSecurityTokenHandler"/> to use.
/// Gets or sets the parameters used to validate identity tokens.
/// </summary>
public JwtSecurityTokenHandler JwtSecurityTokenHandler { get; set; } = default!;
public TokenValidationParameters TokenValidationParameters { get; set; } = default!;

/// <inheritdoc />
public override void Validate()
Expand Down Expand Up @@ -186,12 +190,9 @@ public override void Validate()
}
}

if (ValidateTokens)
if (ConfigurationManager == null)
{
if (string.IsNullOrEmpty(PublicKeyEndpoint))
{
throw new ArgumentException($"The '{nameof(PublicKeyEndpoint)}' option must be provided if the '{nameof(ValidateTokens)}' option is set to true.", nameof(PublicKeyEndpoint));
}
throw new InvalidOperationException($"The {nameof(MetadataEndpoint)}, or {nameof(ConfigurationManager)} option must be provided.");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static AppleAuthenticationOptions UsePrivateKey(
[NotNull] Func<string, IFileInfo> privateKeyFile)
{
options.GenerateClientSecret = true;
options.PrivateKeyBytes = async (keyId) =>
options.PrivateKeyBytes = async (keyId, _) =>
{
var fileInfo = privateKeyFile(keyId);

Expand Down
36 changes: 0 additions & 36 deletions src/AspNet.Security.OAuth.Apple/AppleKeyStore.cs

This file was deleted.

64 changes: 48 additions & 16 deletions src/AspNet.Security.OAuth.Apple/ApplePostConfigureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
* for more information concerning the license and the contributors participating to this project.
*/

using System.IdentityModel.Tokens.Jwt;
using System;
using System.Net.Http;
using AspNet.Security.OAuth.Apple.Internal;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace AspNet.Security.OAuth.Apple
Expand Down Expand Up @@ -46,26 +50,13 @@ public void PostConfigure(
[NotNull] string name,
[NotNull] AppleAuthenticationOptions options)
{
if (options.JwtSecurityTokenHandler is null)
{
options.JwtSecurityTokenHandler = new JwtSecurityTokenHandler();
}

// Use a custom CryptoProviderFactory so that keys are not cached and then disposed of, see below:
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1302
var cryptoProviderFactory = new CryptoProviderFactory() { CacheSignatureProviders = false };

if (options.KeyStore is null)
{
options.KeyStore = new DefaultAppleKeyStore(
_clock,
_loggerFactory.CreateLogger<DefaultAppleKeyStore>());
}

if (options.ClientSecretGenerator is null)
{
options.ClientSecretGenerator = new DefaultAppleClientSecretGenerator(
options.KeyStore,
_cache,
_clock,
cryptoProviderFactory,
Expand All @@ -75,10 +66,51 @@ public void PostConfigure(
if (options.TokenValidator is null)
{
options.TokenValidator = new DefaultAppleIdTokenValidator(
options.KeyStore,
cryptoProviderFactory,
_loggerFactory.CreateLogger<DefaultAppleIdTokenValidator>());
}

if (options.ConfigurationManager is null)
{
if (string.IsNullOrEmpty(options.MetadataEndpoint))
{
throw new InvalidOperationException($"The {nameof(AppleAuthenticationOptions.MetadataEndpoint)} property must be set on the {nameof(AppleAuthenticationOptions)} instance.");
}

// As seen in:
// github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs#L71-L102
// need this now to successfully instantiate ConfigurationManager below.
if (options.Backchannel is null)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
options.Backchannel = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler());
#pragma warning restore CA2000 // Dispose objects before losing scope
options.Backchannel.DefaultRequestHeaders.UserAgent.ParseAdd("Apple OAuth handler");
options.Backchannel.Timeout = options.BackchannelTimeout;
options.Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}

options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
options.MetadataEndpoint,
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever(options.Backchannel));
}

if (options.SecurityTokenHandler is null)
{
options.SecurityTokenHandler = new JsonWebTokenHandler();
}

if (options.TokenValidationParameters is null)
{
options.TokenValidationParameters = new TokenValidationParameters()
{
CryptoProviderFactory = cryptoProviderFactory,
ValidateAudience = true,
ValidateIssuer = true,
ValidAudience = options.ClientId,
ValidIssuer = options.TokenAudience
};
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,14 @@ internal sealed class DefaultAppleClientSecretGenerator : AppleClientSecretGener
private readonly IMemoryCache _cache;
private readonly ISystemClock _clock;
private readonly ILogger _logger;
private readonly AppleKeyStore _keyStore;
private readonly CryptoProviderFactory _cryptoProviderFactory;

public DefaultAppleClientSecretGenerator(
[NotNull] AppleKeyStore keyStore,
[NotNull] IMemoryCache cache,
[NotNull] ISystemClock clock,
[NotNull] CryptoProviderFactory cryptoProviderFactory,
[NotNull] ILogger<DefaultAppleClientSecretGenerator> logger)
{
_keyStore = keyStore;
_cache = cache;
_clock = clock;
_cryptoProviderFactory = cryptoProviderFactory;
Expand Down Expand Up @@ -96,14 +93,14 @@ private static string CreateCacheKey(AppleAuthenticationOptions options)
Subject = new ClaimsIdentity(new[] { subject }),
};

byte[] keyBlob = await _keyStore.LoadPrivateKeyAsync(context);
byte[] keyBlob = await context.Options.PrivateKeyBytes!(context.Options.KeyId!, context.HttpContext.RequestAborted);
string clientSecret;

using (var algorithm = CreateAlgorithm(keyBlob))
{
tokenDescriptor.SigningCredentials = CreateSigningCredentials(context.Options.KeyId!, algorithm);

clientSecret = context.Options.JwtSecurityTokenHandler.CreateEncodedJwt(tokenDescriptor);
clientSecret = context.Options.SecurityTokenHandler.CreateToken(tokenDescriptor);
}

_logger.LogTrace("Generated new client secret with value {ClientSecret}.", clientSecret);
Expand Down
Loading

0 comments on commit 15523d2

Please sign in to comment.