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