From f81f8fc7ddd3b127d9e76d822d6569fd6cdaf8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 4 Feb 2020 13:24:09 +0100 Subject: [PATCH] Introduce introspection support and revamp the events model used by the validation handler --- eng/Versions.props | 3 +- samples/Mvc.Client/Startup.cs | 5 +- samples/Mvc.Client/web.config | 14 - .../Controllers/AuthorizationController.cs | 11 +- .../Controllers/ResourceController.cs | 9 +- .../Controllers/UserinfoController.cs | 14 +- samples/Mvc.Server/Startup.cs | 10 +- samples/Mvc.Server/web.config | 14 - .../OpenIddictExceptions.cs | 66 +++ .../Primitives/OpenIddictMessage.cs | 4 +- .../Primitives/OpenIddictParameter.cs | 162 +++--- .../OpenIddictEntityFrameworkCoreHelpers.cs | 2 +- .../OpenIddictServerAspNetCoreHandlers.cs | 15 +- .../OpenIddictServerDataProtectionBuilder.cs | 3 +- .../OpenIddictServerDataProtectionOptions.cs | 5 +- .../OpenIddictServerOwinHandlers.cs | 15 +- .../OpenIddictServerBuilder.cs | 26 +- .../OpenIddictServerEvents.cs | 3 + .../OpenIddictServerHandlers.cs | 19 +- .../OpenIddictServerOptions.cs | 3 +- .../OpenIddict.Validation.Owin.csproj | 1 - .../OpenIddictValidationOwinExtensions.cs | 2 - ...alidationServerIntegrationConfiguration.cs | 42 +- ...OpenIddict.Validation.SystemNetHttp.csproj | 3 +- ...ictValidationSystemNetHttpConfiguration.cs | 19 +- ...nIddictValidationSystemNetHttpConstants.cs | 16 - ...IddictValidationSystemNetHttpExtensions.cs | 1 - ...ctValidationSystemNetHttpHandlerFilters.cs | 9 +- ...lidationSystemNetHttpHandlers.Discovery.cs | 40 ++ ...tionSystemNetHttpHandlers.Introspection.cs | 29 + ...enIddictValidationSystemNetHttpHandlers.cs | 295 +++++----- ...penIddictValidationSystemNetHttpHelpers.cs | 33 ++ .../OpenIddict.Validation.csproj | 8 +- .../OpenIddictValidationBuilder.cs | 98 +++- .../OpenIddictValidationConfiguration.cs | 134 +++-- .../OpenIddictValidationEvents.Discovery.cs | 143 +++++ ...penIddictValidationEvents.Introspection.cs | 98 ++++ .../OpenIddictValidationEvents.cs | 39 +- .../OpenIddictValidationExtensions.cs | 7 +- .../OpenIddictValidationHandlerFilters.cs | 42 +- .../OpenIddictValidationHandlers.Discovery.cs | 313 +++++++++++ ...nIddictValidationHandlers.Introspection.cs | 504 ++++++++++++++++++ .../OpenIddictValidationHandlers.cs | 193 ++++++- .../OpenIddictValidationOptions.cs | 36 +- .../OpenIddictValidationRetriever.cs | 68 +++ .../OpenIddictValidationService.cs | 493 +++++++++++++++++ .../OpenIddictValidationType.cs | 29 + .../Primitives/OpenIddictParameterTests.cs | 208 +++++--- .../OpenIddictServerIntegrationTests.cs | 2 +- 49 files changed, 2754 insertions(+), 554 deletions(-) delete mode 100644 samples/Mvc.Client/web.config delete mode 100644 samples/Mvc.Server/web.config delete mode 100644 src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs create mode 100644 src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs create mode 100644 src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs create mode 100644 src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHelpers.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationRetriever.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationService.cs create mode 100644 src/OpenIddict.Validation/OpenIddictValidationType.cs diff --git a/eng/Versions.props b/eng/Versions.props index 693acd6a..2c2dc38d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -2,7 +2,7 @@ 3.0.0 - alpha1 + beta1 @@ -42,6 +42,7 @@ 2.9.0 4.13.1 4.1.0 + 3.2.0 4.7.1 4.5.4 diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index c82952b9..32234d0e 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -62,7 +62,7 @@ namespace Mvc.Client services.AddHttpClient(); - services.AddMvc(); + services.AddControllersWithViews(); } public void Configure(IApplicationBuilder app) @@ -71,10 +71,9 @@ namespace Mvc.Client app.UseStaticFiles(); - app.UseAuthentication(); - app.UseRouting(); + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(options => options.MapControllerRoute( diff --git a/samples/Mvc.Client/web.config b/samples/Mvc.Client/web.config deleted file mode 100644 index 5b2b1cbc..00000000 --- a/samples/Mvc.Client/web.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 88176d36..28c57fa5 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -214,12 +214,11 @@ namespace Mvc.Server })); // In every other case, render the consent form. - default: - return View(new AuthorizeViewModel - { - ApplicationName = await _applicationManager.GetDisplayNameAsync(application), - Scope = request.Scope - }); + default: return View(new AuthorizeViewModel + { + ApplicationName = await _applicationManager.GetDisplayNameAsync(application), + Scope = request.Scope + }); } } diff --git a/samples/Mvc.Server/Controllers/ResourceController.cs b/samples/Mvc.Server/Controllers/ResourceController.cs index 2d83b2ae..b0733c76 100644 --- a/samples/Mvc.Server/Controllers/ResourceController.cs +++ b/samples/Mvc.Server/Controllers/ResourceController.cs @@ -42,7 +42,14 @@ namespace Mvc.Server.Controllers var user = await _userManager.GetUserAsync(User); if (user == null) { - return Challenge(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + return Challenge( + authenticationSchemes: OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictValidationAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, + [OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = + "The specified access token is bound to an account that no longer exists." + })); } return Content($"{user.UserName} has been successfully authenticated."); diff --git a/samples/Mvc.Server/Controllers/UserinfoController.cs b/samples/Mvc.Server/Controllers/UserinfoController.cs index 0748ceb7..8675eb80 100644 --- a/samples/Mvc.Server/Controllers/UserinfoController.cs +++ b/samples/Mvc.Server/Controllers/UserinfoController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -27,7 +28,14 @@ namespace Mvc.Server.Controllers var user = await _userManager.GetUserAsync(User); if (user == null) { - return Challenge(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + return Challenge( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The specified access token is bound to an account that no longer exists." + })); } var claims = new Dictionary(StringComparer.Ordinal) @@ -48,9 +56,9 @@ namespace Mvc.Server.Controllers claims[Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user); } - if (User.HasScope("roles")) + if (User.HasScope(Scopes.Roles)) { - claims["roles"] = await _userManager.GetRolesAsync(user); + claims[Claims.Role] = await _userManager.GetRolesAsync(user); } // Note: the complete list of standard claims supported by the OpenID Connect specification diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index b6a51c29..182334fd 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -18,7 +18,7 @@ namespace Mvc.Server public void ConfigureServices(IServiceCollection services) { - services.AddMvc(); + services.AddControllersWithViews(); services.AddDbContext(options => { @@ -59,7 +59,7 @@ namespace Mvc.Server // Register the OpenIddict server components. .AddServer(options => { - // Enable the authorization, logout, token and userinfo endpoints. + // Enable the authorization, device, logout, token, userinfo and verification endpoints. options.SetAuthorizationEndpointUris("/connect/authorize") .SetDeviceEndpointUris("/connect/device") .SetLogoutEndpointUris("/connect/logout") @@ -133,9 +133,8 @@ namespace Mvc.Server // associated authorizations can be validated for each API call. // Enabling these options may have a negative impact on performance. // - // options.EnableAuthorizationValidation(); - // - // options.EnableTokenValidation(); + // options.EnableAuthorizationEntryValidation(); + // options.EnableTokenEntryValidation(); }); services.AddTransient(); @@ -157,7 +156,6 @@ namespace Mvc.Server app.UseRouting(); app.UseAuthentication(); - app.UseAuthorization(); app.UseEndpoints(options => options.MapControllerRoute( diff --git a/samples/Mvc.Server/web.config b/samples/Mvc.Server/web.config deleted file mode 100644 index 5b2b1cbc..00000000 --- a/samples/Mvc.Server/web.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/OpenIddict.Abstractions/OpenIddictExceptions.cs b/src/OpenIddict.Abstractions/OpenIddictExceptions.cs index b9d65650..23cfca58 100644 --- a/src/OpenIddict.Abstractions/OpenIddictExceptions.cs +++ b/src/OpenIddict.Abstractions/OpenIddictExceptions.cs @@ -34,6 +34,72 @@ namespace OpenIddict.Abstractions } } + /// + /// Represents a generic OpenIddict exception. + /// + public class GenericException : Exception + { + /// + /// Creates a new . + /// + /// The exception message. + public GenericException(string message) + : this(message, null) + { + } + + /// + /// Creates a new . + /// + /// The exception message. + /// The error type. + public GenericException(string message, string error) + : this(message, error, description: null) + { + } + + /// + /// Creates a new . + /// + /// The exception message. + /// The error type. + /// The error description. + public GenericException(string message, string error, string description) + : this(message, error, description, uri: null) + { + } + + /// + /// Creates a new . + /// + /// The exception message. + /// The error type. + /// The error description. + /// The error URI. + public GenericException(string message, string error, string description, string uri) + : base(message) + { + Error = error; + ErrorDescription = description; + ErrorUri = uri; + } + + /// + /// Gets the error type. + /// + public string Error { get; } + + /// + /// Gets the error description. + /// + public string ErrorDescription { get; } + + /// + /// Gets the error URI. + /// + public string ErrorUri { get; } + } + /// /// Represents an OpenIddict validation exception. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs index 0162596e..98b2749f 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; @@ -228,7 +229,8 @@ namespace OpenIddict.Abstractions /// Gets all the parameters associated with this instance. /// /// The parameters associated with this instance. - public IReadOnlyDictionary GetParameters() => Parameters; + public IReadOnlyDictionary GetParameters() + => new ReadOnlyDictionary(Parameters); /// /// Determines whether the current message contains the specified parameter. diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs index 23c30ac6..dcf49dd0 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs @@ -22,43 +22,43 @@ namespace OpenIddict.Abstractions public readonly struct OpenIddictParameter : IEquatable { /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(bool value) => Value = value; /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(bool? value) => Value = value; /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(JsonElement value) => Value = value; /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(long value) => Value = value; /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(long? value) => Value = value; /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(string value) => Value = value; /// - /// Initializes a new OpenID Connect parameter using the specified value. + /// Initializes a new parameter using the specified value. /// /// The parameter value. public OpenIddictParameter(string[] value) => Value = value; @@ -68,14 +68,14 @@ namespace OpenIddict.Abstractions /// /// The index of the child item. /// An instance containing the item value. - public OpenIddictParameter? this[int index] => GetParameter(index); + public OpenIddictParameter? this[int index] => GetUnnamedParameter(index); /// /// Gets the child item corresponding to the specified name. /// /// The name of the child item. /// An instance containing the item value. - public OpenIddictParameter? this[string name] => GetParameter(name); + public OpenIddictParameter? this[string name] => GetNamedParameter(name); /// /// Gets the associated value, that can be either a primitive CLR type @@ -92,53 +92,45 @@ namespace OpenIddict.Abstractions /// true if the two instances are equal, false otherwise. public bool Equals(OpenIddictParameter parameter) { - return Value switch + return (left: Value, right: parameter.Value) switch { // If the two parameters reference the same instance, return true. // Note: true will also be returned if the two parameters are null. - var value when ReferenceEquals(value, parameter.Value) => true, + var (left, right) when ReferenceEquals(left, right) => true, // If one of the two parameters is null, return false. - null => false, - var _ when parameter.Value == null => false, + (null, _) => false, + (_, null) => false, // If the two parameters are string arrays, use SequenceEqual(). - string[] value when parameter.Value is string[] array => value.SequenceEqual(array), + (string[] left, string[] right) => left.SequenceEqual(right), // If the two parameters are JsonElement instances, use the custom comparer. - JsonElement value when parameter.Value is JsonElement element => Equals(value, element), + (JsonElement left, JsonElement right) => Equals(left, right), // When one of the parameters is a bool, compare them as booleans. - JsonElement value when value.ValueKind == JsonValueKind.True - && parameter.Value is bool boolean => boolean, - JsonElement value when value.ValueKind == JsonValueKind.False - && parameter.Value is bool boolean => !boolean, + (JsonElement left, bool right) when left.ValueKind == JsonValueKind.True => right, + (JsonElement left, bool right) when left.ValueKind == JsonValueKind.False => !right, - bool value when parameter.Value is JsonElement element - && element.ValueKind == JsonValueKind.True => value, - bool value when parameter.Value is JsonElement element - && element.ValueKind == JsonValueKind.False => !value, + (bool left, JsonElement right) when right.ValueKind == JsonValueKind.True => left, + (bool left, JsonElement right) when right.ValueKind == JsonValueKind.False => !left, // When one of the parameters is a number, compare them as integers. - JsonElement value when value.ValueKind == JsonValueKind.Number - && parameter.Value is long integer - => integer == value.GetInt64(), + (JsonElement left, long right) when left.ValueKind == JsonValueKind.Number + => right == left.GetInt64(), - long value when parameter.Value is JsonElement element - && element.ValueKind == JsonValueKind.Number - => value == element.GetInt64(), + (long left, JsonElement right) when right.ValueKind == JsonValueKind.Number + => left == right.GetInt64(), // When one of the parameters is a string, compare them as texts. - JsonElement value when value.ValueKind == JsonValueKind.String - && parameter.Value is string text - => string.Equals(value.GetString(), text, StringComparison.Ordinal), + (JsonElement left, string right) when left.ValueKind == JsonValueKind.String + => string.Equals(left.GetString(), right, StringComparison.Ordinal), - string value when parameter.Value is JsonElement element - && element.ValueKind == JsonValueKind.String - => string.Equals(value, element.GetString(), StringComparison.Ordinal), + (string left, JsonElement right) when right.ValueKind == JsonValueKind.String + => string.Equals(left, right.GetString(), StringComparison.Ordinal), // Otherwise, use direct CLR comparison. - var value => value.Equals(parameter.Value) + var (left, right) => left.Equals(right) }; static bool Equals(JsonElement left, JsonElement right) @@ -281,12 +273,38 @@ namespace OpenIddict.Abstractions } } + /// + /// Gets the child item corresponding to the specified name. + /// + /// The name of the child item. + /// An instance containing the item value. + public OpenIddictParameter? GetNamedParameter([NotNull] string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The item name cannot be null or empty.", nameof(name)); + } + + if (Value is JsonElement element && element.ValueKind == JsonValueKind.Object) + { + if (element.TryGetProperty(name, out JsonElement value)) + { + return new OpenIddictParameter(value); + } + + // If the item doesn't exist, return a null parameter. + return null; + } + + return null; + } + /// /// Gets the child item corresponding to the specified index. /// /// The index of the child item. /// An instance containing the item value. - public OpenIddictParameter? GetParameter(int index) + public OpenIddictParameter? GetUnnamedParameter(int index) { if (index < 0) { @@ -322,68 +340,51 @@ namespace OpenIddict.Abstractions } /// - /// Gets the child item corresponding to the specified name. + /// Gets the named child items associated with the current parameter, if it represents a JSON object. + /// Note: if the JSON object contains multiple parameters with the same name, only the last occurrence is returned. /// - /// The name of the child item. - /// An instance containing the item value. - public OpenIddictParameter? GetParameter([NotNull] string name) + /// A dictionary of all the parameters associated with the current instance. + public IReadOnlyDictionary GetNamedParameters() { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException("The item name cannot be null or empty.", nameof(name)); - } + var parameters = new Dictionary(); if (Value is JsonElement element && element.ValueKind == JsonValueKind.Object) { - if (element.TryGetProperty(name, out JsonElement value) && value.ValueKind != JsonValueKind.Null) + foreach (var property in element.EnumerateObject()) { - return new OpenIddictParameter(value); + parameters[property.Name] = property.Value; } - - // If the item doesn't exist, return a null parameter. - return null; } - return null; + return parameters; } /// - /// Gets the child items associated with the current parameter. + /// Gets the unnamed child items associated with the current parameter, + /// if it represents an array of strings or a JSON array. /// - /// An enumeration of all the parameters associated with the current instance. - public IEnumerable> GetParameters() + /// An enumeration of all the unnamed parameters associated with the current instance. + public IReadOnlyList GetUnnamedParameters() { + var parameters = new List(); + if (Value is string[] array) { for (var index = 0; index < array.Length; index++) { - yield return new KeyValuePair(null, array[index]); + parameters.Add(array[index]); } } - if (Value is JsonElement element) + else if (Value is JsonElement element && element.ValueKind == JsonValueKind.Array) { - switch (element.ValueKind) + foreach (var value in element.EnumerateArray()) { - case JsonValueKind.Array: - foreach (var value in element.EnumerateArray()) - { - yield return new KeyValuePair(null, value); - } - - break; - - case JsonValueKind.Object: - foreach (var property in element.EnumerateObject()) - { - yield return new KeyValuePair(property.Name, property.Value); - } - - break; + parameters.Add(value); } } - yield break; + return parameters; } /// @@ -399,7 +400,7 @@ namespace OpenIddict.Abstractions JsonElement value => value.ToString(), - _ => Value.ToString() + var value => value.ToString() }; /// @@ -649,8 +650,13 @@ namespace OpenIddict.Abstractions // When the parameter is a JsonElement representing a string, return it as-is. JsonElement value when value.ValueKind == JsonValueKind.String => value.GetString(), - // When the parameter is a JsonElement that doesn't represent a string, return its raw representation. - JsonElement value => value.GetRawText(), + // When the parameter is a JsonElement representing a number, return its representation. + JsonElement value when value.ValueKind == JsonValueKind.Number + => value.GetInt64().ToString(CultureInfo.InvariantCulture), + + // When the parameter is a JsonElement representing a boolean, return its representation. + JsonElement value when value.ValueKind == JsonValueKind.False => bool.FalseString, + JsonElement value when value.ValueKind == JsonValueKind.True => bool.TrueString, // If the parameter is of a different type, return null to indicate the conversion failed. _ => null @@ -773,9 +779,9 @@ namespace OpenIddict.Abstractions public static implicit operator OpenIddictParameter(string[] value) => new OpenIddictParameter(value); /// - /// Determines whether an OpenID Connect parameter is null or empty. + /// Determines whether a parameter is null or empty. /// - /// The OpenID Connect parameter. + /// The parameter. /// true if the parameter is null or empty, false otherwise. public static bool IsNullOrEmpty(OpenIddictParameter parameter) { diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs index bc1bc93e..868d2ad5 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs @@ -127,7 +127,7 @@ namespace Microsoft.EntityFrameworkCore internal static IAsyncEnumerable AsAsyncEnumerable( [NotNull] this IQueryable source, CancellationToken cancellationToken = default) { - if (source is null) + if (source == null) { throw new ArgumentNullException(nameof(source)); } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 4c06a6c3..8fba93d5 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -1384,8 +1384,8 @@ namespace OpenIddict.Server.AspNetCore context.Logger.LogInformation("The authorization response was successfully returned " + "as a plain-text document: {Response}.", context.Response); - using var buffer = new MemoryStream(); - using var writer = new StreamWriter(buffer); + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); foreach (var parameter in context.Response.GetParameters()) { @@ -1397,16 +1397,19 @@ namespace OpenIddict.Server.AspNetCore continue; } - writer.WriteLine("{0}:{1}", parameter.Key, value); + writer.Write(parameter.Key); + writer.Write(':'); + writer.Write(value); + writer.WriteLine(); } writer.Flush(); - response.ContentLength = buffer.Length; + response.ContentLength = stream.Length; response.ContentType = "text/plain;charset=UTF-8"; - buffer.Seek(offset: 0, loc: SeekOrigin.Begin); - await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); + stream.Seek(offset: 0, loc: SeekOrigin.Begin); + await stream.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); context.HandleRequest(); } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs index 3a25db98..a234a7b2 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs @@ -81,8 +81,7 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Configures OpenIddict to use the default token format (JWT) when - /// issuing new access tokens, refresh tokens and authorization codes. + /// Configures OpenIddict to use the default token format (JWT) when issuing new tokens. /// /// The . public OpenIddictServerDataProtectionBuilder PreferDefaultTokenFormat() diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs index 43f581d7..3ed0ab8a 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs @@ -29,9 +29,8 @@ namespace OpenIddict.Server.DataProtection = new OpenIddictServerDataProtectionFormatter(); /// - /// Gets or sets a boolean indicating whether the default token format - /// should be preferred when issuing new access tokens, refresh tokens - /// and authorization codes. This property is set to false by default. + /// Gets or sets a boolean indicating whether the default token format should be + /// used when issuing new tokens. This property is set to false by default. /// public bool PreferDefaultTokenFormat { get; set; } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 7d787c4c..36d6ad68 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -1251,8 +1251,8 @@ namespace OpenIddict.Server.Owin context.Logger.LogInformation("The authorization response was successfully returned " + "as a plain-text document: {Response}.", context.Response); - using var buffer = new MemoryStream(); - using var writer = new StreamWriter(buffer); + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); foreach (var parameter in context.Response.GetParameters()) { @@ -1264,16 +1264,19 @@ namespace OpenIddict.Server.Owin continue; } - writer.WriteLine("{0}:{1}", parameter.Key, value); + writer.Write(parameter.Key); + writer.Write(':'); + writer.Write(value); + writer.WriteLine(); } writer.Flush(); - response.ContentLength = buffer.Length; + response.ContentLength = stream.Length; response.ContentType = "text/plain;charset=UTF-8"; - buffer.Seek(offset: 0, loc: SeekOrigin.Begin); - await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled); + stream.Seek(offset: 0, loc: SeekOrigin.Begin); + await stream.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled); context.HandleRequest(); } diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 1ef78b0a..29981754 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -16,8 +16,8 @@ using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; using OpenIddict.Server; +using static OpenIddict.Abstractions.OpenIddictConstants; namespace Microsoft.Extensions.DependencyInjection { @@ -1042,7 +1042,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public OpenIddictServerBuilder AllowAuthorizationCodeFlow() - => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.AuthorizationCode)); + => Configure(options => options.GrantTypes.Add(GrantTypes.AuthorizationCode)); /// /// Enables client credentials flow support. For more information about this @@ -1050,7 +1050,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public OpenIddictServerBuilder AllowClientCredentialsFlow() - => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.ClientCredentials)); + => Configure(options => options.GrantTypes.Add(GrantTypes.ClientCredentials)); /// /// Enables custom grant type support. @@ -1073,7 +1073,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public OpenIddictServerBuilder AllowDeviceCodeFlow() - => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.DeviceCode)); + => Configure(options => options.GrantTypes.Add(GrantTypes.DeviceCode)); /// /// Enables implicit flow support. For more information @@ -1083,7 +1083,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public OpenIddictServerBuilder AllowImplicitFlow() - => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.Implicit)); + => Configure(options => options.GrantTypes.Add(GrantTypes.Implicit)); /// /// Enables password flow support. For more information about this specific @@ -1091,7 +1091,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public OpenIddictServerBuilder AllowPasswordFlow() - => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.Password)); + => Configure(options => options.GrantTypes.Add(GrantTypes.Password)); /// /// Enables refresh token flow support. For more information about this @@ -1099,7 +1099,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . public OpenIddictServerBuilder AllowRefreshTokenFlow() - => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.RefreshToken)); + => Configure(options => options.GrantTypes.Add(GrantTypes.RefreshToken)); /// /// Sets the relative or absolute URLs associated to the authorization endpoint. @@ -1778,18 +1778,18 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Updates the token validation parameters using the specified delegate. + /// Sets the realm returned to the caller as part of challenge responses. /// - /// The configuration delegate. + /// The issuer address. /// The . - public OpenIddictServerBuilder SetTokenValidationParameters([NotNull] Action configuration) + public OpenIddictServerBuilder SetRealm([NotNull] string realm) { - if (configuration == null) + if (string.IsNullOrEmpty(realm)) { - throw new ArgumentNullException(nameof(configuration)); + throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); } - return Configure(options => configuration(options.TokenValidationParameters)); + return Configure(options => options.Realm = realm); } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 99133dae..6d909183 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -80,6 +80,9 @@ namespace OpenIddict.Server } } + /// + /// Represents an abstract base class used for certain event contexts. + /// [EditorBrowsable(EditorBrowsableState.Never)] public abstract class BaseRequestContext : BaseContext { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 7ee0f0a5..b3411be5 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -357,8 +357,7 @@ namespace OpenIddict.Server } // If the type associated with the token entry doesn't match the expected type, return an error. - if (!string.IsNullOrEmpty(context.TokenType) && - !string.Equals(context.TokenType, await _tokenManager.GetTypeAsync(token))) + if (!string.IsNullOrEmpty(context.TokenType) && !await _tokenManager.HasTypeAsync(token, context.TokenType)) { context.Reject( error: context.EndpointType switch @@ -433,7 +432,7 @@ namespace OpenIddict.Server return default; } - // If the token cannot be validated, don't return an error to allow another handle to validate it. + // If the token cannot be validated, don't return an error to allow another handler to validate it. if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Token)) { return default; @@ -1046,8 +1045,12 @@ namespace OpenIddict.Server context.Reject( error: context.EndpointType switch { + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => Errors.ExpiredToken, + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken + + _ => Errors.InvalidToken }, description: context.EndpointType switch { @@ -1351,7 +1354,7 @@ namespace OpenIddict.Server { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified principal doesn't contain any claims-based identity.") - .Append("Make sure that both 'ClaimsPrincipal.Identity' is not null.") + .Append("Make sure that 'ClaimsPrincipal.Identity' is not null.") .ToString()); } @@ -1437,8 +1440,7 @@ namespace OpenIddict.Server case OpenIddictServerEndpointType.Verification: break; - default: - return default; + default: return default; } var identity = (ClaimsIdentity) context.Principal.Identity; @@ -1586,7 +1588,7 @@ namespace OpenIddict.Server } // Reset the audiences collection, as it's later set, based on the token type. - context.Principal.SetAudiences(Array.Empty()); + context.Principal.SetAudiences(ImmutableArray.Create()); return default; } @@ -2760,7 +2762,6 @@ namespace OpenIddict.Server { Claims.Private.Presenter => false, Claims.Private.Scope => false, - Claims.Private.TokenId => false, _ => true }); diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index a36567d5..7c4aea4f 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -111,6 +111,7 @@ namespace OpenIddict.Server /// public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters { + AuthenticationType = TokenValidationParameters.DefaultAuthenticationType, ClockSkew = TimeSpan.Zero, NameClaimType = OpenIddictConstants.Claims.Name, RoleClaimType = OpenIddictConstants.Claims.Role, @@ -331,7 +332,7 @@ namespace OpenIddict.Server /// /// Gets or sets the optional "realm" value returned to - /// the caller as part of the WWW-Authenticate header. + /// the caller as part of challenge responses. /// public string Realm { get; set; } diff --git a/src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj b/src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj index cafeb9ad..57becc28 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj +++ b/src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj @@ -16,7 +16,6 @@ - diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs index c235a135..3701e495 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs @@ -34,8 +34,6 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddWebEncoders(); - // Note: unlike regular OWIN middleware, the OpenIddict validation middleware is registered // as a scoped service in the DI container. This allows containers that support middleware // resolution (like Autofac) to use it without requiring additional configuration. diff --git a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs index 55477edf..96aced58 100644 --- a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs +++ b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs @@ -5,10 +5,10 @@ */ using System; -using System.Linq; using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using OpenIddict.Server; namespace OpenIddict.Validation.ServerIntegration @@ -40,14 +40,18 @@ namespace OpenIddict.Validation.ServerIntegration throw new ArgumentNullException(nameof(options)); } - // Note: the issuer may be null. In this case, it will be usually be provided by - // a validation handler registered by the host (e.g ASP.NET Core or OWIN/Katana) - options.Issuer = _options.CurrentValue.Issuer; + // Note: the issuer may be null. In this case, it will be usually provided by + // a validation handler registered by the host (e.g ASP.NET Core or OWIN/Katana). + options.Configuration = new OpenIdConnectConfiguration + { + Issuer = _options.CurrentValue.Issuer?.AbsoluteUri + }; - // Import the token validation parameters from the server configuration. - options.TokenValidationParameters.IssuerSigningKeys = - (from credentials in _options.CurrentValue.SigningCredentials - select credentials.Key).ToList(); + // Import the signing keys from the server configuration. + foreach (var credentials in _options.CurrentValue.SigningCredentials) + { + options.Configuration.SigningKeys.Add(credentials.Key); + } // Import the encryption keys from the server configuration. foreach (var credentials in _options.CurrentValue.EncryptionCredentials) @@ -55,8 +59,8 @@ namespace OpenIddict.Validation.ServerIntegration options.EncryptionCredentials.Add(credentials); } - // Note: token validation must be enabled to be able to validate reference tokens. - options.EnableTokenValidation = _options.CurrentValue.UseReferenceTokens; + // Note: token entry validation must be enabled to be able to validate reference tokens. + options.EnableTokenEntryValidation = _options.CurrentValue.UseReferenceTokens; } /// @@ -67,13 +71,23 @@ namespace OpenIddict.Validation.ServerIntegration /// The options instance to initialize. public void PostConfigure([CanBeNull] string name, [NotNull] OpenIddictValidationOptions options) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.ValidationType != OpenIddictValidationType.Direct) + { + throw new InvalidOperationException("The local server integration can only be used with direct validation."); + } + // Note: authorization validation requires that authorizations have an entry // in the database (containing at least the authorization metadata), which is // not created if the authorization storage is disabled in the server options. - if (options.EnableAuthorizationValidation && _options.CurrentValue.DisableAuthorizationStorage) + if (options.EnableAuthorizationEntryValidation && _options.CurrentValue.DisableAuthorizationStorage) { throw new InvalidOperationException(new StringBuilder() - .Append("Authorization validation cannot be enabled when authorization ") + .Append("Authorization entry validation cannot be enabled when authorization ") .Append("storage is disabled in the OpenIddict server options.") .ToString()); } @@ -81,10 +95,10 @@ namespace OpenIddict.Validation.ServerIntegration // Note: token validation requires that tokens have an entry in the database // (containing at least the token metadata), which is not created if the // token storage is disabled in the OpenIddict server options. - if (options.EnableTokenValidation && _options.CurrentValue.DisableTokenStorage) + if (options.EnableTokenEntryValidation && _options.CurrentValue.DisableTokenStorage) { throw new InvalidOperationException(new StringBuilder() - .Append("Token validation cannot be enabled when token storage ") + .Append("Token entry validation cannot be enabled when token storage ") .Append("is disabled in the OpenIddict server options.") .ToString()); } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj b/src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj index 3bd8b16f..6ba6ab30 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj @@ -14,10 +14,9 @@ - - + diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs index 35e4a9c4..99383928 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs @@ -11,7 +11,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; -using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants; namespace OpenIddict.Validation.SystemNetHttp { @@ -22,10 +21,10 @@ namespace OpenIddict.Validation.SystemNetHttp IConfigureNamedOptions { #if !SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER - private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _provider; - public OpenIddictValidationSystemNetHttpConfiguration([NotNull] IServiceProvider serviceProvider) - => _serviceProvider = serviceProvider; + public OpenIddictValidationSystemNetHttpConfiguration([NotNull] IServiceProvider provider) + => _provider = provider; #endif public void Configure([NotNull] OpenIddictValidationOptions options) @@ -52,18 +51,18 @@ namespace OpenIddict.Validation.SystemNetHttp throw new ArgumentNullException(nameof(options)); } - if (!string.Equals(name, Clients.Discovery, StringComparison.Ordinal)) + var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); + + if (!string.Equals(name, assembly.Name, StringComparison.Ordinal)) { return; } options.HttpClientActions.Add(client => { - var name = typeof(OpenIddictValidationSystemNetHttpConfiguration).Assembly.GetName(); - client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue( - productName: name.Name, - productVersion: name.Version.ToString())); + productName: assembly.Name, + productVersion: assembly.Version.ToString())); }); options.HttpMessageHandlerBuilderActions.Add(builder => @@ -71,7 +70,7 @@ namespace OpenIddict.Validation.SystemNetHttp #if SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER var options = builder.Services.GetRequiredService>(); #else - var options = _serviceProvider.GetRequiredService>(); + var options = _provider.GetRequiredService>(); #endif var policy = options.CurrentValue.HttpErrorPolicy; if (policy != null) diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs deleted file mode 100644 index 3dba825b..00000000 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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. - */ - -namespace OpenIddict.Validation.SystemNetHttp -{ - public static class OpenIddictValidationSystemNetHttpConstants - { - public static class Clients - { - public const string Discovery = "OpenIddict.Validation.SystemNetHttp.Discovery"; - } - } -} diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs index 5088151d..344c9941 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs @@ -36,7 +36,6 @@ namespace Microsoft.Extensions.DependencyInjection } builder.Services.AddHttpClient(); - builder.Services.AddMemoryCache(); // Register the built-in validation event handlers used by the OpenIddict System.Net.Http components. // Note: the order used here is not important, as the actual order is set in the options. diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs index 838b9229..50a5d3ee 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs @@ -27,14 +27,9 @@ namespace OpenIddict.Validation.SystemNetHttp 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)); + 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.Discovery.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs new file mode 100644 index 00000000..e3ff9149 --- /dev/null +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs @@ -0,0 +1,40 @@ +/* + * 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.Collections.Immutable; +using static OpenIddict.Validation.OpenIddictValidationEvents; + +namespace OpenIddict.Validation.SystemNetHttp +{ + public static partial class OpenIddictValidationSystemNetHttpHandlers + { + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration request processing: + */ + PrepareGetHttpRequest.Descriptor, + SendHttpRequest.Descriptor, + + /* + * Configuration response processing: + */ + ExtractJsonHttpResponse.Descriptor, + + /* + * Cryptography request processing: + */ + PrepareGetHttpRequest.Descriptor, + SendHttpRequest.Descriptor, + + /* + * Configuration response processing: + */ + ExtractJsonHttpResponse.Descriptor); + } + } +} diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs new file mode 100644 index 00000000..1448227c --- /dev/null +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs @@ -0,0 +1,29 @@ +/* + * 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.Collections.Immutable; +using static OpenIddict.Validation.OpenIddictValidationEvents; + +namespace OpenIddict.Validation.SystemNetHttp +{ + public static partial class OpenIddictValidationSystemNetHttpHandlers + { + public static class Introspection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Introspection request processing: + */ + PreparePostHttpRequest.Descriptor, + SendHttpRequest.Descriptor, + + /* + * Introspection response processing: + */ + ExtractJsonHttpResponse.Descriptor); + } + } +} diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index b6b2e778..a9b59330 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -8,20 +8,14 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; -using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; -using System.Text.Json; +using System.Net.Http.Json; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; -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 @@ -29,177 +23,186 @@ namespace OpenIddict.Validation.SystemNetHttp [EditorBrowsable(EditorBrowsableState.Never)] public static partial class OpenIddictValidationSystemNetHttpHandlers { - public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( - /* - * Authentication processing: - */ - PopulateTokenValidationParameters.Descriptor); + public static ImmutableArray DefaultHandlers { get; } + = ImmutableArray.Create() + .AddRange(Discovery.DefaultHandlers) + .AddRange(Introspection.DefaultHandlers); /// - /// Contains the logic responsible of populating the token validation - /// parameters using OAuth 2.0/OpenID Connect discovery. + /// Contains the logic responsible of preparing an HTTP GET request message. /// - public class PopulateTokenValidationParameters : IOpenIddictValidationHandler + public class PrepareGetHttpRequest : IOpenIddictValidationHandler where TContext : BaseExternalContext { - private readonly IMemoryCache _cache; - private readonly IHttpClientFactory _factory; + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); - public PopulateTokenValidationParameters( - [NotNull] IMemoryCache cache, - [NotNull] IHttpClientFactory factory) + public async ValueTask HandleAsync([NotNull] TContext context) { - _cache = cache; - _factory = factory; + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: System.Net.Http doesn't expose convenient methods allowing to create + // query strings from existing key/value pairs. To work around this limitation, + // a FormUrlEncodedContent is instantiated and used to manually create the URL. + using var content = new FormUrlEncodedContent( + from parameter in context.Request.GetParameters() + let values = (string[]) parameter.Value + where values != null + from value in values + select new KeyValuePair(parameter.Key, value)); + + var builder = new UriBuilder(context.Address) + { + Query = await content.ReadAsStringAsync() + }; + + var request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); + + // Store the HttpRequestMessage in the transaction properties. + context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request; } + } + /// + /// Contains the logic responsible of preparing an HTTP POST request message. + /// + public class PreparePostHttpRequest : IOpenIddictValidationHandler where TContext : BaseExternalContext + { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() + = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500) + .UseSingletonHandler>() + .SetOrder(PrepareGetHttpRequest.Descriptor.Order - 1_000) .Build(); - public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + public ValueTask HandleAsync([NotNull] TContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - 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); + var request = new HttpRequestMessage(HttpMethod.Post, context.Address); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); + + request.Content = new FormUrlEncodedContent( + from parameter in context.Request.GetParameters() + let values = (string[]) parameter.Value + where values != null + from value in values + select new KeyValuePair(parameter.Key, value)); + + // Store the HttpRequestMessage in the transaction properties. + context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request; - return await GetTokenValidationParametersAsync(); - }); + return default; + } + } + + /// + /// Contains the logic responsible of sending the HTTP request to the remote server. + /// + public class SendHttpRequest : IOpenIddictValidationHandler where TContext : BaseExternalContext + { + private readonly IHttpClientFactory _factory; - context.TokenValidationParameters.ValidIssuer = parameters.ValidIssuer; - context.TokenValidationParameters.IssuerSigningKeys = parameters.IssuerSigningKeys; + public SendHttpRequest([NotNull] IHttpClientFactory factory) + => _factory = factory; - async ValueTask GetTokenValidationParametersAsync() + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + public async ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var request = context.Transaction.GetHttpRequestMessage(); + if (request == null) { - using var client = _factory.CreateClient(Clients.Discovery); - var response = await SendHttpRequestMessageAsync(client, context.Options.MetadataAddress); - - // 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."); - } - - 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() - }; + throw new InvalidOperationException("The System.Net.Http request cannot be resolved."); } - static async IAsyncEnumerable GetSigningKeysAsync(HttpClient client, Uri address) + var assembly = typeof(OpenIddictValidationSystemNetHttpOptions).Assembly.GetName(); + using var client = _factory.CreateClient(assembly.Name); + if (client == null) + { + throw new InvalidOperationException("An unknown error occurred while creating a System.Net.Http client."); + } + + var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead); + if (response == null) + { + throw new InvalidOperationException("An unknown error occurred while sending a System.Net.Http request."); + } + + // Store the HttpResponseMessage in the transaction properties. + context.Transaction.Properties[typeof(HttpResponseMessage).FullName] = response; + } + } + + /// + /// Contains the logic responsible of extracting the response from the JSON-encoded HTTP body. + /// + public class ExtractJsonHttpResponse : IOpenIddictValidationHandler where TContext : BaseExternalContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + public async ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) { - var response = await SendHttpRequestMessageAsync(client, address); - - var keys = response[JsonWebKeySetParameterNames.Keys]; - if (keys == null) - { - throw new InvalidOperationException("The OAuth 2.0/OpenID Connect cryptography didn't contain any JSON web key"); - } - - foreach (var payload in keys.Value.GetParameters()) - { - var type = (string) payload.Value[JsonWebKeyParameterNames.Kty]; - if (string.IsNullOrEmpty(type)) - { - throw new InvalidOperationException("A JWKS response containing an invalid key was returned."); - } - - var key = type switch - { - JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey - { - Kty = JsonWebAlgorithmsKeyTypes.RSA, - E = (string) payload.Value[JsonWebKeyParameterNames.E], - N = (string) payload.Value[JsonWebKeyParameterNames.N] - }, - - JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey - { - Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve, - Crv = (string) payload.Value[JsonWebKeyParameterNames.Crv], - X = (string) payload.Value[JsonWebKeyParameterNames.X], - Y = (string) payload.Value[JsonWebKeyParameterNames.Y] - }, - - _ => throw new InvalidOperationException("A JWKS response containing an unsupported key was returned.") - }; - - key.KeyId = (string) payload.Value[JsonWebKeyParameterNames.Kid]; - key.X5t = (string) payload.Value[JsonWebKeyParameterNames.X5t]; - key.X5tS256 = (string) payload.Value[JsonWebKeyParameterNames.X5tS256]; - - if (payload.Value.TryGetParameter(JsonWebKeyParameterNames.X5c, out var chain)) - { - foreach (var certificate in chain.GetParameters()) - { - var value = (string) certificate.Value; - if (string.IsNullOrEmpty(value)) - { - throw new InvalidOperationException("A JWKS response containing an invalid key was returned."); - } - - key.X5c.Add(value); - } - } - - yield return key; - } + throw new ArgumentNullException(nameof(context)); } - static async ValueTask SendHttpRequestMessageAsync(HttpClient client, Uri address) + // This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved, + // this may indicate that the request was incorrectly processed by another client stack. + var response = context.Transaction.GetHttpResponseMessage(); + if (response == null) { - using var request = new HttpRequestMessage(HttpMethod.Get, address); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); - - using var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead); - if (!response.IsSuccessStatusCode) - { - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, - "The OAuth 2.0/OpenID Connect discovery failed because an invalid response was received:" + - "the identity provider returned returned a {0} response with the following payload: {1} {2}.", - /* Status: */ response.StatusCode, - /* Headers: */ response.Headers.ToString(), - /* Body: */ await response.Content.ReadAsStringAsync())); - } - - var type = response.Content?.Headers.ContentType?.MediaType; - if (!string.Equals(type, "application/json", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, - "The OAuth 2.0/OpenID Connect discovery failed because an invalid content type was received:" + - "the identity provider returned returned a {0} response with the following payload: {1} {2}.", - /* Status: */ response.StatusCode, - /* Headers: */ response.Headers.ToString(), - /* Body: */ await response.Content.ReadAsStringAsync())); - } - - // Note: ReadAsStreamAsync() is deliberately not used here, as we can't guarantee that - // the validation handler will always be used with OAuth 2.0 servers returning UTF-8 - // responses (which is not required by the OAuth 2.0/OpenID Connect discovery specs). - // Unlike ReadAsStreamAsync(), ReadAsStringAsync() will use the response charset - // to determine whether the payload is UTF-8-encoded and transcode it if necessary. - return JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync()); + throw new InvalidOperationException("The System.Net.Http request cannot be resolved."); } + + // The status code is deliberately not validated to ensure even errored responses + // (typically in the 4xx range) can be deserialized and handled by the event handlers. + + // Note: ReadFromJsonAsync() automatically validates the content type and the content encoding + // and transcode the response stream if a non-UTF-8 response is returned by the remote server. + context.Response = await response.Content.ReadFromJsonAsync(); } } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHelpers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHelpers.cs new file mode 100644 index 00000000..ef7d849a --- /dev/null +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHelpers.cs @@ -0,0 +1,33 @@ +/* + * 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 JetBrains.Annotations; +using OpenIddict.Validation; + +namespace System.Net.Http +{ + /// + /// Exposes companion extensions for the OpenIddict/ASP.NET Core integration. + /// + public static class OpenIddictValidationSystemNetHttpHelpers + { + /// + /// Gets the associated with the current context. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static HttpRequestMessage GetHttpRequestMessage([NotNull] this OpenIddictValidationTransaction transaction) + => transaction.GetProperty(typeof(HttpRequestMessage).FullName); + + /// + /// Gets the associated with the current context. + /// + /// The transaction instance. + /// The instance or null if it couldn't be found. + public static HttpResponseMessage GetHttpResponseMessage([NotNull] this OpenIddictValidationTransaction transaction) + => transaction.GetProperty(typeof(HttpResponseMessage).FullName); + } +} diff --git a/src/OpenIddict.Validation/OpenIddict.Validation.csproj b/src/OpenIddict.Validation/OpenIddict.Validation.csproj index 151fed11..28dcd50c 100644 --- a/src/OpenIddict.Validation/OpenIddict.Validation.csproj +++ b/src/OpenIddict.Validation/OpenIddict.Validation.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1 + net461;net472;netcoreapp2.1;netcoreapp3.1;netstandard2.0;netstandard2.1 @@ -16,7 +16,11 @@ - + + + $(DefineConstants);SUPPORTS_EPHEMERAL_KEY_SETS + + diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index b2b22c2a..91f6806c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -9,10 +9,12 @@ using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using OpenIddict.Validation; @@ -411,8 +413,8 @@ namespace Microsoft.Extensions.DependencyInjection /// can only be used with an OpenIddict-based authorization server. /// /// The . - public OpenIddictValidationBuilder EnableAuthorizationValidation() - => Configure(options => options.EnableAuthorizationValidation = true); + public OpenIddictValidationBuilder EnableAuthorizationEntryValidation() + => Configure(options => options.EnableAuthorizationEntryValidation = true); /// /// Enables token validation so that a database call is made for each API request @@ -421,8 +423,56 @@ namespace Microsoft.Extensions.DependencyInjection /// when the OpenIddict server is configured to use reference tokens. /// /// The . - public OpenIddictValidationBuilder EnableTokenValidation() - => Configure(options => options.EnableTokenValidation = true); + public OpenIddictValidationBuilder EnableTokenEntryValidation() + => Configure(options => options.EnableTokenEntryValidation = true); + + /// + /// Sets a static OpenID Connect server configuration, that will be used to + /// resolve the metadata/introspection endpoints and the issuer signing keys. + /// + /// The server configuration. + /// The . + public OpenIddictValidationBuilder SetConfiguration([NotNull] OpenIdConnectConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + return Configure(options => options.Configuration = configuration); + } + + /// + /// Sets the client identifier client_id used when communicating + /// with the remote authorization server (e.g for introspection). + /// + /// The client identifier. + /// The . + public OpenIddictValidationBuilder SetClientId([NotNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(identifier)); + } + + return Configure(options => options.ClientId = identifier); + } + + /// + /// Sets the client identifier client_secret used when communicating + /// with the remote authorization server (e.g for introspection). + /// + /// The client secret. + /// The . + public OpenIddictValidationBuilder SetClientSecret([NotNull] string secret) + { + if (string.IsNullOrEmpty(secret)) + { + throw new ArgumentException("The client secret cannot be null or empty.", nameof(secret)); + } + + return Configure(options => options.ClientSecret = secret); + } /// /// Sets the issuer address, which is used to determine the actual location of the @@ -441,20 +491,48 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// Updates the token validation parameters using the specified delegate. + /// Sets the issuer address, which is used to determine the actual location of the + /// OAuth 2.0/OpenID Connect configuration document when using provider discovery. /// - /// The configuration delegate. + /// The issuer address. /// The . - public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] Action configuration) + public OpenIddictValidationBuilder SetIssuer([NotNull] string address) { - if (configuration == null) + if (string.IsNullOrEmpty(address)) { - throw new ArgumentNullException(nameof(configuration)); + throw new ArgumentException("The issuer cannot be null or empty.", nameof(address)); + } + + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) + { + throw new ArgumentException("The issuer must be a valid absolute URL.", nameof(address)); } - return Configure(options => configuration(options.TokenValidationParameters)); + return SetIssuer(uri); } + /// + /// Sets the realm returned to the caller as part of challenge responses. + /// + /// The issuer address. + /// The . + public OpenIddictValidationBuilder SetRealm([NotNull] string realm) + { + if (string.IsNullOrEmpty(realm)) + { + throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); + } + + return Configure(options => options.Realm = realm); + } + + /// + /// Configures OpenIddict to use introspection instead of local/direct validation. + /// + /// The . + public OpenIddictValidationBuilder UseIntrospection() + => Configure(options => options.ValidationType = OpenIddictValidationType.Introspection); + /// /// Determines whether the specified object is equal to the current object. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 3c400d77..4ce5f15c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -5,11 +5,13 @@ */ using System; -using System.Diagnostics; using System.Linq; +using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using static OpenIddict.Validation.OpenIddictValidationEvents; namespace OpenIddict.Validation { @@ -18,6 +20,11 @@ namespace OpenIddict.Validation /// public class OpenIddictValidationConfiguration : IPostConfigureOptions { + private readonly OpenIddictValidationService _service; + + public OpenIddictValidationConfiguration([NotNull] OpenIddictValidationService service) + => _service = service; + /// /// Populates the default OpenIddict validation options and ensures /// that the configuration is in a consistent and valid state. @@ -36,8 +43,72 @@ namespace OpenIddict.Validation throw new InvalidOperationException("The security token handler cannot be null."); } - if (options.Issuer != null || options.MetadataAddress != null) + if (options.Configuration == null && options.ConfigurationManager == null && + options.Issuer == null && options.MetadataAddress == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("An OAuth 2.0/OpenID Connect server configuration or an issuer address must be registered.") + .Append("To use a local OpenIddict server, reference the 'OpenIddict.Validation.ServerIntegration' package ") + .AppendLine("and call 'services.AddOpenIddict().AddValidation().UseLocalServer()' to import the server settings.") + .Append("To use a remote server, reference the 'OpenIddict.Validation.SystemNetHttp' package and call ") + .Append("'services.AddOpenIddict().AddValidation().UseSystemNetHttp()' ") + .AppendLine("and 'services.AddOpenIddict().AddValidation().SetIssuer()' to use server discovery.") + .Append("Alternatively, you can register a static server configuration by calling ") + .Append("'services.AddOpenIddict().AddValidation().SetConfiguration()'.") + .ToString()); + } + + if (options.ValidationType == OpenIddictValidationType.Introspection) + { + if (!options.DefaultHandlers.Any(descriptor => descriptor.ContextType == typeof(ApplyIntrospectionRequestContext))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("An introspection client must be registered when using introspection.") + .Append("Reference the 'OpenIddict.Validation.SystemNetHttp' package and call ") + .Append("'services.AddOpenIddict().AddValidation().UseSystemNetHttp()' ") + .Append("to register the default System.Net.Http-based integration.") + .ToString()); + } + + if (options.Issuer == null && options.MetadataAddress == null) + { + throw new InvalidOperationException("The issuer or the metadata address must be set when using introspection."); + } + + if (string.IsNullOrEmpty(options.ClientId)) + { + throw new InvalidOperationException("The client identifier cannot be null or empty when using introspection."); + } + + if (string.IsNullOrEmpty(options.ClientSecret)) + { + throw new InvalidOperationException("The client secret cannot be null or empty when using introspection."); + } + + if (options.EnableAuthorizationEntryValidation) + { + throw new InvalidOperationException("Authorization validation cannot be enabled when using introspection."); + } + + if (options.EnableTokenEntryValidation) + { + throw new InvalidOperationException("Token validation cannot be enabled when using introspection."); + } + } + + if (options.Configuration == null && options.ConfigurationManager == null) { + if (!options.DefaultHandlers.Any(descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) || + !options.DefaultHandlers.Any(descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("A discovery client must be registered when using server discovery.") + .Append("Reference the 'OpenIddict.Validation.SystemNetHttp' package and call ") + .Append("'services.AddOpenIddict().AddValidation().UseSystemNetHttp()' ") + .Append("to register the default System.Net.Http-based integration.") + .ToString()); + } + if (options.MetadataAddress == null) { options.MetadataAddress = new Uri(".well-known/openid-configuration", UriKind.Relative); @@ -70,61 +141,22 @@ namespace OpenIddict.Validation } } - foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key)) - { - if (!string.IsNullOrEmpty(key.KeyId)) - { - continue; - } - - key.KeyId = GetKeyIdentifier(key); - } - - static string GetKeyIdentifier(SecurityKey key) + if (options.ConfigurationManager == null) { - // When no key identifier can be retrieved from the security keys, a value is automatically - // inferred from the hexadecimal representation of the certificate thumbprint (SHA-1) - // when the key is bound to a X.509 certificate or from the public part of the signing key. - - if (key is X509SecurityKey x509SecurityKey) + if (options.Configuration != null) { - return x509SecurityKey.Certificate.Thumbprint; + options.ConfigurationManager = new StaticConfigurationManager(options.Configuration); } - if (key is RsaSecurityKey rsaSecurityKey) + else { - // Note: if the RSA parameters are not attached to the signing key, - // extract them by calling ExportParameters on the RSA instance. - var parameters = rsaSecurityKey.Parameters; - if (parameters.Modulus == null) + options.ConfigurationManager = new ConfigurationManager( + options.MetadataAddress.AbsoluteUri, new OpenIddictValidationRetriever(_service)) { - parameters = rsaSecurityKey.Rsa.ExportParameters(includePrivateParameters: false); - - Debug.Assert(parameters.Modulus != null, - "A null modulus shouldn't be returned by RSA.ExportParameters()."); - } - - // Only use the 40 first chars of the base64url-encoded modulus. - var identifier = Base64UrlEncoder.Encode(parameters.Modulus); - return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant(); + AutomaticRefreshInterval = ConfigurationManager.DefaultAutomaticRefreshInterval, + RefreshInterval = ConfigurationManager.DefaultRefreshInterval + }; } - -#if SUPPORTS_ECDSA - if (key is ECDsaSecurityKey ecsdaSecurityKey) - { - // Extract the ECDSA parameters from the signing credentials. - var parameters = ecsdaSecurityKey.ECDsa.ExportParameters(includePrivateParameters: false); - - Debug.Assert(parameters.Q.X != null, - "Invalid coordinates shouldn't be returned by ECDsa.ExportParameters()."); - - // Only use the 40 first chars of the base64url-encoded X coordinate. - var identifier = Base64UrlEncoder.Encode(parameters.Q.X); - return identifier.Substring(0, Math.Min(identifier.Length, 40)).ToUpperInvariant(); - } -#endif - - return null; } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs new file mode 100644 index 00000000..d97eedd1 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs @@ -0,0 +1,143 @@ +/* + * 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 JetBrains.Annotations; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict.Validation +{ + public static partial class OpenIddictValidationEvents + { + /// + /// Represents an event called for each request to the configuration endpoint + /// to give the user code a chance to add parameters to the configuration request. + /// + public class PrepareConfigurationRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareConfigurationRequestContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the configuration endpoint + /// to send the configuration request to the remote authorization server. + /// + public class ApplyConfigurationRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyConfigurationRequestContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each configuration response + /// to extract the response parameters from the server response. + /// + public class ExtractConfigurationResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractConfigurationResponseContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated configuration response. + /// + public class HandleConfigurationResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public HandleConfigurationResponseContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the OpenID Connect configuration. + /// + public OpenIdConnectConfiguration Configuration { get; } = new OpenIdConnectConfiguration(); + } + + /// + /// Represents an event called for each request to the cryptography endpoint + /// to give the user code a chance to add parameters to the cryptography request. + /// + public class PrepareCryptographyRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareCryptographyRequestContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the cryptography endpoint + /// to send the cryptography request to the remote authorization server. + /// + public class ApplyCryptographyRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyCryptographyRequestContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each cryptography response + /// to extract the response parameters from the server response. + /// + public class ExtractCryptographyResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractCryptographyResponseContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated cryptography response. + /// + public class HandleCryptographyResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public HandleCryptographyResponseContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the security keys. + /// + public JsonWebKeySet SecurityKeys { get; } = new JsonWebKeySet(); + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs new file mode 100644 index 00000000..eace68cd --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs @@ -0,0 +1,98 @@ +/* + * 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.Security.Claims; +using JetBrains.Annotations; + +namespace OpenIddict.Validation +{ + public static partial class OpenIddictValidationEvents + { + /// + /// Represents an event called for each request to the introspection endpoint + /// to give the user code a chance to add parameters to the introspection request. + /// + public class PrepareIntrospectionRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public PrepareIntrospectionRequestContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the token sent to the introspection endpoint. + /// + public string Token { get; set; } + + /// + /// Gets or sets the token type sent to the introspection endpoint. + /// + public string TokenType { get; set; } + } + + /// + /// Represents an event called for each request to the introspection endpoint + /// to send the introspection request to the remote authorization server. + /// + public class ApplyIntrospectionRequestContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyIntrospectionRequestContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each introspection response + /// to extract the response parameters from the server response. + /// + public class ExtractIntrospectionResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractIntrospectionResponseContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated introspection response. + /// + public class HandleIntrospectionResponseContext : BaseExternalContext + { + /// + /// Creates a new instance of the class. + /// + public HandleIntrospectionResponseContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the token sent to the introspection endpoint. + /// + public string Token { get; set; } + + /// + /// Gets or sets the token type sent to the introspection endpoint. + /// + public string TokenType { get; set; } + + /// + /// Gets or sets the principal containing the claims resolved from the introspection response. + /// + public ClaimsPrincipal Principal { get; set; } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index 0d5dab00..e5b21c44 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -5,12 +5,10 @@ */ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Security.Claims; using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; namespace OpenIddict.Validation @@ -62,12 +60,6 @@ namespace OpenIddict.Validation /// public OpenIddictValidationOptions Options => Transaction.Options; - /// - /// Gets the dictionary containing the properties associated with this event. - /// - public IDictionary Properties { get; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); - /// /// Gets or sets the OpenIddict request or null if it couldn't be extracted. /// @@ -87,6 +79,9 @@ namespace OpenIddict.Validation } } + /// + /// Represents an abstract base class used for certain event contexts. + /// [EditorBrowsable(EditorBrowsableState.Never)] public abstract class BaseRequestContext : BaseContext { @@ -123,6 +118,26 @@ namespace OpenIddict.Validation public void SkipRequest() => IsRequestSkipped = true; } + /// + /// Represents an abstract base class used for certain event contexts. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseExternalContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + protected BaseExternalContext([NotNull] OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the address of the external endpoint to communicate with. + /// + public Uri Address { get; set; } + } + /// /// Represents an abstract base class used for certain event contexts. /// @@ -240,12 +255,8 @@ namespace OpenIddict.Validation /// public ProcessAuthenticationContext([NotNull] OpenIddictValidationTransaction transaction) : base(transaction) - => TokenValidationParameters = transaction.Options.TokenValidationParameters.Clone(); - - /// - /// Gets the token validation parameters used for the current request. - /// - public TokenValidationParameters TokenValidationParameters { get; } + { + } /// /// Gets or sets the security principal. diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 6aa924d0..a44c53a4 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -36,6 +36,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddLogging(); builder.Services.AddOptions(); + builder.Services.TryAddSingleton(); builder.Services.TryAddScoped(); // Register the built-in validation event handlers used by the OpenIddict validation components. @@ -43,8 +44,10 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); // Register the built-in filters used by the default OpenIddict validation event handlers. - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index 104d6eca..338c1737 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -18,7 +18,7 @@ namespace OpenIddict.Validation /// /// Represents a filter that excludes the associated handlers if authorization validation was not enabled. /// - public class RequireAuthorizationValidationEnabled : IOpenIddictValidationHandlerFilter + public class RequireAuthorizationEntryValidationEnabled : IOpenIddictValidationHandlerFilter { public ValueTask IsActiveAsync([NotNull] BaseContext context) { @@ -27,14 +27,46 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - return new ValueTask(context.Options.EnableAuthorizationValidation); + return new ValueTask(context.Options.EnableAuthorizationEntryValidation); } } /// - /// Represents a filter that excludes the associated handlers if authorization validation was not enabled. + /// Represents a filter that excludes the associated handlers if local validation is not used. + /// + public class RequireLocalValidation : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.Options.ValidationType == OpenIddictValidationType.Direct); + } + } + + /// + /// Represents a filter that excludes the associated handlers if introspection is not used. + /// + public class RequireIntrospectionValidation : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.Options.ValidationType == OpenIddictValidationType.Introspection); + } + } + + /// + /// Represents a filter that excludes the associated handlers if token validation was not enabled. /// - public class RequireTokenValidationEnabled : IOpenIddictValidationHandlerFilter + public class RequireTokenEntryValidationEnabled : IOpenIddictValidationHandlerFilter { public ValueTask IsActiveAsync([NotNull] BaseContext context) { @@ -43,7 +75,7 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - return new ValueTask(context.Options.EnableTokenValidation); + return new ValueTask(context.Options.EnableTokenEntryValidation); } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs new file mode 100644 index 00000000..c6b51820 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs @@ -0,0 +1,313 @@ +/* + * 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.Collections.Immutable; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.IdentityModel.Tokens; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Validation.OpenIddictValidationEvents; + +namespace OpenIddict.Validation +{ + public static partial class OpenIddictValidationHandlers + { + public static class Discovery + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Configuration response handling: + */ + HandleErrorResponse.Descriptor, + ValidateIssuer.Descriptor, + ExtractCryptographyEndpointUri.Descriptor, + ExtractIntrospectionEndpointUri.Descriptor, + + /* + * Cryptography response handling: + */ + HandleErrorResponse.Descriptor, + ExtractSigningKeys.Descriptor); + + /// + /// Contains the logic responsible of extracting the issuer from the discovery document. + /// + public class ValidateIssuer : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_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] HandleConfigurationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // The issuer returned in the discovery document must exactly match the URL used to access it. + // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation. + var issuer = (string) context.Response[Metadata.Issuer]; + if (string.IsNullOrEmpty(issuer)) + { + context.Reject( + error: Errors.ServerError, + description: "No issuer could be found in the discovery document."); + + return default; + } + + if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri address)) + { + context.Reject( + error: Errors.ServerError, + description: "A discovery response containing an invalid issuer was returned."); + + return default; + } + + if (context.Issuer != null && context.Issuer != address) + { + context.Reject( + error: Errors.ServerError, + description: "The issuer returned by the discovery endpoint is not valid."); + + return default; + } + + context.Configuration.Issuer = issuer; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document. + /// + public class ExtractCryptographyEndpointUri : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIssuer.Descriptor.Order + 1_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] HandleConfigurationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the jwks_uri node is required by the OpenID Connect discovery specification. + // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation. + var address = (string) context.Response[Metadata.JwksUri]; + if (string.IsNullOrEmpty(address)) + { + context.Reject( + error: Errors.ServerError, + description: "No JWKS endpoint could be found in the discovery document."); + + return default; + } + + if (!Uri.IsWellFormedUriString(address, UriKind.Absolute)) + { + context.Reject( + error: Errors.ServerError, + description: "A discovery response containing an invalid JWKS endpoint URL was returned."); + + return default; + } + + context.Configuration.JwksUri = address; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the introspection endpoint address from the discovery document. + /// + public class ExtractIntrospectionEndpointUri : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractCryptographyEndpointUri.Descriptor.Order + 1_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] HandleConfigurationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var address = (string) context.Response[Metadata.IntrospectionEndpoint]; + if (!string.IsNullOrEmpty(address) && !Uri.IsWellFormedUriString(address, UriKind.Absolute)) + { + context.Reject( + error: Errors.ServerError, + description: "A discovery response containing an invalid introspection endpoint URL was returned."); + + return default; + } + + context.Configuration.IntrospectionEndpoint = address; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the signing keys from the JWKS document. + /// + public class ExtractSigningKeys : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_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] HandleCryptographyResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var keys = context.Response[JsonWebKeySetParameterNames.Keys]?.GetUnnamedParameters(); + if (keys == null || keys.Count == 0) + { + context.Reject( + error: Errors.ServerError, + description: "The JWKS document didn't contain a valid 'jwks' node with at least one key."); + + return default; + } + + for (var index = 0; index < keys.Count; index++) + { + // Note: the "use" parameter is defined as optional by the specification. + // To prevent key swapping attacks, OpenIddict requires that this parameter + // be present and will ignore keys that don't include a "use" parameter. + var use = (string) keys[index][JsonWebKeyParameterNames.Use]; + if (string.IsNullOrEmpty(use)) + { + continue; + } + + // Ignore security keys that are not used for signing. + if (!string.Equals(use, JsonWebKeyUseNames.Sig, StringComparison.Ordinal)) + { + continue; + } + + var key = (string) keys[index][JsonWebKeyParameterNames.Kty] switch + { + JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey + { + Kty = JsonWebAlgorithmsKeyTypes.RSA, + E = (string) keys[index][JsonWebKeyParameterNames.E], + N = (string) keys[index][JsonWebKeyParameterNames.N] + }, + + JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey + { + Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve, + Crv = (string) keys[index][JsonWebKeyParameterNames.Crv], + X = (string) keys[index][JsonWebKeyParameterNames.X], + Y = (string) keys[index][JsonWebKeyParameterNames.Y] + }, + + _ => null + }; + + if (key == null) + { + context.Reject( + error: Errors.ServerError, + description: "A JWKS response containing an unsupported key was returned."); + + return default; + } + + key.KeyId = (string) keys[index][JsonWebKeyParameterNames.Kid]; + key.X5t = (string) keys[index][JsonWebKeyParameterNames.X5t]; + key.X5tS256 = (string) keys[index][JsonWebKeyParameterNames.X5tS256]; + + if (keys[index].TryGetParameter(JsonWebKeyParameterNames.X5c, out var chain)) + { + foreach (var certificate in chain.GetNamedParameters()) + { + var value = (string) certificate.Value; + if (string.IsNullOrEmpty(value)) + { + context.Reject( + error: Errors.ServerError, + description: "A JWKS response containing an invalid key was returned."); + + return default; + } + + key.X5c.Add(value); + } + } + + context.SecurityKeys.Keys.Add(key); + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs new file mode 100644 index 00000000..3288aa72 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -0,0 +1,504 @@ +/* + * 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.Collections.Immutable; +using System.Globalization; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.IdentityModel.JsonWebTokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Validation.OpenIddictValidationEvents; + +namespace OpenIddict.Validation +{ + public static partial class OpenIddictValidationHandlers + { + public static class Introspection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Introspection response handling: + */ + AttachCredentials.Descriptor, + AttachAccessToken.Descriptor, + + /* + * Introspection response handling: + */ + HandleErrorResponse.Descriptor, + HandleInactiveResponse.Descriptor, + ValidateWellKnownClaims.Descriptor, + ValidateIssuer.Descriptor, + ValidateTokenType.Descriptor, + PopulateClaims.Descriptor); + + /// + /// Contains the logic responsible of attaching the client credentials to the introspection request. + /// + public class AttachCredentials : 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] PrepareIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Request.ClientId = context.Options.ClientId; + context.Request.ClientSecret = context.Options.ClientSecret; + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the access token to the introspection request. + /// + public class AttachAccessToken : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachCredentials.Descriptor.Order + 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] PrepareIntrospectionRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Request.Token = context.Token; + context.Request.TokenTypeHint = context.TokenType; + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the active: false marker from the response. + /// + public class HandleInactiveResponse : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_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] HandleIntrospectionResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the introspection specification requires that server return "active: false" instead of a proper + // OAuth 2.0 error when the token is invalid, expired, revoked or invalid for any other reason. + // While OpenIddict's server can be tweaked to return a proper error (by removing NormalizeErrorResponse) + // from the enabled handlers, supporting "active: false" is required to ensure total compatibility. + + if (!context.Response.TryGetParameter(Parameters.Active, out var parameter)) + { + context.Reject( + error: Errors.ServerError, + description: "The mandatory 'active' parameter couldn't be found in the introspection response."); + + return default; + } + + // Note: if the parameter cannot be converted to a boolean instance, the default value + // (false) is returned by the static operator, which is appropriate for this check. + if (!(bool) parameter) + { + context.Reject( + error: Errors.InvalidToken, + description: "The token was rejected by the remote authorization server."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of validating the well-known claims contained in the introspection response. + /// + public class ValidateWellKnownClaims : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleInactiveResponse.Descriptor.Order + 1_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] HandleIntrospectionResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + foreach (var parameter in context.Response.GetParameters()) + { + if (ValidateClaimType(parameter.Key, parameter.Value)) + { + continue; + } + + context.Reject( + error: Errors.ServerError, + description: $"The {parameter.Key} claim is malformed or isn't of the expected type."); + + return default; + } + + return default; + + static bool ValidateClaimType(string name, OpenIddictParameter value) + { + switch ((name, value.Value)) + { + // The 'aud' claim CAN be represented either as a unique string or as an array of multiple strings. + case (Claims.Audience, string _): + case (Claims.Audience, string[] _): + case (Claims.Audience, JsonElement element) when element.ValueKind == JsonValueKind.String || + (element.ValueKind == JsonValueKind.Array && ValidateArrayChildren(element, JsonValueKind.String)): + return true; + + // The 'exp', 'iat' and 'nbf' claims MUST be formatted as numeric date values. + case (Claims.ExpiresAt, long _): + case (Claims.ExpiresAt, JsonElement element) when element.ValueKind == JsonValueKind.Number: + return true; + + case (Claims.IssuedAt, long _): + case (Claims.IssuedAt, JsonElement element) when element.ValueKind == JsonValueKind.Number: + return true; + + case (Claims.NotBefore, long _): + case (Claims.NotBefore, JsonElement element) when element.ValueKind == JsonValueKind.Number: + return true; + + // The 'jti' claim MUST be formatted as a unique string. + case (Claims.JwtId, string _): + case (Claims.JwtId, JsonElement element) when element.ValueKind == JsonValueKind.String: + return true; + + // The 'iss' claim MUST be formatted as a unique string. + case (Claims.Issuer, string _): + case (Claims.Issuer, JsonElement element) when element.ValueKind == JsonValueKind.String: + return true; + + // The 'scope' claim MUST be formatted as a unique string. + case (Claims.Scope, string _): + case (Claims.Scope, JsonElement element) when element.ValueKind == JsonValueKind.String: + return true; + + // The 'token_usage' claim MUST be formatted as a unique string. + case (Claims.TokenUsage, string _): + case (Claims.TokenUsage, JsonElement element) when element.ValueKind == JsonValueKind.String: + return true; + + // If the previously listed claims are represented differently, + // return false to indicate the claims validation logic failed. + case (Claims.Audience, _): + case (Claims.ExpiresAt, _): + case (Claims.IssuedAt, _): + case (Claims.Issuer, _): + case (Claims.NotBefore, _): + case (Claims.JwtId, _): + case (Claims.Scope, _): + case (Claims.TokenUsage, _): + return false; + + // Claims that are not in the well-known list can be of any type. + default: return true; + } + } + + static bool ValidateArrayChildren(JsonElement element, JsonValueKind kind) + { + foreach (var child in element.EnumerateArray()) + { + if (child.ValueKind != kind) + { + return false; + } + } + + return true; + } + } + } + + /// + /// Contains the logic responsible of extracting the issuer from the introspection response. + /// + public class ValidateIssuer : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_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] HandleIntrospectionResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // The issuer claim is optional. If it's not null or empty, validate it to + // ensure it matches the issuer registered in the server configuration. + var issuer = (string) context.Response[Claims.Issuer]; + if (!string.IsNullOrEmpty(issuer)) + { + if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri uri)) + { + context.Reject( + error: Errors.ServerError, + description: "An introspection response containing an invalid issuer was returned."); + + return default; + } + + if (context.Issuer != null && context.Issuer != uri) + { + context.Reject( + error: Errors.ServerError, + description: "The issuer returned in the introspection response is not valid."); + + return default; + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting and validating the token type from the introspection response. + /// + public class ValidateTokenType : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIssuer.Descriptor.Order + 1_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] HandleIntrospectionResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // OpenIddict-based authorization servers always return the actual token type using + // the special "token_usage" claim, that helps resource servers determine whether the + // introspected token is an access token and thus prevent token substitution attacks. + var usage = (string) context.Response[Claims.TokenUsage]; + if (!string.IsNullOrEmpty(usage) && !string.Equals(usage, context.TokenType, StringComparison.OrdinalIgnoreCase)) + { + context.Reject( + error: Errors.InvalidToken, + description: "The introspected token is not an access token."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of extracting the claims from the introspection response. + /// + public class PopulateClaims : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateTokenType.Descriptor.Order + 1_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] HandleIntrospectionResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var identity = new ClaimsIdentity(context.Options.TokenValidationParameters.AuthenticationType); + + foreach (var parameter in context.Response.GetParameters()) + { + // Always exclude null keys and values, as they can't be represented as valid claims. + if (string.IsNullOrEmpty(parameter.Key) || OpenIddictParameter.IsNullOrEmpty(parameter.Value)) + { + continue; + } + + // Exclude OpenIddict-specific private claims, that MUST NOT be set based on data returned + // by the remote authorization server (that may or may not be an OpenIddict server). + if (parameter.Key.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + switch ((name: parameter.Key, value: parameter.Value.Value)) + { + // Ignore all protocol claims that are not mapped to CLR claims. + case (Claims.Active, _): + case (Claims.Issuer, _): + case (Claims.NotBefore, _): + case (Claims.TokenType, _): + case (Claims.TokenUsage, _): + continue; + + // Claims represented as arrays are split and mapped to multiple CLR claims. + case (var name, JsonElement value) when value.ValueKind == JsonValueKind.Array: + foreach (var element in value.EnumerateArray()) + { + identity.AddClaim(new Claim(name, element.ToString(), GetClaimValueType(value.ValueKind))); + } + break; + + case (var name, JsonElement value): + identity.AddClaim(new Claim(name, value.ToString(), GetClaimValueType(value.ValueKind))); + break; + + // Note: in the typical case, the introspection parameters should be deserialized from + // a JSON response and thus represented as System.Text.Json.JsonElement instances. + // However, to support responses resolved from custom locations and parameters manually added + // by the application using the events model, the CLR primitive types are also supported. + + case (var name, bool value): + identity.AddClaim(new Claim(name, value.ToString(), ClaimValueTypes.Boolean)); + break; + + case (var name, long value): + identity.AddClaim(new Claim(name, value.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); + break; + + case (var name, string value): + identity.AddClaim(new Claim(name, value, ClaimValueTypes.String)); + break; + + // Claims represented as arrays are split and mapped to multiple CLR claims. + case (var name, string[] value): + for (var index = 0; index < value.Length; index++) + { + identity.AddClaim(new Claim(name, value[index], ClaimValueTypes.String)); + } + break; + } + } + + context.Principal = new ClaimsPrincipal(identity); + + return default; + + static string GetClaimValueType(JsonValueKind kind) => kind switch + { + JsonValueKind.True => ClaimValueTypes.Boolean, + JsonValueKind.False => ClaimValueTypes.Boolean, + JsonValueKind.String => ClaimValueTypes.String, + JsonValueKind.Number => ClaimValueTypes.Integer64, + + JsonValueKind.Array => JsonClaimValueTypes.JsonArray, + JsonValueKind.Object => JsonClaimValueTypes.Json, + + _ => JsonClaimValueTypes.Json + }; + } + } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 960dfee6..11c91518 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -13,6 +13,7 @@ using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; @@ -31,6 +32,7 @@ namespace OpenIddict.Validation ValidateAccessTokenParameter.Descriptor, ValidateReferenceTokenIdentifier.Descriptor, ValidateIdentityModelToken.Descriptor, + IntrospectToken.Descriptor, MapInternalClaims.Descriptor, RestoreReferenceTokenProperties.Descriptor, ValidatePrincipal.Descriptor, @@ -42,7 +44,10 @@ namespace OpenIddict.Validation /* * Challenge processing: */ - AttachDefaultChallengeError.Descriptor); + AttachDefaultChallengeError.Descriptor) + + .AddRange(Discovery.DefaultHandlers) + .AddRange(Introspection.DefaultHandlers); /// /// Contains the logic responsible of validating the access token resolved from the current request. @@ -98,7 +103,7 @@ namespace OpenIddict.Validation private readonly IOpenIddictTokenManager _tokenManager; public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") + .AppendLine("The core services must be registered when enabling token entry validation.") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .ToString()); @@ -111,7 +116,8 @@ namespace OpenIddict.Validation /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() + .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000) .Build(); @@ -137,8 +143,7 @@ namespace OpenIddict.Validation return; } - var type = await _tokenManager.GetTypeAsync(token); - if (!string.Equals(type, TokenTypeHints.AccessToken, StringComparison.OrdinalIgnoreCase)) + if (!await _tokenManager.HasTypeAsync(token, TokenTypeHints.AccessToken)) { context.Reject( error: Errors.InvalidToken, @@ -175,6 +180,7 @@ namespace OpenIddict.Validation /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() .UseSingletonHandler() .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) .Build(); @@ -186,7 +192,7 @@ namespace OpenIddict.Validation /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { @@ -196,34 +202,43 @@ namespace OpenIddict.Validation // If a principal was already attached, don't overwrite it. if (context.Principal != null) { - return default; + return; } - // If the token cannot be validated, don't return an error to allow another handle to validate it. + // If the token cannot be validated, don't return an error to allow another handler to validate it. if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Token)) { - return default; + return; } - // 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 default; - } + var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the server configuration."); + + // Clone the token validation parameters and set the issuer and the signing keys using the + // OpenID Connect server configuration (that can be static or retrieved using discovery). + var parameters = context.Options.TokenValidationParameters.Clone(); + parameters.ValidIssuer = configuration.Issuer ?? context.Issuer?.AbsoluteUri; + parameters.IssuerSigningKeys = configuration.SigningKeys; - // Clone the token validation parameters before mutating them. - parameters = parameters.Clone(); - parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key); - parameters.ValidIssuer = context.Issuer?.AbsoluteUri; + // Populate the token decryption keys from the encryption credentials set in the options. + parameters.TokenDecryptionKeys = + from credentials in context.Options.EncryptionCredentials + select credentials.Key; // If the token cannot be validated, don't return an error to allow another handle to validate it. var result = context.Options.JsonWebTokenHandler.ValidateToken(context.Token, parameters); if (!result.IsValid) { + // If validation failed because of an unrecognized key identifier, inform the configuration manager + // that the configuration MAY have be refreshed by sending a new discovery request to the server. + if (result.Exception is SecurityTokenSignatureKeyNotFoundException) + { + context.Options.ConfigurationManager.RequestRefresh(); + } + context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token); - return default; + return; } // Note: tokens that are considered valid at this point are guaranteed to be access tokens, @@ -233,8 +248,83 @@ namespace OpenIddict.Validation context.Logger.LogTrace("The self-contained JWT token '{Token}' was successfully validated and the following " + "claims could be extracted: {Claims}.", context.Token, context.Principal.Claims); + } + } - return default; + /// + /// Contains the logic responsible of validating the tokens using OAuth 2.0 introspection. + /// + public class IntrospectToken : IOpenIddictValidationHandler + { + private readonly OpenIddictValidationService _service; + + public IntrospectToken([NotNull] OpenIddictValidationService service) + => _service = service; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal != null) + { + return; + } + + var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the server configuration."); + + if (string.IsNullOrEmpty(configuration.IntrospectionEndpoint) || + !Uri.TryCreate(configuration.IntrospectionEndpoint, UriKind.Absolute, out Uri address) || + !address.IsWellFormedOriginalString()) + { + context.Reject( + error: Errors.ServerError, + description: "This resource server is currently unavailable. Try again later."); + + return; + } + + try + { + var principal = await _service.IntrospectTokenAsync(address, context.Token, TokenTypeHints.AccessToken) ?? + throw new InvalidOperationException("An unknown error occurred while introspecting the access token."); + + // Note: tokens that are considered valid at this point are assumed to be access tokens, + // as the introspection handlers ensure the introspected token type matches the expected + // type when a "token_usage" claim was returned as part of the introspection response. + context.Principal = principal.SetTokenType(TokenTypeHints.AccessToken); + + context.Logger.LogTrace("The token '{Token}' was successfully introspected and the following claims " + + "could be extracted: {Claims}.", context.Token, context.Principal.Claims); + } + + catch (Exception exception) + { + context.Logger.LogDebug(exception, "An error occurred while introspecting the access token."); + + // If an error occurred while introspecting the token, allow other handlers to validate it. + } } } @@ -249,7 +339,7 @@ namespace OpenIddict.Validation public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetOrder(IntrospectToken.Descriptor.Order + 1_000) .Build(); /// @@ -308,7 +398,7 @@ namespace OpenIddict.Validation private readonly IOpenIddictTokenManager _tokenManager; public RestoreReferenceTokenProperties() => throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") + .AppendLine("The core services must be registered when enabling token entry validation.") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .ToString()); @@ -321,7 +411,8 @@ namespace OpenIddict.Validation /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() + .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) .Build(); @@ -370,7 +461,7 @@ namespace OpenIddict.Validation public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) .Build(); /// @@ -541,7 +632,7 @@ namespace OpenIddict.Validation private readonly IOpenIddictTokenManager _tokenManager; public ValidateTokenEntry() => throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") + .AppendLine("The core services must be registered when enabling token entry validation.") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .ToString()); @@ -554,7 +645,8 @@ namespace OpenIddict.Validation /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() + .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateAudience.Descriptor.Order + 1_000) .Build(); @@ -603,7 +695,7 @@ namespace OpenIddict.Validation private readonly IOpenIddictAuthorizationManager _authorizationManager; public ValidateAuthorizationEntry() => throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling reference tokens support.") + .AppendLine("The core services must be registered when enabling authorization entry validation.") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") .ToString()); @@ -616,7 +708,8 @@ namespace OpenIddict.Validation /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() + .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) .Build(); @@ -712,5 +805,47 @@ namespace OpenIddict.Validation return default; } } + + /// + /// Contains the logic responsible of extracting potential errors from the response. + /// + public class HandleErrorResponse : IOpenIddictValidationHandler where TContext : BaseValidatingContext + { + /// + /// 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] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Reject( + error: context.Response.Error, + description: context.Response.ErrorDescription, + uri: context.Response.ErrorUri); + + return default; + } + + return default; + } + } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 6c10ecb9..5abf0915 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -45,13 +47,29 @@ namespace OpenIddict.Validation public IList DefaultHandlers { get; } = new List(OpenIddictValidationHandlers.DefaultHandlers); + /// + /// Gets or sets the type of validation used by the OpenIddict validation services. + /// By default, local validation is always used. + /// + public OpenIddictValidationType ValidationType { get; set; } = OpenIddictValidationType.Direct; + + /// + /// Gets or sets the client identifier sent to the authorization server when using remote validation. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the client secret sent to the authorization server when using remote validation. + /// + public string ClientSecret { get; set; } + /// /// Gets or sets a boolean indicating whether a database call is made /// to validate the authorization entry associated with the received tokens. /// Note: enabling this option may have an impact on performance and /// can only be used with an OpenIddict-based authorization server. /// - public bool EnableAuthorizationValidation { get; set; } + public bool EnableAuthorizationEntryValidation { get; set; } /// /// Gets or sets a boolean indicating whether a database call is made @@ -59,7 +77,7 @@ namespace OpenIddict.Validation /// Note: enabling this option may have an impact on performance but /// is required when the OpenIddict server emits reference tokens. /// - public bool EnableTokenValidation { get; set; } + public bool EnableTokenEntryValidation { get; set; } /// /// Gets or sets the absolute URL of the OAuth 2.0/OpenID Connect server. @@ -72,6 +90,17 @@ namespace OpenIddict.Validation /// public Uri MetadataAddress { get; set; } + /// + /// Gets or sets the OAuth 2.0/OpenID Connect static server configuration, if applicable. + /// + public OpenIdConnectConfiguration Configuration { get; set; } + + /// + /// Gets or sets the configuration manager used to retrieve + /// and cache the OAuth 2.0/OpenID Connect server configuration. + /// + public IConfigurationManager ConfigurationManager { get; set; } + /// /// Gets the intended audiences of this resource server. /// Setting this property is recommended when the authorization @@ -81,7 +110,7 @@ namespace OpenIddict.Validation /// /// Gets or sets the optional "realm" value returned to - /// the caller as part of the WWW-Authenticate header. + /// the caller as part of challenge responses. /// public string Realm { get; set; } @@ -90,6 +119,7 @@ namespace OpenIddict.Validation /// public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters { + AuthenticationType = TokenValidationParameters.DefaultAuthenticationType, ClockSkew = TimeSpan.Zero, NameClaimType = Claims.Name, RoleClaimType = Claims.Role, diff --git a/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs b/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs new file mode 100644 index 00000000..218eb439 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationRetriever.cs @@ -0,0 +1,68 @@ +/* + * 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.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace OpenIddict.Validation +{ + public class OpenIddictValidationRetriever : IConfigurationRetriever + { + private readonly OpenIddictValidationService _service; + + /// + /// Creates a new instance of the class. + /// + /// The validation service. + public OpenIddictValidationRetriever([NotNull] OpenIddictValidationService service) + => _service = service; + + /// + /// Retrieves the OpenID Connect server configuration from the specified address. + /// + /// The address of the remote metadata endpoint. + /// The retriever used by IdentityModel. + /// The that can be used to abort the operation. + /// The OpenID Connect server configuration retrieved from the remote server. + async Task IConfigurationRetriever.GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString()) + { + throw new ArgumentException("The address must be a valid absolute URI.", nameof(address)); + } + + cancel.ThrowIfCancellationRequested(); + + var configuration = await _service.GetConfigurationAsync(uri, cancel) ?? + throw new InvalidOperationException("The server configuration couldn't be retrieved."); + + if (!Uri.TryCreate(configuration.JwksUri, UriKind.Absolute, out uri) || !uri.IsWellFormedOriginalString()) + { + throw new InvalidOperationException("The JWKS URI couldn't be resolved from the provider metadata."); + } + + configuration.JsonWebKeySet = await _service.GetSecurityKeysAsync(uri, cancel) ?? + throw new InvalidOperationException("The server JSON Web Key set couldn't be retrieved."); + + // Copy the signing keys found in the JSON Web Key Set to the SigningKeys collection. + foreach (var key in configuration.JsonWebKeySet.GetSigningKeys()) + { + configuration.SigningKeys.Add(key); + } + + return configuration; + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs new file mode 100644 index 00000000..c9756c38 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs @@ -0,0 +1,493 @@ +/* + * 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.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Validation.OpenIddictValidationEvents; + +namespace OpenIddict.Validation +{ + public class OpenIddictValidationService + { + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + /// The service provider. + public OpenIddictValidationService([NotNull] IServiceProvider provider) + => _provider = provider; + + /// + /// Retrieves the OpenID Connect server configuration from the specified address. + /// + /// The address of the remote metadata endpoint. + /// The that can be used to abort the operation. + /// The OpenID Connect server configuration retrieved from the remote server. + public async ValueTask GetConfigurationAsync( + [NotNull] Uri address, CancellationToken cancellationToken = default) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.IsAbsoluteUri) + { + throw new ArgumentException("The address must be an absolute URI.", nameof(address)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var provider = scope.ServiceProvider.GetRequiredService(); + var transaction = await provider.CreateTransactionAsync(); + + var request = new OpenIddictRequest(); + request = await PrepareConfigurationRequestAsync(); + request = await ApplyConfigurationRequestAsync(); + var response = await ExtractConfigurationResponseAsync(); + + var configuration = await HandleConfigurationResponseAsync(); + if (configuration == null) + { + throw new InvalidOperationException("The OpenID Connect server configuration couldn't be retrieved."); + } + + return configuration; + + async ValueTask PrepareConfigurationRequestAsync() + { + var context = new PrepareConfigurationRequestContext(transaction) + { + Address = address, + Request = request + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while preparing the configuration request.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ApplyConfigurationRequestAsync() + { + var context = new ApplyConfigurationRequestContext(transaction) + { + Request = request + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while sending the configuration request.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ExtractConfigurationResponseAsync() + { + var context = new ExtractConfigurationResponseContext(transaction) + { + Request = request + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while extracting the configuration response.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Response; + } + + async ValueTask HandleConfigurationResponseAsync() + { + var context = new HandleConfigurationResponseContext(transaction) + { + Request = request, + Response = response + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while handling the configuration response.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Configuration; + } + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } + + /// + /// Retrieves the security keys exposed by the specified JWKS endpoint. + /// + /// The address of the remote metadata endpoint. + /// The that can be used to abort the operation. + /// The security keys retrieved from the remote server. + public async ValueTask GetSecurityKeysAsync( + [NotNull] Uri address, CancellationToken cancellationToken = default) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.IsAbsoluteUri) + { + throw new ArgumentException("The address must be an absolute URI.", nameof(address)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var provider = scope.ServiceProvider.GetRequiredService(); + var transaction = await provider.CreateTransactionAsync(); + + var request = new OpenIddictRequest(); + request = await PrepareCryptographyRequestAsync(); + request = await ApplyCryptographyRequestAsync(); + + var response = await ExtractCryptographyResponseAsync(); + + var keys = await HandleCryptographyResponseAsync(); + if (keys == null) + { + throw new InvalidOperationException("An unknown error occurred while retrieving the JWK set."); + } + + return keys; + + async ValueTask PrepareCryptographyRequestAsync() + { + var context = new PrepareCryptographyRequestContext(transaction) + { + Address = address, + Request = request + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while preparing the cryptography request.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ApplyCryptographyRequestAsync() + { + var context = new ApplyCryptographyRequestContext(transaction) + { + Request = request + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while sending the cryptography request.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Request; + } + + async ValueTask ExtractCryptographyResponseAsync() + { + var context = new ExtractCryptographyResponseContext(transaction) + { + Request = request + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while extracting the cryptography response.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.Response; + } + + async ValueTask HandleCryptographyResponseAsync() + { + var context = new HandleCryptographyResponseContext(transaction) + { + Request = request, + Response = response + }; + + await provider.DispatchAsync(context); + + if (context.IsRejected) + { + var message = new StringBuilder() + .AppendLine("An error occurred while handling the cryptography response.") + .AppendFormat("Error: {0}", context.Error ?? "(not available)") + .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") + .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") + .ToString(); + + throw new OpenIddictExceptions.GenericException(message, + context.Error, context.ErrorDescription, context.ErrorUri); + } + + return context.SecurityKeys; + } + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } + + /// + /// Sends an introspection request to the specified address and returns the corresponding principal. + /// + /// The address of the remote metadata endpoint. + /// The token to introspect. + /// The that can be used to abort the operation. + /// The claims principal created from the claim retrieved from the remote server. + public ValueTask IntrospectTokenAsync( + [NotNull] Uri address, [NotNull] string token, CancellationToken cancellationToken = default) + => IntrospectTokenAsync(address, token, type: null, cancellationToken); + + /// + /// Sends an introspection request to the specified address and returns the corresponding principal. + /// + /// The address of the remote metadata endpoint. + /// The token to introspect. + /// The token type to introspect. + /// The that can be used to abort the operation. + /// The claims principal created from the claim retrieved from the remote server. + public async ValueTask IntrospectTokenAsync( + [NotNull] Uri address, [NotNull] string token, + [CanBeNull] string type, CancellationToken cancellationToken = default) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.IsAbsoluteUri) + { + throw new ArgumentException("The address must be an absolute URI.", nameof(address)); + } + + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentException("The token cannot be null or empty.", nameof(token)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Note: this service is registered as a singleton service. As such, it cannot + // directly depend on scoped services like the validation provider. To work around + // this limitation, a scope is manually created for each method to this service. + var scope = _provider.CreateScope(); + + // Note: a try/finally block is deliberately used here to ensure the service scope + // can be disposed of asynchronously if it implements IAsyncDisposable. + try + { + var provider = scope.ServiceProvider.GetRequiredService(); + var transaction = await provider.CreateTransactionAsync(); + + var request = new OpenIddictRequest(); + request = await PrepareIntrospectionRequestAsync(); + request = await ApplyIntrospectionRequestAsync(); + var response = await ExtractIntrospectionResponseAsync(); + + var principal = await HandleIntrospectionResponseAsync(); + if (principal == null) + { + throw new InvalidOperationException("An unknown error occurred while introspecting the token."); + } + + return principal; + + async ValueTask PrepareIntrospectionRequestAsync() + { + var context = new PrepareIntrospectionRequestContext(transaction) + { + Address = address, + Request = request, + Token = token, + TokenType = type + }; + + await provider.DispatchAsync(context); + + return context.Request; + } + + async ValueTask ApplyIntrospectionRequestAsync() + { + var context = new ApplyIntrospectionRequestContext(transaction) + { + Request = request + }; + + await provider.DispatchAsync(context); + + return context.Request; + } + + async ValueTask ExtractIntrospectionResponseAsync() + { + var context = new ExtractIntrospectionResponseContext(transaction) + { + Request = request + }; + + await provider.DispatchAsync(context); + + return context.Response; + } + + async ValueTask HandleIntrospectionResponseAsync() + { + var context = new HandleIntrospectionResponseContext(transaction) + { + Request = request, + Response = response, + Token = token, + TokenType = type + }; + + await provider.DispatchAsync(context); + + return context.Principal; + } + } + + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationType.cs b/src/OpenIddict.Validation/OpenIddictValidationType.cs new file mode 100644 index 00000000..16c26422 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationType.cs @@ -0,0 +1,29 @@ +/* + * 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. + */ + +namespace OpenIddict.Validation +{ + /// + /// Represents the type of validation performed by the OpenIddict validation services. + /// + public enum OpenIddictValidationType + { + /// + /// Configures the OpenIddict validation services to use direct validation. + /// By default, direct validation uses IdentityModel to validate JWT tokens, + /// but a different token format can be used by registering the corresponding + /// package (e.g OpenIddict.Validation.DataProtection, for Data Protection tokens). + /// + Direct = 0, + + /// + /// Configures the OpenIddict validation services to use introspection. + /// When using introspection, an OAuth 2.0 introspection request is sent + /// to the authorization server to validate the received access token. + /// + Introspection = 1 + } +} diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs index f60ac547..44e6cac7 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs @@ -178,45 +178,100 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Equal(value.GetHashCode(), parameter.GetHashCode()); } - [Fact] - public void GetParameter_ThrowsAnExceptionForNegativeIndex() + [Theory] + [InlineData(null)] + [InlineData("")] + public void GetNamedParameter_ThrowsAnExceptionForNullOrEmptyName(string name) { // Arrange var parameter = new OpenIddictParameter(); // Act - var exception = Assert.Throws(() => parameter.GetParameter(-1)); + var exception = Assert.Throws(() => parameter.GetNamedParameter(name)); // Assert - Assert.Equal("index", exception.ParamName); - Assert.StartsWith("The item index cannot be negative.", exception.Message); + Assert.Equal("name", exception.ParamName); + Assert.StartsWith("The item name cannot be null or empty.", exception.Message); } - [Theory] - [InlineData(null)] - [InlineData("")] - public void GetParameter_ThrowsAnExceptionForNullOrEmptyName(string name) + [Fact] + public void GetNamedParameter_ReturnsNullForPrimitiveValues() + { + // Arrange + var parameter = new OpenIddictParameter(42); + + // Act and assert + Assert.Null(parameter.GetNamedParameter("parameter")); + } + + [Fact] + public void GetNamedParameter_ReturnsNullForArrays() + { + // Arrange + var parameter = new OpenIddictParameter(new[] + { + "Fabrikam", + "Contoso" + }); + + // Act and assert + Assert.Null(parameter.GetNamedParameter("Fabrikam")); + } + + [Fact] + public void GetNamedParameter_ReturnsNullForNonexistentItem() + { + // Arrange + var parameter = new OpenIddictParameter(new JsonElement()); + + // Act and assert + Assert.Null(parameter.GetNamedParameter("parameter")); + } + + [Fact] + public void GetNamedParameter_ReturnsNullForJsonArrays() + { + // Arrange + var parameter = new OpenIddictParameter( + JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); + + // Act and assert + Assert.Null(parameter.GetNamedParameter("Fabrikam")); + } + + [Fact] + public void GetNamedParameter_ReturnsExpectedParameterForJsonObject() + { + // Arrange + var parameter = new OpenIddictParameter( + JsonSerializer.Deserialize(@"{""parameter"":""value""}")); + + // Act and assert + Assert.Equal("value", (string) parameter.GetNamedParameter("parameter")); + } + + [Fact] + public void GetUnnamedParameter_ThrowsAnExceptionForNegativeIndex() { // Arrange var parameter = new OpenIddictParameter(); // Act - var exception = Assert.Throws(() => parameter.GetParameter(name)); + var exception = Assert.Throws(() => parameter.GetUnnamedParameter(-1)); // Assert - Assert.Equal("name", exception.ParamName); - Assert.StartsWith("The item name cannot be null or empty.", exception.Message); + Assert.Equal("index", exception.ParamName); + Assert.StartsWith("The item index cannot be negative.", exception.Message); } [Fact] - public void GetParameter_ReturnsNullForPrimitiveValues() + public void GetUnnamedParameter_ReturnsNullForPrimitiveValues() { // Arrange var parameter = new OpenIddictParameter(42); // Act and assert - Assert.Null(parameter.GetParameter(0)); - Assert.Null(parameter.GetParameter("parameter")); + Assert.Null(parameter.GetUnnamedParameter(0)); } [Fact] @@ -230,11 +285,11 @@ namespace OpenIddict.Abstractions.Tests.Primitives }); // Act and assert - Assert.Null(parameter.GetParameter(2)); + Assert.Null(parameter.GetUnnamedParameter(2)); } [Fact] - public void GetParameter_ReturnsNullForArrays() + public void GetUnnamedParameter_ReturnsExpectedNodeForArray() { // Arrange var parameter = new OpenIddictParameter(new[] @@ -244,109 +299,129 @@ namespace OpenIddict.Abstractions.Tests.Primitives }); // Act and assert - Assert.Null(parameter.GetParameter("Fabrikam")); + Assert.Equal("Fabrikam", (string) parameter.GetUnnamedParameter(0)); } [Fact] - public void GetParameter_ReturnsNullForOutOfRangeJsonArrayIndex() + public void GetUnnamedParameter_ReturnsNullForOutOfRangeJsonArrayIndex() { // Arrange var parameter = new OpenIddictParameter( JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); // Act and assert - Assert.Null(parameter.GetParameter(2)); + Assert.Null(parameter.GetUnnamedParameter(2)); } [Fact] - public void GetParameter_ReturnsNullForNonexistentItem() + public void GetUnnamedParameter_ReturnsNullForJsonObjects() { // Arrange - var parameter = new OpenIddictParameter(new JsonElement()); + var parameter = new OpenIddictParameter( + JsonSerializer.Deserialize(@"{""parameter"":""value""}")); // Act and assert - Assert.Null(parameter.GetParameter("parameter")); + Assert.Null(parameter.GetUnnamedParameter(0)); } [Fact] - public void GetParameter_ReturnsNullForJsonArrays() + public void GetUnnamedParameter_ReturnsExpectedNodeForJsonArray() { // Arrange var parameter = new OpenIddictParameter( JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); // Act and assert - Assert.Null(parameter.GetParameter("Fabrikam")); + Assert.Equal("Fabrikam", (string) parameter.GetUnnamedParameter(0)); } [Fact] - public void GetParameter_ReturnsNullForJsonObjects() + public void GetNamedParameters_ReturnsEmptyDictionaryForPrimitiveValues() { // Arrange - var parameter = new OpenIddictParameter( - JsonSerializer.Deserialize(@"{""parameter"":""value""}")); + var parameter = new OpenIddictParameter(42); + + // Act and assert + Assert.Empty(parameter.GetNamedParameters()); + } + + [Fact] + public void GetNamedParameters_ReturnsEmptyDictionaryForArrays() + { + // Arrange + var parameters = new[] + { + "Fabrikam", + "Contoso" + }; + + var parameter = new OpenIddictParameter(parameters); // Act and assert - Assert.Null(parameter.GetParameter(0)); + Assert.Empty(parameter.GetNamedParameters()); } [Fact] - public void GetParameter_ReturnsNullForNullJsonObjects() + public void GetNamedParameters_ReturnsEmptyDictionaryForJsonValues() { // Arrange var parameter = new OpenIddictParameter( - JsonSerializer.Deserialize(@"{""parameter"":null}")); + JsonSerializer.Deserialize(@"{""field"":42}").GetProperty("field")); // Act and assert - Assert.Null(parameter.GetParameter(0)); - Assert.Null(parameter.GetParameter("parameter")); + Assert.Empty(parameter.GetNamedParameters()); } [Fact] - public void GetParameter_ReturnsExpectedNodeForArray() + public void GetNamedParameters_ReturnsEmptyDictionaryForJsonArrays() { // Arrange var parameter = new OpenIddictParameter( JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); // Act and assert - Assert.Equal("Fabrikam", (string) parameter.GetParameter(0)); + Assert.Empty(parameter.GetNamedParameters()); } [Fact] - public void GetParameter_ReturnsExpectedParameterForJsonObject() + public void GetNamedParameters_ReturnsExpectedParametersForJsonObjects() { // Arrange + var parameters = new Dictionary + { + ["parameter"] = "value" + }; + var parameter = new OpenIddictParameter( JsonSerializer.Deserialize(@"{""parameter"":""value""}")); // Act and assert - Assert.Equal("value", (string) parameter.GetParameter("parameter")); + Assert.Equal(parameters, parameter.GetNamedParameters().ToDictionary(pair => pair.Key, pair => (string) pair.Value)); } [Fact] - public void GetParameter_ReturnsExpectedNodeForJsonArray() + public void GetNamedParameters_ReturnsLastOccurrenceOfMultipleParameters() { // Arrange var parameter = new OpenIddictParameter( - JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); + JsonSerializer.Deserialize(@"{""parameter"":""value_1"",""parameter"":""value_2""}")); // Act and assert - Assert.Equal("Fabrikam", (string) parameter.GetParameter(0)); + Assert.Equal("value_2", parameter.GetNamedParameters()["parameter"]); } [Fact] - public void GetParameters_ReturnsEmptyEnumerationForPrimitiveValues() + public void GetUnnamedParameters_ReturnsEmptyListForPrimitiveValues() { // Arrange var parameter = new OpenIddictParameter(42); // Act and assert - Assert.Empty(parameter.GetParameters()); + Assert.Empty(parameter.GetUnnamedParameters()); } [Fact] - public void GetParameters_ReturnsExpectedParametersForArrays() + public void GetUnnamedParameters_ReturnsExpectedParametersForArrays() { // Arrange var parameters = new[] @@ -358,35 +433,23 @@ namespace OpenIddict.Abstractions.Tests.Primitives var parameter = new OpenIddictParameter(parameters); // Act and assert - Assert.Equal(parameters, from element in parameter.GetParameters() - select (string) element.Value); + Assert.Equal(parameters, from element in parameter.GetUnnamedParameters() + select (string) element); } [Fact] - public void GetParameters_ReturnsEmptyEnumerationForJsonValues() + public void GetUnnamedParameters_ReturnsEmptyListForJsonValues() { // Arrange var parameter = new OpenIddictParameter( JsonSerializer.Deserialize(@"{""field"":42}").GetProperty("field")); // Act and assert - Assert.Empty(parameter.GetParameters()); + Assert.Empty(parameter.GetUnnamedParameters()); } [Fact] - public void GetParameters_ReturnsNullKeysForJsonArrays() - { - // Arrange - var parameter = new OpenIddictParameter( - JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); - - // Act and assert - Assert.All(from element in parameter.GetParameters() - select element.Key, key => Assert.Null(key)); - } - - [Fact] - public void GetParameters_ReturnsExpectedParametersForJsonArrays() + public void GetUnnamedParameters_ReturnsExpectedParametersForJsonArrays() { // Arrange var parameters = new[] @@ -399,24 +462,19 @@ namespace OpenIddict.Abstractions.Tests.Primitives JsonSerializer.Deserialize(@"[""Fabrikam"",""Contoso""]")); // Act and assert - Assert.Equal(parameters, from element in parameter.GetParameters() - select (string) element.Value); + Assert.Equal(parameters, from element in parameter.GetUnnamedParameters() + select (string) element); } [Fact] - public void GetParameters_ReturnsExpectedParametersForJsonObjects() + public void GetUnnamedParameters_ReturnsEmptyListForJsonObjects() { // Arrange - var parameters = new Dictionary - { - ["parameter"] = "value" - }; - var parameter = new OpenIddictParameter( JsonSerializer.Deserialize(@"{""parameter"":""value""}")); // Act and assert - Assert.Equal(parameters, parameter.GetParameters().ToDictionary(pair => pair.Key, pair => (string) pair.Value)); + Assert.Empty(parameter.GetUnnamedParameters()); } [Fact] @@ -852,6 +910,16 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Null((string) new OpenIddictParameter(new[] { "Contoso", "Fabrikam" })); } + [Fact] + public void StringConverter_ReturnsDefaultValueForUnsupportedJsonValues() + { + // Arrange, act and assert + Assert.Null((string) new OpenIddictParameter( + JsonSerializer.Deserialize(@"[""Contoso"",""Fabrikam""]"))); + Assert.Null((string) new OpenIddictParameter( + JsonSerializer.Deserialize(@"{""field"":""Fabrikam""}"))); + } + [Fact] public void StringConverter_CanConvertFromPrimitiveValues() { @@ -867,7 +935,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives // Arrange, act and assert Assert.Equal("Fabrikam", (string) new OpenIddictParameter( JsonSerializer.Deserialize(@"{""field"":""Fabrikam""}").GetProperty("field"))); - Assert.Equal("false", (string) new OpenIddictParameter( + Assert.Equal(bool.FalseString, (string) new OpenIddictParameter( JsonSerializer.Deserialize(@"{""field"":false}").GetProperty("field"))); Assert.Equal("42", (string) new OpenIddictParameter( JsonSerializer.Deserialize(@"{""field"":42}").GetProperty("field"))); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index cc580065..c9fd481d 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -1093,7 +1093,7 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(new StringBuilder() .AppendLine("The specified principal doesn't contain any claims-based identity.") - .Append("Make sure that both 'ClaimsPrincipal.Identity' is not null.") + .Append("Make sure that 'ClaimsPrincipal.Identity' is not null.") .ToString(), exception.Message); }