Browse Source

Expose the token validation parameters used by OpenIddict.Server and rework existing handlers

pull/818/head
Kévin Chalet 6 years ago
committed by GitHub
parent
commit
5627188737
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  2. 2
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  3. 130
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  4. 2
      src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs
  5. 17
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  6. 4
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConfiguration.cs
  7. 8
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs
  8. 4
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs
  9. 41
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs
  10. 187
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  11. 12
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  12. 17
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  13. 7
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  14. 102
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  15. 2
      src/OpenIddict.Validation/OpenIddictValidationJsonWebTokenHandler.cs
  16. 17
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs

15
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1682,6 +1682,21 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.Issuer = address);
}
/// <summary>
/// Updates the token validation parameters using the specified delegate.
/// </summary>
/// <param name="configuration">The configuration delegate.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetTokenValidationParameters([NotNull] Action<TokenValidationParameters> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options => configuration(options.TokenValidationParameters));
}
/// <summary>
/// Configures OpenIddict to use reference tokens, so that authorization codes,
/// access tokens and refresh tokens are stored as ciphertext in the database

2
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.");
}

130
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<TokenValidationResult> ValidateTokenAsync(string token, string type)
{
var parameters = new TokenValidationParameters
{
NameClaimType = Claims.Name,
PropertyBag = new Dictionary<string, object> { [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<string, object> { [Claims.Private.TokenUsage] = type };
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.IssuerSigningKeys = type switch
{
@ -298,7 +294,7 @@ namespace OpenIddict.Server
_ => Array.Empty<SecurityKey>()
};
return await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(token, parameters);
return await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters);
}
async ValueTask<TokenValidationResult> 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<TokenValidationResult> ValidateTokenAsync(string token, string type)
{
var parameters = new TokenValidationParameters
{
NameClaimType = Claims.Name,
PropertyBag = new Dictionary<string, object> { [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<string, object> { [Claims.Private.TokenUsage] = type };
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.IssuerSigningKeys = type switch
{
@ -485,7 +475,7 @@ namespace OpenIddict.Server
_ => Array.Empty<SecurityKey>()
};
return await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(token, parameters);
return await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters);
}
async ValueTask<TokenValidationResult> 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<string, object> { [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<string, object> { [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<string, object> { [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<string, object> { [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<string, object> { [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<string, object> { [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<string, object> { [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());

2
src/OpenIddict.Server/OpenIddictServerTokenHandler.cs → 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<string> CreateTokenFromDescriptorAsync(SecurityTokenDescriptor descriptor)
{

17
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -84,13 +84,26 @@ namespace OpenIddict.Server
public IList<Uri> UserinfoEndpointUris { get; } = new List<Uri>();
/// <summary>
/// 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.
/// </summary>
public OpenIddictServerTokenHandler SecurityTokenHandler { get; set; } = new OpenIddictServerTokenHandler
public OpenIddictServerJsonWebTokenHandler JsonWebTokenHandler { get; set; } = new OpenIddictServerJsonWebTokenHandler
{
SetDefaultTimesOnTokenCreation = false
};
/// <summary>
/// Gets the token validation parameters used by the OpenIddict server services.
/// </summary>
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
};
/// <summary>
/// Gets or sets the period of time the authorization codes remain valid after being issued.
/// While not recommended, this property can be set to <c>null</c> to issue codes that never expire.

4
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)
{

8
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)

4
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<RequireHttpMetadataAddress>();
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{

41
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
{
/// <summary>
/// Represents a filter that excludes the associated handlers if the metadata address of the issuer is not available.
/// </summary>
public class RequireHttpMetadataAddress : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Options.MetadataAddress == null)
{
return new ValueTask<bool>(false);
}
return new ValueTask<bool>(
string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase));
}
}
}
}

187
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);
/// <summary>
/// Contains the logic responsible of populating the token validation parameters from the memory cache.
/// </summary>
public class PopulateTokenValidationParametersFromMemoryCache : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IMemoryCache _cache;
public PopulateTokenValidationParametersFromMemoryCache([NotNull] IMemoryCache cache)
=> _cache = cache;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<PopulateTokenValidationParametersFromMemoryCache>()
.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);
/// <summary>
/// Contains the logic responsible of populating the token validation
/// parameters using OAuth 2.0/OpenID Connect discovery.
/// </summary>
public class PopulateTokenValidationParametersFromProviderConfiguration : IOpenIddictValidationHandler<ProcessAuthenticationContext>
public class PopulateTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
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;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<PopulateTokenValidationParametersFromProviderConfiguration>()
.SetOrder(ValidateTokenValidationParameters.Descriptor.Order - 1_000)
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<PopulateTokenValidationParameters>()
.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<TokenValidationParameters> 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<SecurityKey> GetSigningKeysAsync(Uri address)
async IAsyncEnumerable<SecurityKey> 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<OpenIddictResponse> SendHttpRequestMessageAsync(Uri address)
static async ValueTask<OpenIddictResponse> 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
}
}
}
/// <summary>
/// Contains the logic responsible of caching the token validation parameters.
/// </summary>
public class CacheTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IMemoryCache _cache;
public CacheTokenValidationParameters([NotNull] IMemoryCache cache)
=> _cache = cache;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<CacheTokenValidationParameters>()
.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;
}
}
}
}

12
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -605,18 +605,18 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Sets the static token validation parameters.
/// Updates the token validation parameters using the specified delegate.
/// </summary>
/// <param name="parameters">The issuer address.</param>
/// <param name="configuration">The configuration delegate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] TokenValidationParameters parameters)
public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] Action<TokenValidationParameters> 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));
}
/// <summary>

17
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);

7
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -240,13 +240,12 @@ namespace OpenIddict.Validation
/// </summary>
public ProcessAuthenticationContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
=> TokenValidationParameters = transaction.Options.TokenValidationParameters.Clone();
/// <summary>
/// Gets or sets the token validation parameters used for the current request.
/// Gets the token validation parameters used for the current request.
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; set; }
public TokenValidationParameters TokenValidationParameters { get; }
/// <summary>
/// Gets or sets the security principal.

102
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);
/// <summary>
/// Contains the logic responsible of ensuring the token validation parameters are populated.
/// </summary>
public class ValidateTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateTokenValidationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string, object> { [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;
}
}
/// <summary>
/// Contains the logic responsible of validating the access token resolved from the current request.
/// </summary>
@ -111,7 +52,7 @@ namespace OpenIddict.Validation
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateAccessTokenParameter>()
.SetOrder(ValidateTokenValidationParameters.Descriptor.Order + 1_000)
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
@ -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<string, object> { [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<string, object> { [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<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000)
.SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000)
.Build();
/// <summary>

2
src/OpenIddict.Validation/OpenIddictValidationTokenHandler.cs → 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<TokenValidationResult> ValidateTokenStringAsync(string token, TokenValidationParameters parameters)
{

17
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<EncryptingCredentials> EncryptionCredentials { get; } = new List<EncryptingCredentials>();
/// <summary>
/// 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.
/// </summary>
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<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the token validation parameters used by the OpenIddict validation services.
/// Gets the token validation parameters used by the OpenIddict validation services.
/// </summary>
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
};
}
}

Loading…
Cancel
Save