From 5627188737a11bc895427b9e15d2bdd97c979af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 3 Oct 2019 17:21:51 +0200 Subject: [PATCH] Expose the token validation parameters used by OpenIddict.Server and rework existing handlers --- .../OpenIddictServerBuilder.cs | 15 ++ .../OpenIddictServerConfiguration.cs | 2 +- .../OpenIddictServerHandlers.cs | 130 +++++++----- ...=> OpenIddictServerJsonWebTokenHandler.cs} | 2 +- .../OpenIddictServerOptions.cs | 17 +- ...ctValidationDataProtectionConfiguration.cs | 4 - ...alidationServerIntegrationConfiguration.cs | 8 +- ...IddictValidationSystemNetHttpExtensions.cs | 4 + ...ctValidationSystemNetHttpHandlerFilters.cs | 41 ++++ ...enIddictValidationSystemNetHttpHandlers.cs | 187 +++++------------- .../OpenIddictValidationBuilder.cs | 12 +- .../OpenIddictValidationConfiguration.cs | 17 +- .../OpenIddictValidationEvents.cs | 7 +- .../OpenIddictValidationHandlers.cs | 102 ++++------ ...penIddictValidationJsonWebTokenHandler.cs} | 2 +- .../OpenIddictValidationOptions.cs | 17 +- 16 files changed, 261 insertions(+), 306 deletions(-) rename src/OpenIddict.Server/{OpenIddictServerTokenHandler.cs => OpenIddictServerJsonWebTokenHandler.cs} (98%) create mode 100644 src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs rename src/OpenIddict.Validation/{OpenIddictValidationTokenHandler.cs => OpenIddictValidationJsonWebTokenHandler.cs} (97%) diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index e9bcb2f1..f3b020da 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -1682,6 +1682,21 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Issuer = address); } + /// + /// Updates the token validation parameters using the specified delegate. + /// + /// The configuration delegate. + /// The . + public OpenIddictServerBuilder SetTokenValidationParameters([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + return Configure(options => configuration(options.TokenValidationParameters)); + } + /// /// Configures OpenIddict to use reference tokens, so that authorization codes, /// access tokens and refresh tokens are stored as ciphertext in the database diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index d83e339a..ca3c465e 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -35,7 +35,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(options)); } - if (options.SecurityTokenHandler == null) + if (options.JsonWebTokenHandler == null) { throw new InvalidOperationException("The security token handler cannot be null."); } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 9acc60a4..79746325 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -77,7 +77,9 @@ namespace OpenIddict.Server AttachSelfContainedRefreshToken.Descriptor, AttachTokenDigests.Descriptor, - AttachSelfContainedIdentityToken.Descriptor) + AttachSelfContainedIdentityToken.Descriptor, + + AttachAdditionalProperties.Descriptor) .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) @@ -229,7 +231,7 @@ namespace OpenIddict.Server } // If the token cannot be validated, don't return an error to allow another handle to validate it. - if (!context.Options.SecurityTokenHandler.CanReadToken(payload)) + if (!context.Options.JsonWebTokenHandler.CanReadToken(payload)) { return; } @@ -267,15 +269,9 @@ namespace OpenIddict.Server async ValueTask ValidateTokenAsync(string token, string type) { - var parameters = new TokenValidationParameters - { - NameClaimType = Claims.Name, - PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }, - RoleClaimType = Claims.Role, - ValidIssuer = context.Issuer?.AbsoluteUri, - ValidateAudience = false, - ValidateLifetime = false - }; + var parameters = context.Options.TokenValidationParameters.Clone(); + parameters.PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }; + parameters.ValidIssuer = context.Issuer?.AbsoluteUri; parameters.IssuerSigningKeys = type switch { @@ -298,7 +294,7 @@ namespace OpenIddict.Server _ => Array.Empty() }; - return await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(token, parameters); + return await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters); } async ValueTask ValidateAnyTokenAsync(string token) @@ -406,7 +402,7 @@ namespace OpenIddict.Server }; // If the token cannot be validated, don't return an error to allow another handle to validate it. - if (string.IsNullOrEmpty(token) || !context.Options.SecurityTokenHandler.CanReadToken(token)) + if (string.IsNullOrEmpty(token) || !context.Options.JsonWebTokenHandler.CanReadToken(token)) { return; } @@ -450,15 +446,9 @@ namespace OpenIddict.Server async ValueTask ValidateTokenAsync(string token, string type) { - var parameters = new TokenValidationParameters - { - NameClaimType = Claims.Name, - PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }, - RoleClaimType = Claims.Role, - ValidIssuer = context.Issuer?.AbsoluteUri, - ValidateAudience = false, - ValidateLifetime = false - }; + var parameters = context.Options.TokenValidationParameters.Clone(); + parameters.PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }; + parameters.ValidIssuer = context.Issuer?.AbsoluteUri; parameters.IssuerSigningKeys = type switch { @@ -485,7 +475,7 @@ namespace OpenIddict.Server _ => Array.Empty() }; - return await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(token, parameters); + return await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters); } async ValueTask ValidateAnyTokenAsync(string token) @@ -1413,6 +1403,14 @@ namespace OpenIddict.Server return true; } + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + // Always exclude private claims, whose values must generally be kept secret. if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) { @@ -1437,11 +1435,8 @@ namespace OpenIddict.Server claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); } - // Note: the internal token identifier is automatically reset to ensure - // the identifier inherited from the parent token is not automatically reused. - principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) - .SetCreationDate(DateTimeOffset.UtcNow) - .SetInternalTokenId(identifier: null); + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); + principal.SetCreationDate(DateTimeOffset.UtcNow); var lifetime = context.Principal.GetAccessTokenLifetime() ?? context.Options.AccessTokenLifetime; if (lifetime.HasValue) @@ -1512,12 +1507,24 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // Note: the internal token identifier is automatically reset to ensure - // the identifier inherited from the parent token is not automatically reused. - var principal = context.Principal.Clone(_ => true) - .SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) - .SetCreationDate(DateTimeOffset.UtcNow) - .SetInternalTokenId(identifier: null); + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Other claims are always included in the authorization code, even private claims. + return true; + }); + + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); + principal.SetCreationDate(DateTimeOffset.UtcNow); var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; if (lifetime.HasValue) @@ -1587,12 +1594,24 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // Note: the internal token identifier is automatically reset to ensure - // the identifier inherited from the parent token is not automatically reused. - var principal = context.Principal.Clone(_ => true) - .SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) - .SetCreationDate(DateTimeOffset.UtcNow) - .SetInternalTokenId(identifier: null); + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Other claims are always included in the refresh token, even private claims. + return true; + }); + + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); + principal.SetCreationDate(DateTimeOffset.UtcNow); // When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed // and must exactly match the expiration date of the refresh token used in the token request. @@ -1663,6 +1682,14 @@ namespace OpenIddict.Server return true; } + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + // Always exclude private claims by default, whose values must generally be kept secret. if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) { @@ -1687,11 +1714,8 @@ namespace OpenIddict.Server claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); } - // Note: the internal token identifier is automatically reset to ensure - // the identifier inherited from the parent token is not automatically reused. - principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) - .SetCreationDate(DateTimeOffset.UtcNow) - .SetInternalTokenId(identifier: null); + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); + principal.SetCreationDate(DateTimeOffset.UtcNow); var lifetime = context.Principal.GetIdentityTokenLifetime() ?? context.Options.IdentityTokenLifetime; if (lifetime.HasValue) @@ -2058,7 +2082,7 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + descriptor.Payload = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, @@ -2169,7 +2193,7 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + descriptor.Payload = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, @@ -2280,7 +2304,7 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + descriptor.Payload = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, @@ -2510,7 +2534,7 @@ namespace OpenIddict.Server return; } - context.Response.AccessToken = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + context.Response.AccessToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, @@ -2561,7 +2585,7 @@ namespace OpenIddict.Server return; } - context.Response.Code = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + context.Response.Code = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, @@ -2612,7 +2636,7 @@ namespace OpenIddict.Server return; } - context.Response.RefreshToken = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + context.Response.RefreshToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, @@ -2793,7 +2817,7 @@ namespace OpenIddict.Server return; } - context.Response.IdToken = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + context.Response.IdToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.IdToken }, @@ -2850,7 +2874,7 @@ namespace OpenIddict.Server // If the granted access token scopes differ from the requested scopes, return the granted scopes // list as a parameter to inform the client application of the fact the scopes set will be reduced. - if (context.Request.IsAuthorizationCodeGrantType() || + if ((context.EndpointType == OpenIddictServerEndpointType.Token && context.Request.IsAuthorizationCodeGrantType()) || !context.AccessTokenPrincipal.GetScopes().SetEquals(context.Request.GetScopes())) { context.Response.Scope = string.Join(" ", context.AccessTokenPrincipal.GetScopes()); diff --git a/src/OpenIddict.Server/OpenIddictServerTokenHandler.cs b/src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs similarity index 98% rename from src/OpenIddict.Server/OpenIddictServerTokenHandler.cs rename to src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs index c549a282..1b9c1dd2 100644 --- a/src/OpenIddict.Server/OpenIddictServerTokenHandler.cs +++ b/src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs @@ -15,7 +15,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Server { - public class OpenIddictServerTokenHandler : JsonWebTokenHandler + public class OpenIddictServerJsonWebTokenHandler : JsonWebTokenHandler { public ValueTask CreateTokenFromDescriptorAsync(SecurityTokenDescriptor descriptor) { diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 3a8c5709..05055cf7 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -84,13 +84,26 @@ namespace OpenIddict.Server public IList UserinfoEndpointUris { get; } = new List(); /// - /// Gets or sets the security token handler used to protect and unprotect tokens. + /// Gets or sets the JWT handler used to protect and unprotect tokens. /// - public OpenIddictServerTokenHandler SecurityTokenHandler { get; set; } = new OpenIddictServerTokenHandler + public OpenIddictServerJsonWebTokenHandler JsonWebTokenHandler { get; set; } = new OpenIddictServerJsonWebTokenHandler { SetDefaultTimesOnTokenCreation = false }; + /// + /// Gets the token validation parameters used by the OpenIddict server services. + /// + public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, + NameClaimType = OpenIddictConstants.Claims.Name, + RoleClaimType = OpenIddictConstants.Claims.Role, + // Note: audience and lifetime are manually validated by OpenIddict itself. + ValidateAudience = false, + ValidateLifetime = false + }; + /// /// Gets or sets the period of time the authorization codes remain valid after being issued. /// While not recommended, this property can be set to null to issue codes that never expire. diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs index 3a48f608..815b8013 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs @@ -34,10 +34,6 @@ namespace OpenIddict.Validation.DataProtection throw new ArgumentNullException(nameof(options)); } - // Use empty token validation parameters to ensure the core OpenIddict validation components - // don't throw an exception stating that an issuer or a metadata address was not set. - options.TokenValidationParameters = new TokenValidationParameters(); - // Register the built-in event handlers used by the OpenIddict Data Protection validation components. foreach (var handler in OpenIddictValidationDataProtectionHandlers.DefaultHandlers) { diff --git a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs index b71bd62d..4f91b2e7 100644 --- a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs +++ b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs @@ -44,11 +44,9 @@ namespace OpenIddict.Validation.ServerIntegration options.Issuer = _options.CurrentValue.Issuer; // Import the token validation parameters from the server configuration. - options.TokenValidationParameters = new TokenValidationParameters - { - IssuerSigningKeys = (from credentials in _options.CurrentValue.SigningCredentials - select credentials.Key).ToList() - }; + options.TokenValidationParameters.IssuerSigningKeys = + (from credentials in _options.CurrentValue.SigningCredentials + select credentials.Key).ToList(); // Import the symmetric encryption keys from the server configuration. foreach (var credentials in _options.CurrentValue.EncryptionCredentials) diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs index 5b74fa16..5088151d 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using OpenIddict.Validation; using OpenIddict.Validation.SystemNetHttp; +using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpHandlerFilters; using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpHandlers; namespace Microsoft.Extensions.DependencyInjection @@ -41,6 +42,9 @@ namespace Microsoft.Extensions.DependencyInjection // Note: the order used here is not important, as the actual order is set in the options. builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + // Register the built-in filters used by the default OpenIddict System.Net.Http event handlers. + builder.Services.TryAddSingleton(); + // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. builder.Services.TryAddEnumerable(new[] { diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs new file mode 100644 index 00000000..838b9229 --- /dev/null +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Validation.OpenIddictValidationEvents; + +namespace OpenIddict.Validation.SystemNetHttp +{ + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static class OpenIddictValidationSystemNetHttpHandlerFilters + { + /// + /// Represents a filter that excludes the associated handlers if the metadata address of the issuer is not available. + /// + public class RequireHttpMetadataAddress : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Options.MetadataAddress == null) + { + return new ValueTask(false); + } + + return new ValueTask( + string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)); + } + } + } +} diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index b0d41494..c5fd8721 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -23,6 +23,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationHandlers; using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants; +using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpHandlerFilters; namespace OpenIddict.Validation.SystemNetHttp { @@ -33,79 +34,33 @@ namespace OpenIddict.Validation.SystemNetHttp /* * Authentication processing: */ - PopulateTokenValidationParametersFromMemoryCache.Descriptor, - PopulateTokenValidationParametersFromProviderConfiguration.Descriptor, - CacheTokenValidationParameters.Descriptor); - - /// - /// Contains the logic responsible of populating the token validation parameters from the memory cache. - /// - public class PopulateTokenValidationParametersFromMemoryCache : IOpenIddictValidationHandler - { - private readonly IMemoryCache _cache; - - public PopulateTokenValidationParametersFromMemoryCache([NotNull] IMemoryCache cache) - => _cache = cache; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(PopulateTokenValidationParametersFromProviderConfiguration.Descriptor.Order - 1_000) - .Build(); - - public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If token validation parameters were already attached, don't overwrite them. - if (context.TokenValidationParameters != null) - { - return default; - } - - // If the metadata address is not an HTTP/HTTPS address, let another handler populate the validation parameters. - if (!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && - !string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - return default; - } - - // Resolve the token validation parameters from the memory cache. - if (_cache.TryGetValue( - key: string.Concat("af84c073-c27c-49fd-a54f-584fd60320d3", "\x1e", context.Issuer?.AbsoluteUri), - value: out TokenValidationParameters parameters)) - { - context.TokenValidationParameters = parameters; - } - - return default; - } - } + PopulateTokenValidationParameters.Descriptor); /// /// Contains the logic responsible of populating the token validation /// parameters using OAuth 2.0/OpenID Connect discovery. /// - public class PopulateTokenValidationParametersFromProviderConfiguration : IOpenIddictValidationHandler + public class PopulateTokenValidationParameters : IOpenIddictValidationHandler { + private readonly IMemoryCache _cache; private readonly IHttpClientFactory _factory; - public PopulateTokenValidationParametersFromProviderConfiguration([NotNull] IHttpClientFactory factory) - => _factory = factory; + public PopulateTokenValidationParameters( + [NotNull] IMemoryCache cache, + [NotNull] IHttpClientFactory factory) + { + _cache = cache; + _factory = factory; + } /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateTokenValidationParameters.Descriptor.Order - 1_000) + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateSelfContainedToken.Descriptor.Order - 500) .Build(); public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) @@ -115,42 +70,45 @@ namespace OpenIddict.Validation.SystemNetHttp throw new ArgumentNullException(nameof(context)); } - // If token validation parameters were already attached, don't overwrite them. - if (context.TokenValidationParameters != null) - { - return; - } + var parameters = await _cache.GetOrCreateAsync( + key: string.Concat("af84c073-c27c-49fd-a54f-584fd60320d3", "\x1e", context.Issuer?.AbsoluteUri), + factory: async entry => + { + entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); + entry.SetPriority(CacheItemPriority.NeverRemove); - // If the metadata address is not an HTTP/HTTPS address, let another handler populate the validation parameters. - if (!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && - !string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - return; - } + return await GetTokenValidationParametersAsync(); + }); - using var client = _factory.CreateClient(Clients.Discovery); - var response = await SendHttpRequestMessageAsync(context.Options.MetadataAddress); + context.TokenValidationParameters.ValidIssuer = parameters.ValidIssuer; + context.TokenValidationParameters.IssuerSigningKeys = parameters.IssuerSigningKeys; - // Ensure the JWKS endpoint URL is present and valid. - if (!response.TryGetParameter(Metadata.JwksUri, out var endpoint) || OpenIddictParameter.IsNullOrEmpty(endpoint)) + async ValueTask GetTokenValidationParametersAsync() { - throw new InvalidOperationException("A discovery response containing an empty JWKS endpoint URL was returned."); - } + using var client = _factory.CreateClient(Clients.Discovery); + var response = await SendHttpRequestMessageAsync(client, context.Options.MetadataAddress); - if (!Uri.TryCreate((string) endpoint, UriKind.Absolute, out Uri uri)) - { - throw new InvalidOperationException("A discovery response containing an invalid JWKS endpoint URL was returned."); - } + // Ensure the JWKS endpoint URL is present and valid. + if (!response.TryGetParameter(Metadata.JwksUri, out var endpoint) || OpenIddictParameter.IsNullOrEmpty(endpoint)) + { + throw new InvalidOperationException("A discovery response containing an empty JWKS endpoint URL was returned."); + } - context.TokenValidationParameters = new TokenValidationParameters - { - ValidIssuer = (string) response[Metadata.Issuer], - IssuerSigningKeys = await GetSigningKeysAsync(uri).ToListAsync() - }; + if (!Uri.TryCreate((string) endpoint, UriKind.Absolute, out Uri uri)) + { + throw new InvalidOperationException("A discovery response containing an invalid JWKS endpoint URL was returned."); + } + + return new TokenValidationParameters + { + ValidIssuer = (string) response[Metadata.Issuer], + IssuerSigningKeys = await GetSigningKeysAsync(client, uri).ToListAsync() + }; + } - async IAsyncEnumerable GetSigningKeysAsync(Uri address) + async IAsyncEnumerable GetSigningKeysAsync(HttpClient client, Uri address) { - var response = await SendHttpRequestMessageAsync(address); + var response = await SendHttpRequestMessageAsync(client, address); var keys = response[JsonWebKeySetParameterNames.Keys]; if (keys == null) @@ -208,7 +166,7 @@ namespace OpenIddict.Validation.SystemNetHttp } } - async ValueTask SendHttpRequestMessageAsync(Uri address) + static async ValueTask SendHttpRequestMessageAsync(HttpClient client, Uri address) { using var request = new HttpRequestMessage(HttpMethod.Get, address); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); @@ -243,58 +201,5 @@ namespace OpenIddict.Validation.SystemNetHttp } } } - - /// - /// Contains the logic responsible of caching the token validation parameters. - /// - public class CacheTokenValidationParameters : IOpenIddictValidationHandler - { - private readonly IMemoryCache _cache; - - public CacheTokenValidationParameters([NotNull] IMemoryCache cache) - => _cache = cache; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateTokenValidationParameters.Descriptor.Order + 500) - .Build(); - - public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.TokenValidationParameters == null) - { - return default; - } - - // If the metadata address is not an HTTP/HTTPS address, let another handler populate the validation parameters. - if (!string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && - !string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - return default; - } - - // Store the token validation parameters in the memory cache. - _ = _cache.GetOrCreate( - key: string.Concat("af84c073-c27c-49fd-a54f-584fd60320d3", "\x1e", context.Issuer?.AbsoluteUri), - factory: entry => - { - entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); - entry.SetPriority(CacheItemPriority.NeverRemove); - - return context.TokenValidationParameters; - }); - - return default; - } - } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 1655e4dc..f6d9cf98 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -605,18 +605,18 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Sets the static token validation parameters. + /// Updates the token validation parameters using the specified delegate. /// - /// The issuer address. + /// The configuration delegate. /// The . - public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] TokenValidationParameters parameters) + public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] Action configuration) { - if (parameters == null) + if (configuration == null) { - throw new ArgumentNullException(nameof(parameters)); + throw new ArgumentNullException(nameof(configuration)); } - return Configure(options => options.TokenValidationParameters = parameters); + return Configure(options => configuration(options.TokenValidationParameters)); } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 4518eb77..4c23fe45 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -7,7 +7,6 @@ using System; using System.Diagnostics; using System.Linq; -using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -32,25 +31,13 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(options)); } - if (options.SecurityTokenHandler == null) + if (options.JsonWebTokenHandler == null) { throw new InvalidOperationException("The security token handler cannot be null."); } - if (options.TokenValidationParameters == null) + if (options.Issuer != null || options.MetadataAddress != null) { - if (options.Issuer == null && options.MetadataAddress == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The authority or an absolute metadata endpoint address must be provided.") - .Append("Alternatively, token validation parameters can be manually set by calling ") - .AppendLine("'services.AddOpenIddict().AddValidation().SetTokenValidationParameters()'.") - .Append("To use the server configuration of a local OpenIddict server instance, ") - .Append("reference the 'OpenIddict.Validation.ServerIntegration' package ") - .Append("and call 'services.AddOpenIddict().AddValidation().UseLocalServer()'.") - .ToString()); - } - if (options.MetadataAddress == null) { options.MetadataAddress = new Uri(".well-known/openid-configuration", UriKind.Relative); diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index f839ddb6..eebaff5d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -240,13 +240,12 @@ namespace OpenIddict.Validation /// public ProcessAuthenticationContext([NotNull] OpenIddictValidationTransaction transaction) : base(transaction) - { - } + => TokenValidationParameters = transaction.Options.TokenValidationParameters.Clone(); /// - /// Gets or sets the token validation parameters used for the current request. + /// Gets the token validation parameters used for the current request. /// - public TokenValidationParameters TokenValidationParameters { get; set; } + public TokenValidationParameters TokenValidationParameters { get; } /// /// Gets or sets the security principal. diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index f791780b..5d943aa0 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -28,7 +28,6 @@ namespace OpenIddict.Validation /* * Authentication processing: */ - ValidateTokenValidationParameters.Descriptor, ValidateAccessTokenParameter.Descriptor, ValidateReferenceToken.Descriptor, ValidateSelfContainedToken.Descriptor, @@ -42,64 +41,6 @@ namespace OpenIddict.Validation */ AttachDefaultChallengeError.Descriptor); - /// - /// Contains the logic responsible of ensuring the token validation parameters are populated. - /// - public class ValidateTokenValidationParameters : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: at this stage, throw an exception if the token validation parameters cannot be found. - var parameters = context.TokenValidationParameters ?? context.Options.TokenValidationParameters; - if (parameters == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The token validation parameters cannot be retrieved.") - .Append("To register the default client, reference the 'OpenIddict.Validation.SystemNetHttp' package ") - .AppendLine("and call 'services.AddOpenIddict().AddValidation().UseSystemNetHttp()'.") - .Append("Alternatively, you can manually provide the token validation parameters ") - .Append("by calling 'services.AddOpenIddict().AddValidation().SetTokenValidationParameters()'.") - .ToString()); - } - - // Clone the token validation parameters before mutating them to ensure the - // shared token validation parameters registered as options are not modified. - parameters = parameters.Clone(); - parameters.NameClaimType = Claims.Name; - parameters.PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }; - parameters.RoleClaimType = Claims.Role; - parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key); - parameters.ValidIssuer = context.Issuer?.AbsoluteUri; - parameters.ValidateAudience = false; - parameters.ValidateLifetime = false; - - context.TokenValidationParameters = parameters; - - return default; - } - } - /// /// Contains the logic responsible of validating the access token resolved from the current request. /// @@ -111,7 +52,7 @@ namespace OpenIddict.Validation public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateTokenValidationParameters.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .Build(); /// @@ -205,15 +146,27 @@ namespace OpenIddict.Validation } // If the token cannot be validated, don't return an error to allow another handle to validate it. - if (!context.Options.SecurityTokenHandler.CanReadToken(payload)) + if (!context.Options.JsonWebTokenHandler.CanReadToken(payload)) { return; } - // If the token cannot be validated, don't return an error to allow another handle to validate it. - var result = await context.Options.SecurityTokenHandler.ValidateTokenStringAsync( - payload, context.TokenValidationParameters); + // If no issuer signing key was attached, don't return an error to allow another handle to validate it. + var parameters = context.TokenValidationParameters; + if (parameters?.IssuerSigningKeys == null) + { + return; + } + // Clone the token validation parameters before mutating them to ensure the + // shared token validation parameters registered as options are not modified. + parameters = parameters.Clone(); + parameters.PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }; + parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key); + parameters.ValidIssuer = context.Issuer?.AbsoluteUri; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(payload, parameters); if (result.ClaimsIdentity == null) { return; @@ -265,15 +218,26 @@ namespace OpenIddict.Validation } // If the token cannot be validated, don't return an error to allow another handle to validate it. - if (!context.Options.SecurityTokenHandler.CanReadToken(context.Request.AccessToken)) + if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Request.AccessToken)) { return; } - // If the token cannot be validated, don't return an error to allow another handle to validate it. - var result = await context.Options.SecurityTokenHandler.ValidateTokenStringAsync( - context.Request.AccessToken, context.TokenValidationParameters); + // If no issuer signing key was attached, don't return an error to allow another handle to validate it. + var parameters = context.TokenValidationParameters; + if (parameters?.IssuerSigningKeys == null) + { + return; + } + + // Clone the token validation parameters before mutating them. + parameters = parameters.Clone(); + parameters.PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }; + parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key); + parameters.ValidIssuer = context.Issuer?.AbsoluteUri; + // If the token cannot be validated, don't return an error to allow another handle to validate it. + var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Request.AccessToken, parameters); if (result.ClaimsIdentity == null) { return; @@ -295,7 +259,7 @@ namespace OpenIddict.Validation public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000) + .SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000) .Build(); /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationTokenHandler.cs b/src/OpenIddict.Validation/OpenIddictValidationJsonWebTokenHandler.cs similarity index 97% rename from src/OpenIddict.Validation/OpenIddictValidationTokenHandler.cs rename to src/OpenIddict.Validation/OpenIddictValidationJsonWebTokenHandler.cs index 93ed4164..fc797f93 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationTokenHandler.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationJsonWebTokenHandler.cs @@ -15,7 +15,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Validation { - public class OpenIddictValidationTokenHandler : JsonWebTokenHandler + public class OpenIddictValidationJsonWebTokenHandler : JsonWebTokenHandler { public ValueTask ValidateTokenStringAsync(string token, TokenValidationParameters parameters) { diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index e431f1a3..468d054b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; namespace OpenIddict.Validation { @@ -22,9 +23,9 @@ namespace OpenIddict.Validation public IList EncryptionCredentials { get; } = new List(); /// - /// Gets or sets the security token handler used to protect and unprotect tokens. + /// Gets or sets the JWT handler used to protect and unprotect tokens. /// - public OpenIddictValidationTokenHandler SecurityTokenHandler { get; set; } = new OpenIddictValidationTokenHandler + public OpenIddictValidationJsonWebTokenHandler JsonWebTokenHandler { get; set; } = new OpenIddictValidationJsonWebTokenHandler { SetDefaultTimesOnTokenCreation = false }; @@ -79,8 +80,16 @@ namespace OpenIddict.Validation public ISet Audiences { get; } = new HashSet(StringComparer.Ordinal); /// - /// Gets or sets the token validation parameters used by the OpenIddict validation services. + /// Gets the token validation parameters used by the OpenIddict validation services. /// - public TokenValidationParameters TokenValidationParameters { get; set; } + public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, + NameClaimType = OpenIddictConstants.Claims.Name, + RoleClaimType = OpenIddictConstants.Claims.Role, + // Note: audience and lifetime are manually validated by OpenIddict itself. + ValidateAudience = false, + ValidateLifetime = false + }; } }