Browse Source

Introduce introspection support and revamp the events model used by the validation handler

pull/950/head
Kévin Chalet 6 years ago
parent
commit
f81f8fc7dd
  1. 3
      eng/Versions.props
  2. 5
      samples/Mvc.Client/Startup.cs
  3. 14
      samples/Mvc.Client/web.config
  4. 11
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  5. 9
      samples/Mvc.Server/Controllers/ResourceController.cs
  6. 14
      samples/Mvc.Server/Controllers/UserinfoController.cs
  7. 10
      samples/Mvc.Server/Startup.cs
  8. 14
      samples/Mvc.Server/web.config
  9. 66
      src/OpenIddict.Abstractions/OpenIddictExceptions.cs
  10. 4
      src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs
  11. 162
      src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs
  12. 2
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
  13. 15
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  14. 3
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs
  15. 5
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs
  16. 15
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  17. 26
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  18. 3
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  19. 19
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  20. 3
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  21. 1
      src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj
  22. 2
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs
  23. 42
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs
  24. 3
      src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj
  25. 19
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  26. 16
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs
  27. 1
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpExtensions.cs
  28. 9
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlerFilters.cs
  29. 40
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Discovery.cs
  30. 29
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs
  31. 295
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  32. 33
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHelpers.cs
  33. 8
      src/OpenIddict.Validation/OpenIddict.Validation.csproj
  34. 98
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  35. 134
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  36. 143
      src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs
  37. 98
      src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs
  38. 39
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  39. 7
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  40. 42
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  41. 313
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
  42. 504
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  43. 193
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  44. 36
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  45. 68
      src/OpenIddict.Validation/OpenIddictValidationRetriever.cs
  46. 493
      src/OpenIddict.Validation/OpenIddictValidationService.cs
  47. 29
      src/OpenIddict.Validation/OpenIddictValidationType.cs
  48. 208
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictParameterTests.cs
  49. 2
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

3
eng/Versions.props

@ -2,7 +2,7 @@
<PropertyGroup>
<VersionPrefix>3.0.0</VersionPrefix>
<PreReleaseVersionLabel>alpha1</PreReleaseVersionLabel>
<PreReleaseVersionLabel>beta1</PreReleaseVersionLabel>
</PropertyGroup>
<PropertyGroup>
@ -42,6 +42,7 @@
<MongoDbVersion>2.9.0</MongoDbVersion>
<MoqVersion>4.13.1</MoqVersion>
<OwinVersion>4.1.0</OwinVersion>
<SystemNetHttpJsonVersion>3.2.0</SystemNetHttpJsonVersion>
<SystemTextJsonVersion>4.7.1</SystemTextJsonVersion>
<TasksExtensionsVersion>4.5.4</TasksExtensionsVersion>
</PropertyGroup>

5
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(

14
samples/Mvc.Client/web.config

@ -1,14 +0,0 @@
<?xml version="1.0"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="COMPLUS_ForceENC" value="1" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>

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

9
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<string, string>
{
[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.");

14
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<string, string>
{
[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<string, object>(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

10
samples/Mvc.Server/Startup.cs

@ -18,7 +18,7 @@ namespace Mvc.Server
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddControllersWithViews();
services.AddDbContext<ApplicationDbContext>(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<IEmailSender, AuthMessageSender>();
@ -157,7 +156,6 @@ namespace Mvc.Server
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(options => options.MapControllerRoute(

14
samples/Mvc.Server/web.config

@ -1,14 +0,0 @@
<?xml version="1.0"?>
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
<environmentVariable name="COMPLUS_ForceENC" value="1" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>

66
src/OpenIddict.Abstractions/OpenIddictExceptions.cs

@ -34,6 +34,72 @@ namespace OpenIddict.Abstractions
}
}
/// <summary>
/// Represents a generic OpenIddict exception.
/// </summary>
public class GenericException : Exception
{
/// <summary>
/// Creates a new <see cref="GenericException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
public GenericException(string message)
: this(message, null)
{
}
/// <summary>
/// Creates a new <see cref="GenericException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="error">The error type.</param>
public GenericException(string message, string error)
: this(message, error, description: null)
{
}
/// <summary>
/// Creates a new <see cref="GenericException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="error">The error type.</param>
/// <param name="description">The error description.</param>
public GenericException(string message, string error, string description)
: this(message, error, description, uri: null)
{
}
/// <summary>
/// Creates a new <see cref="GenericException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
/// <param name="error">The error type.</param>
/// <param name="description">The error description.</param>
/// <param name="uri">The error URI.</param>
public GenericException(string message, string error, string description, string uri)
: base(message)
{
Error = error;
ErrorDescription = description;
ErrorUri = uri;
}
/// <summary>
/// Gets the error type.
/// </summary>
public string Error { get; }
/// <summary>
/// Gets the error description.
/// </summary>
public string ErrorDescription { get; }
/// <summary>
/// Gets the error URI.
/// </summary>
public string ErrorUri { get; }
}
/// <summary>
/// Represents an OpenIddict validation exception.
/// </summary>

4
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.
/// </summary>
/// <returns>The parameters associated with this instance.</returns>
public IReadOnlyDictionary<string, OpenIddictParameter> GetParameters() => Parameters;
public IReadOnlyDictionary<string, OpenIddictParameter> GetParameters()
=> new ReadOnlyDictionary<string, OpenIddictParameter>(Parameters);
/// <summary>
/// Determines whether the current message contains the specified parameter.

162
src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs

@ -22,43 +22,43 @@ namespace OpenIddict.Abstractions
public readonly struct OpenIddictParameter : IEquatable<OpenIddictParameter>
{
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(bool value) => Value = value;
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(bool? value) => Value = value;
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(JsonElement value) => Value = value;
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(long value) => Value = value;
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(long? value) => Value = value;
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(string value) => Value = value;
/// <summary>
/// Initializes a new OpenID Connect parameter using the specified value.
/// Initializes a new parameter using the specified value.
/// </summary>
/// <param name="value">The parameter value.</param>
public OpenIddictParameter(string[] value) => Value = value;
@ -68,14 +68,14 @@ namespace OpenIddict.Abstractions
/// </summary>
/// <param name="index">The index of the child item.</param>
/// <returns>An <see cref="OpenIddictParameter"/> instance containing the item value.</returns>
public OpenIddictParameter? this[int index] => GetParameter(index);
public OpenIddictParameter? this[int index] => GetUnnamedParameter(index);
/// <summary>
/// Gets the child item corresponding to the specified name.
/// </summary>
/// <param name="name">The name of the child item.</param>
/// <returns>An <see cref="OpenIddictParameter"/> instance containing the item value.</returns>
public OpenIddictParameter? this[string name] => GetParameter(name);
public OpenIddictParameter? this[string name] => GetNamedParameter(name);
/// <summary>
/// Gets the associated value, that can be either a primitive CLR type
@ -92,53 +92,45 @@ namespace OpenIddict.Abstractions
/// <returns><c>true</c> if the two instances are equal, <c>false</c> otherwise.</returns>
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
}
}
/// <summary>
/// Gets the child item corresponding to the specified name.
/// </summary>
/// <param name="name">The name of the child item.</param>
/// <returns>An <see cref="OpenIddictParameter"/> instance containing the item value.</returns>
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;
}
/// <summary>
/// Gets the child item corresponding to the specified index.
/// </summary>
/// <param name="index">The index of the child item.</param>
/// <returns>An <see cref="OpenIddictParameter"/> instance containing the item value.</returns>
public OpenIddictParameter? GetParameter(int index)
public OpenIddictParameter? GetUnnamedParameter(int index)
{
if (index < 0)
{
@ -322,68 +340,51 @@ namespace OpenIddict.Abstractions
}
/// <summary>
/// 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.
/// </summary>
/// <param name="name">The name of the child item.</param>
/// <returns>An <see cref="OpenIddictParameter"/> instance containing the item value.</returns>
public OpenIddictParameter? GetParameter([NotNull] string name)
/// <returns>A dictionary of all the parameters associated with the current instance.</returns>
public IReadOnlyDictionary<string, OpenIddictParameter> GetNamedParameters()
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The item name cannot be null or empty.", nameof(name));
}
var parameters = new Dictionary<string, OpenIddictParameter>();
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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>An enumeration of all the parameters associated with the current instance.</returns>
public IEnumerable<KeyValuePair<string, OpenIddictParameter>> GetParameters()
/// <returns>An enumeration of all the unnamed parameters associated with the current instance.</returns>
public IReadOnlyList<OpenIddictParameter> GetUnnamedParameters()
{
var parameters = new List<OpenIddictParameter>();
if (Value is string[] array)
{
for (var index = 0; index < array.Length; index++)
{
yield return new KeyValuePair<string, OpenIddictParameter>(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<string, OpenIddictParameter>(null, value);
}
break;
case JsonValueKind.Object:
foreach (var property in element.EnumerateObject())
{
yield return new KeyValuePair<string, OpenIddictParameter>(property.Name, property.Value);
}
break;
parameters.Add(value);
}
}
yield break;
return parameters;
}
/// <summary>
@ -399,7 +400,7 @@ namespace OpenIddict.Abstractions
JsonElement value => value.ToString(),
_ => Value.ToString()
var value => value.ToString()
};
/// <summary>
@ -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);
/// <summary>
/// Determines whether an OpenID Connect parameter is null or empty.
/// Determines whether a parameter is null or empty.
/// </summary>
/// <param name="parameter">The OpenID Connect parameter.</param>
/// <param name="parameter">The parameter.</param>
/// <returns><c>true</c> if the parameter is null or empty, <c>false</c> otherwise.</returns>
public static bool IsNullOrEmpty(OpenIddictParameter parameter)
{

2
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs

@ -127,7 +127,7 @@ namespace Microsoft.EntityFrameworkCore
internal static IAsyncEnumerable<T> AsAsyncEnumerable<T>(
[NotNull] this IQueryable<T> source, CancellationToken cancellationToken = default)
{
if (source is null)
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}

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

3
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs

@ -81,8 +81,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictServerDataProtectionBuilder"/>.</returns>
public OpenIddictServerDataProtectionBuilder PreferDefaultTokenFormat()

5
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs

@ -29,9 +29,8 @@ namespace OpenIddict.Server.DataProtection
= new OpenIddictServerDataProtectionFormatter();
/// <summary>
/// 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 <c>false</c> 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 <c>false</c> by default.
/// </summary>
public bool PreferDefaultTokenFormat { get; set; }
}

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

26
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
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowAuthorizationCodeFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.AuthorizationCode));
=> Configure(options => options.GrantTypes.Add(GrantTypes.AuthorizationCode));
/// <summary>
/// Enables client credentials flow support. For more information about this
@ -1050,7 +1050,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowClientCredentialsFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.ClientCredentials));
=> Configure(options => options.GrantTypes.Add(GrantTypes.ClientCredentials));
/// <summary>
/// Enables custom grant type support.
@ -1073,7 +1073,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowDeviceCodeFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.DeviceCode));
=> Configure(options => options.GrantTypes.Add(GrantTypes.DeviceCode));
/// <summary>
/// Enables implicit flow support. For more information
@ -1083,7 +1083,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowImplicitFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.Implicit));
=> Configure(options => options.GrantTypes.Add(GrantTypes.Implicit));
/// <summary>
/// Enables password flow support. For more information about this specific
@ -1091,7 +1091,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowPasswordFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.Password));
=> Configure(options => options.GrantTypes.Add(GrantTypes.Password));
/// <summary>
/// Enables refresh token flow support. For more information about this
@ -1099,7 +1099,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowRefreshTokenFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.RefreshToken));
=> Configure(options => options.GrantTypes.Add(GrantTypes.RefreshToken));
/// <summary>
/// Sets the relative or absolute URLs associated to the authorization endpoint.
@ -1778,18 +1778,18 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Updates the token validation parameters using the specified delegate.
/// Sets the realm returned to the caller as part of challenge responses.
/// </summary>
/// <param name="configuration">The configuration delegate.</param>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetTokenValidationParameters([NotNull] Action<TokenValidationParameters> 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);
}
/// <summary>

3
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -80,6 +80,9 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseRequestContext : BaseContext
{

19
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<string>());
context.Principal.SetAudiences(ImmutableArray.Create<string>());
return default;
}
@ -2760,7 +2762,6 @@ namespace OpenIddict.Server
{
Claims.Private.Presenter => false,
Claims.Private.Scope => false,
Claims.Private.TokenId => false,
_ => true
});

3
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -111,6 +111,7 @@ namespace OpenIddict.Server
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
public string Realm { get; set; }

1
src/OpenIddict.Validation.Owin/OpenIddict.Validation.Owin.csproj

@ -16,7 +16,6 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Owin.Security" Version="$(OwinVersion)" />
</ItemGroup>

2
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.

42
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;
}
/// <summary>
@ -67,13 +71,23 @@ namespace OpenIddict.Validation.ServerIntegration
/// <param name="options">The options instance to initialize.</param>
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());
}

3
src/OpenIddict.Validation.SystemNetHttp/OpenIddict.Validation.SystemNetHttp.csproj

@ -14,10 +14,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="$(ExtensionsVersion)" />
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="System.Linq.Async" Version="$(LinqAsyncVersion)" />
<PackageReference Include="System.Net.Http.Json" Version="$(SystemNetHttpJsonVersion)" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' ">

19
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<HttpClientFactoryOptions>
{
#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<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>();
#else
var options = _serviceProvider.GetRequiredService<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>();
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationSystemNetHttpOptions>>();
#endif
var policy = options.CurrentValue.HttpErrorPolicy;
if (policy != null)

16
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConstants.cs

@ -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";
}
}
}

1
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.

9
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<bool>(false);
}
return new ValueTask<bool>(
string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.Options.MetadataAddress.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase));
string.Equals(context.Options.MetadataAddress?.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.Options.MetadataAddress?.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase));
}
}
}

40
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<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Configuration request processing:
*/
PrepareGetHttpRequest<PrepareConfigurationRequestContext>.Descriptor,
SendHttpRequest<ApplyConfigurationRequestContext>.Descriptor,
/*
* Configuration response processing:
*/
ExtractJsonHttpResponse<ExtractConfigurationResponseContext>.Descriptor,
/*
* Cryptography request processing:
*/
PrepareGetHttpRequest<PrepareCryptographyRequestContext>.Descriptor,
SendHttpRequest<ApplyCryptographyRequestContext>.Descriptor,
/*
* Configuration response processing:
*/
ExtractJsonHttpResponse<ExtractCryptographyResponseContext>.Descriptor);
}
}
}

29
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<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Introspection request processing:
*/
PreparePostHttpRequest<PrepareIntrospectionRequestContext>.Descriptor,
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor,
/*
* Introspection response processing:
*/
ExtractJsonHttpResponse<ExtractIntrospectionResponseContext>.Descriptor);
}
}
}

295
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<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authentication processing:
*/
PopulateTokenValidationParameters.Descriptor);
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; }
= ImmutableArray.Create<OpenIddictValidationHandlerDescriptor>()
.AddRange(Discovery.DefaultHandlers)
.AddRange(Introspection.DefaultHandlers);
/// <summary>
/// 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.
/// </summary>
public class PopulateTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
public class PrepareGetHttpRequest<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
{
private readonly IMemoryCache _cache;
private readonly IHttpClientFactory _factory;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<PrepareGetHttpRequest<TContext>>()
.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<string, string>(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;
}
}
/// <summary>
/// Contains the logic responsible of preparing an HTTP POST request message.
/// </summary>
public class PreparePostHttpRequest<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<PopulateTokenValidationParameters>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500)
.UseSingletonHandler<PreparePostHttpRequest<TContext>>()
.SetOrder(PrepareGetHttpRequest<TContext>.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<string, string>(parameter.Key, value));
// Store the HttpRequestMessage in the transaction properties.
context.Transaction.Properties[typeof(HttpRequestMessage).FullName] = request;
return await GetTokenValidationParametersAsync();
});
return default;
}
}
/// <summary>
/// Contains the logic responsible of sending the HTTP request to the remote server.
/// </summary>
public class SendHttpRequest<TContext> : IOpenIddictValidationHandler<TContext> 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<TokenValidationParameters> GetTokenValidationParametersAsync()
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<SendHttpRequest<TContext>>()
.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<SecurityKey> 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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the response from the JSON-encoded HTTP body.
/// </summary>
public class ExtractJsonHttpResponse<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseExternalContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<ExtractJsonHttpResponse<TContext>>()
.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<OpenIddictResponse> 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<OpenIddictResponse>(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<OpenIddictResponse>();
}
}
}

33
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
{
/// <summary>
/// Exposes companion extensions for the OpenIddict/ASP.NET Core integration.
/// </summary>
public static class OpenIddictValidationSystemNetHttpHelpers
{
/// <summary>
/// Gets the <see cref="HttpRequestMessage"/> associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="HttpRequestMessage"/> instance or <c>null</c> if it couldn't be found.</returns>
public static HttpRequestMessage GetHttpRequestMessage([NotNull] this OpenIddictValidationTransaction transaction)
=> transaction.GetProperty<HttpRequestMessage>(typeof(HttpRequestMessage).FullName);
/// <summary>
/// Gets the <see cref="HttpResponseMessage"/> associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="HttpResponseMessage"/> instance or <c>null</c> if it couldn't be found.</returns>
public static HttpResponseMessage GetHttpResponseMessage([NotNull] this OpenIddictValidationTransaction transaction)
=> transaction.GetProperty<HttpResponseMessage>(typeof(HttpResponseMessage).FullName);
}
}

8
src/OpenIddict.Validation/OpenIddict.Validation.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net461;net472;netcoreapp2.1;netcoreapp3.1;netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
@ -16,7 +16,11 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="$(IdentityModelVersion)" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(IdentityModelVersion)" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' != 'net461' And '$(TargetFramework)' != 'netstandard2.0' ">
<DefineConstants>$(DefineConstants);SUPPORTS_EPHEMERAL_KEY_SETS</DefineConstants>
</PropertyGroup>
</Project>

98
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.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder EnableAuthorizationValidation()
=> Configure(options => options.EnableAuthorizationValidation = true);
public OpenIddictValidationBuilder EnableAuthorizationEntryValidation()
=> Configure(options => options.EnableAuthorizationEntryValidation = true);
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder EnableTokenValidation()
=> Configure(options => options.EnableTokenValidation = true);
public OpenIddictValidationBuilder EnableTokenEntryValidation()
=> Configure(options => options.EnableTokenEntryValidation = true);
/// <summary>
/// Sets a static OpenID Connect server configuration, that will be used to
/// resolve the metadata/introspection endpoints and the issuer signing keys.
/// </summary>
/// <param name="configuration">The server configuration.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder SetConfiguration([NotNull] OpenIdConnectConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options => options.Configuration = configuration);
}
/// <summary>
/// Sets the client identifier client_id used when communicating
/// with the remote authorization server (e.g for introspection).
/// </summary>
/// <param name="identifier">The client identifier.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
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);
}
/// <summary>
/// Sets the client identifier client_secret used when communicating
/// with the remote authorization server (e.g for introspection).
/// </summary>
/// <param name="secret">The client secret.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
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);
}
/// <summary>
/// Sets the issuer address, which is used to determine the actual location of the
@ -441,20 +491,48 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// 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.
/// </summary>
/// <param name="configuration">The configuration delegate.</param>
/// <param name="address">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder SetTokenValidationParameters([NotNull] Action<TokenValidationParameters> 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);
}
/// <summary>
/// Sets the realm returned to the caller as part of challenge responses.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
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);
}
/// <summary>
/// Configures OpenIddict to use introspection instead of local/direct validation.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder UseIntrospection()
=> Configure(options => options.ValidationType = OpenIddictValidationType.Introspection);
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>

134
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
/// </summary>
public class OpenIddictValidationConfiguration : IPostConfigureOptions<OpenIddictValidationOptions>
{
private readonly OpenIddictValidationService _service;
public OpenIddictValidationConfiguration([NotNull] OpenIddictValidationService service)
=> _service = service;
/// <summary>
/// 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<OpenIdConnectConfiguration>(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<OpenIdConnectConfiguration>(
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<OpenIdConnectConfiguration>.DefaultAutomaticRefreshInterval,
RefreshInterval = ConfigurationManager<OpenIdConnectConfiguration>.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;
}
}
}

143
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
{
/// <summary>
/// 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.
/// </summary>
public class PrepareConfigurationRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="PrepareConfigurationRequestContext"/> class.
/// </summary>
public PrepareConfigurationRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each request to the configuration endpoint
/// to send the configuration request to the remote authorization server.
/// </summary>
public class ApplyConfigurationRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyConfigurationRequestContext"/> class.
/// </summary>
public ApplyConfigurationRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each configuration response
/// to extract the response parameters from the server response.
/// </summary>
public class ExtractConfigurationResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractConfigurationResponseContext"/> class.
/// </summary>
public ExtractConfigurationResponseContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each validated configuration response.
/// </summary>
public class HandleConfigurationResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandleConfigurationResponseContext"/> class.
/// </summary>
public HandleConfigurationResponseContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets the OpenID Connect configuration.
/// </summary>
public OpenIdConnectConfiguration Configuration { get; } = new OpenIdConnectConfiguration();
}
/// <summary>
/// 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.
/// </summary>
public class PrepareCryptographyRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="PrepareCryptographyRequestContext"/> class.
/// </summary>
public PrepareCryptographyRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each request to the cryptography endpoint
/// to send the cryptography request to the remote authorization server.
/// </summary>
public class ApplyCryptographyRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyCryptographyRequestContext"/> class.
/// </summary>
public ApplyCryptographyRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each cryptography response
/// to extract the response parameters from the server response.
/// </summary>
public class ExtractCryptographyResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractCryptographyResponseContext"/> class.
/// </summary>
public ExtractCryptographyResponseContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each validated cryptography response.
/// </summary>
public class HandleCryptographyResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandleCryptographyResponseContext"/> class.
/// </summary>
public HandleCryptographyResponseContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets the security keys.
/// </summary>
public JsonWebKeySet SecurityKeys { get; } = new JsonWebKeySet();
}
}
}

98
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
{
/// <summary>
/// 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.
/// </summary>
public class PrepareIntrospectionRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="PrepareIntrospectionRequestContext"/> class.
/// </summary>
public PrepareIntrospectionRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the token sent to the introspection endpoint.
/// </summary>
public string Token { get; set; }
/// <summary>
/// Gets or sets the token type sent to the introspection endpoint.
/// </summary>
public string TokenType { get; set; }
}
/// <summary>
/// Represents an event called for each request to the introspection endpoint
/// to send the introspection request to the remote authorization server.
/// </summary>
public class ApplyIntrospectionRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyIntrospectionRequestContext"/> class.
/// </summary>
public ApplyIntrospectionRequestContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each introspection response
/// to extract the response parameters from the server response.
/// </summary>
public class ExtractIntrospectionResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractIntrospectionResponseContext"/> class.
/// </summary>
public ExtractIntrospectionResponseContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each validated introspection response.
/// </summary>
public class HandleIntrospectionResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandleIntrospectionResponseContext"/> class.
/// </summary>
public HandleIntrospectionResponseContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the token sent to the introspection endpoint.
/// </summary>
public string Token { get; set; }
/// <summary>
/// Gets or sets the token type sent to the introspection endpoint.
/// </summary>
public string TokenType { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims resolved from the introspection response.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
}
}
}

39
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
/// </summary>
public OpenIddictValidationOptions Options => Transaction.Options;
/// <summary>
/// Gets the dictionary containing the properties associated with this event.
/// </summary>
public IDictionary<string, object> Properties { get; }
= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the OpenIddict request or <c>null</c> if it couldn't be extracted.
/// </summary>
@ -87,6 +79,9 @@ namespace OpenIddict.Validation
}
}
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseRequestContext : BaseContext
{
@ -123,6 +118,26 @@ namespace OpenIddict.Validation
public void SkipRequest() => IsRequestSkipped = true;
}
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseExternalContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="BaseRequestContext"/> class.
/// </summary>
protected BaseExternalContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the address of the external endpoint to communicate with.
/// </summary>
public Uri Address { get; set; }
}
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
@ -240,12 +255,8 @@ namespace OpenIddict.Validation
/// </summary>
public ProcessAuthenticationContext([NotNull] OpenIddictValidationTransaction transaction)
: base(transaction)
=> TokenValidationParameters = transaction.Options.TokenValidationParameters.Clone();
/// <summary>
/// Gets the token validation parameters used for the current request.
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; }
{
}
/// <summary>
/// Gets or sets the security principal.

7
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -36,6 +36,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddLogging();
builder.Services.AddOptions();
builder.Services.TryAddSingleton<OpenIddictValidationService>();
builder.Services.TryAddScoped<IOpenIddictValidationProvider, OpenIddictValidationProvider>();
// 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<RequireAuthorizationValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenValidationEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireLocalValidation>();
builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireIntrospectionValidation>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

42
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -18,7 +18,7 @@ namespace OpenIddict.Validation
/// <summary>
/// Represents a filter that excludes the associated handlers if authorization validation was not enabled.
/// </summary>
public class RequireAuthorizationValidationEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
public class RequireAuthorizationEntryValidationEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
@ -27,14 +27,46 @@ namespace OpenIddict.Validation
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.EnableAuthorizationValidation);
return new ValueTask<bool>(context.Options.EnableAuthorizationEntryValidation);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireLocalValidation : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.ValidationType == OpenIddictValidationType.Direct);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if introspection is not used.
/// </summary>
public class RequireIntrospectionValidation : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.ValidationType == OpenIddictValidationType.Introspection);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if token validation was not enabled.
/// </summary>
public class RequireTokenValidationEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
public class RequireTokenEntryValidationEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
@ -43,7 +75,7 @@ namespace OpenIddict.Validation
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.EnableTokenValidation);
return new ValueTask<bool>(context.Options.EnableTokenEntryValidation);
}
}
}

313
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<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Configuration response handling:
*/
HandleErrorResponse<HandleConfigurationResponseContext>.Descriptor,
ValidateIssuer.Descriptor,
ExtractCryptographyEndpointUri.Descriptor,
ExtractIntrospectionEndpointUri.Descriptor,
/*
* Cryptography response handling:
*/
HandleErrorResponse<HandleCryptographyResponseContext>.Descriptor,
ExtractSigningKeys.Descriptor);
/// <summary>
/// Contains the logic responsible of extracting the issuer from the discovery document.
/// </summary>
public class ValidateIssuer : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ValidateIssuer>()
.SetOrder(HandleErrorResponse<HandleConfigurationResponseContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document.
/// </summary>
public class ExtractCryptographyEndpointUri : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractCryptographyEndpointUri>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the introspection endpoint address from the discovery document.
/// </summary>
public class ExtractIntrospectionEndpointUri : IOpenIddictValidationHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractIntrospectionEndpointUri>()
.SetOrder(ExtractCryptographyEndpointUri.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the signing keys from the JWKS document.
/// </summary>
public class ExtractSigningKeys : IOpenIddictValidationHandler<HandleCryptographyResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleCryptographyResponseContext>()
.UseSingletonHandler<ExtractSigningKeys>()
.SetOrder(HandleErrorResponse<HandleCryptographyResponseContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
}
}
}

504
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<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Introspection response handling:
*/
AttachCredentials.Descriptor,
AttachAccessToken.Descriptor,
/*
* Introspection response handling:
*/
HandleErrorResponse<HandleIntrospectionResponseContext>.Descriptor,
HandleInactiveResponse.Descriptor,
ValidateWellKnownClaims.Descriptor,
ValidateIssuer.Descriptor,
ValidateTokenType.Descriptor,
PopulateClaims.Descriptor);
/// <summary>
/// Contains the logic responsible of attaching the client credentials to the introspection request.
/// </summary>
public class AttachCredentials : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.UseSingletonHandler<AttachCredentials>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] PrepareIntrospectionRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.ClientId = context.Options.ClientId;
context.Request.ClientSecret = context.Options.ClientSecret;
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching the access token to the introspection request.
/// </summary>
public class AttachAccessToken : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.UseSingletonHandler<AttachAccessToken>()
.SetOrder(AttachCredentials.Descriptor.Order + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] PrepareIntrospectionRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.Token = context.Token;
context.Request.TokenTypeHint = context.TokenType;
return default;
}
}
/// <summary>
/// Contains the logic responsible of extracting the active: false marker from the response.
/// </summary>
public class HandleInactiveResponse : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<HandleInactiveResponse>()
.SetOrder(HandleErrorResponse<HandleIntrospectionResponseContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of validating the well-known claims contained in the introspection response.
/// </summary>
public class ValidateWellKnownClaims : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateWellKnownClaims>()
.SetOrder(HandleInactiveResponse.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
}
/// <summary>
/// Contains the logic responsible of extracting the issuer from the introspection response.
/// </summary>
public class ValidateIssuer : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateIssuer>()
.SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of extracting and validating the token type from the introspection response.
/// </summary>
public class ValidateTokenType : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateTokenType>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the claims from the introspection response.
/// </summary>
public class PopulateClaims : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<PopulateClaims>()
.SetOrder(ValidateTokenType.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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
};
}
}
}
}
}

193
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);
/// <summary>
/// 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
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenValidationEnabled>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.UseScopedHandler<ValidateReferenceTokenIdentifier>()
.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
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireLocalValidation>()
.UseSingletonHandler<ValidateIdentityModelToken>()
.SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000)
.Build();
@ -186,7 +192,7 @@ namespace OpenIddict.Validation
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
/// <summary>
/// Contains the logic responsible of validating the tokens using OAuth 2.0 introspection.
/// </summary>
public class IntrospectToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly OpenIddictValidationService _service;
public IntrospectToken([NotNull] OpenIddictValidationService service)
=> _service = service;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionValidation>()
.UseSingletonHandler<IntrospectToken>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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<ProcessAuthenticationContext>()
.UseSingletonHandler<MapInternalClaims>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.SetOrder(IntrospectToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -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
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenValidationEnabled>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.UseScopedHandler<RestoreReferenceTokenProperties>()
.SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
.Build();
@ -370,7 +461,7 @@ namespace OpenIddict.Validation
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -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
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenValidationEnabled>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.UseScopedHandler<ValidateTokenEntry>()
.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
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAuthorizationValidationEnabled>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireAuthorizationEntryValidationEnabled>()
.UseScopedHandler<ValidateAuthorizationEntry>()
.SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000)
.Build();
@ -712,5 +805,47 @@ namespace OpenIddict.Validation
return default;
}
}
/// <summary>
/// Contains the logic responsible of extracting potential errors from the response.
/// </summary>
public class HandleErrorResponse<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.UseSingletonHandler<HandleErrorResponse<TContext>>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
}
}

36
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<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } =
new List<OpenIddictValidationHandlerDescriptor>(OpenIddictValidationHandlers.DefaultHandlers);
/// <summary>
/// Gets or sets the type of validation used by the OpenIddict validation services.
/// By default, local validation is always used.
/// </summary>
public OpenIddictValidationType ValidationType { get; set; } = OpenIddictValidationType.Direct;
/// <summary>
/// Gets or sets the client identifier sent to the authorization server when using remote validation.
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret sent to the authorization server when using remote validation.
/// </summary>
public string ClientSecret { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool EnableAuthorizationValidation { get; set; }
public bool EnableAuthorizationEntryValidation { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool EnableTokenValidation { get; set; }
public bool EnableTokenEntryValidation { get; set; }
/// <summary>
/// Gets or sets the absolute URL of the OAuth 2.0/OpenID Connect server.
@ -72,6 +90,17 @@ namespace OpenIddict.Validation
/// </summary>
public Uri MetadataAddress { get; set; }
/// <summary>
/// Gets or sets the OAuth 2.0/OpenID Connect static server configuration, if applicable.
/// </summary>
public OpenIdConnectConfiguration Configuration { get; set; }
/// <summary>
/// Gets or sets the configuration manager used to retrieve
/// and cache the OAuth 2.0/OpenID Connect server configuration.
/// </summary>
public IConfigurationManager<OpenIdConnectConfiguration> ConfigurationManager { get; set; }
/// <summary>
/// Gets the intended audiences of this resource server.
/// Setting this property is recommended when the authorization
@ -81,7 +110,7 @@ namespace OpenIddict.Validation
/// <summary>
/// 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.
/// </summary>
public string Realm { get; set; }
@ -90,6 +119,7 @@ namespace OpenIddict.Validation
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters
{
AuthenticationType = TokenValidationParameters.DefaultAuthenticationType,
ClockSkew = TimeSpan.Zero,
NameClaimType = Claims.Name,
RoleClaimType = Claims.Role,

68
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<OpenIdConnectConfiguration>
{
private readonly OpenIddictValidationService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationRetriever"/> class.
/// </summary>
/// <param name="service">The validation service.</param>
public OpenIddictValidationRetriever([NotNull] OpenIddictValidationService service)
=> _service = service;
/// <summary>
/// Retrieves the OpenID Connect server configuration from the specified address.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="retriever">The retriever used by IdentityModel.</param>
/// <param name="cancel">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The OpenID Connect server configuration retrieved from the remote server.</returns>
async Task<OpenIdConnectConfiguration> IConfigurationRetriever<OpenIdConnectConfiguration>.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;
}
}
}

493
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;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationService"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
public OpenIddictValidationService([NotNull] IServiceProvider provider)
=> _provider = provider;
/// <summary>
/// Retrieves the OpenID Connect server configuration from the specified address.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The OpenID Connect server configuration retrieved from the remote server.</returns>
public async ValueTask<OpenIdConnectConfiguration> 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<IOpenIddictValidationProvider>();
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<OpenIddictRequest> 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<OpenIddictRequest> 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<OpenIddictResponse> 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<OpenIdConnectConfiguration> 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();
}
}
}
/// <summary>
/// Retrieves the security keys exposed by the specified JWKS endpoint.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The security keys retrieved from the remote server.</returns>
public async ValueTask<JsonWebKeySet> 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<IOpenIddictValidationProvider>();
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<OpenIddictRequest> 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<OpenIddictRequest> 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<OpenIddictResponse> 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<JsonWebKeySet> 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();
}
}
}
/// <summary>
/// Sends an introspection request to the specified address and returns the corresponding principal.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="token">The token to introspect.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The claims principal created from the claim retrieved from the remote server.</returns>
public ValueTask<ClaimsPrincipal> IntrospectTokenAsync(
[NotNull] Uri address, [NotNull] string token, CancellationToken cancellationToken = default)
=> IntrospectTokenAsync(address, token, type: null, cancellationToken);
/// <summary>
/// Sends an introspection request to the specified address and returns the corresponding principal.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="token">The token to introspect.</param>
/// <param name="type">The token type to introspect.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The claims principal created from the claim retrieved from the remote server.</returns>
public async ValueTask<ClaimsPrincipal> 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<IOpenIddictValidationProvider>();
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<OpenIddictRequest> PrepareIntrospectionRequestAsync()
{
var context = new PrepareIntrospectionRequestContext(transaction)
{
Address = address,
Request = request,
Token = token,
TokenType = type
};
await provider.DispatchAsync(context);
return context.Request;
}
async ValueTask<OpenIddictRequest> ApplyIntrospectionRequestAsync()
{
var context = new ApplyIntrospectionRequestContext(transaction)
{
Request = request
};
await provider.DispatchAsync(context);
return context.Request;
}
async ValueTask<OpenIddictResponse> ExtractIntrospectionResponseAsync()
{
var context = new ExtractIntrospectionResponseContext(transaction)
{
Request = request
};
await provider.DispatchAsync(context);
return context.Response;
}
async ValueTask<ClaimsPrincipal> 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();
}
}
}
}
}

29
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
{
/// <summary>
/// Represents the type of validation performed by the OpenIddict validation services.
/// </summary>
public enum OpenIddictValidationType
{
/// <summary>
/// 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).
/// </summary>
Direct = 0,
/// <summary>
/// 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.
/// </summary>
Introspection = 1
}
}

208
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<ArgumentOutOfRangeException>(() => parameter.GetParameter(-1));
var exception = Assert.Throws<ArgumentException>(() => 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<JsonElement>(@"[""Fabrikam"",""Contoso""]"));
// Act and assert
Assert.Null(parameter.GetNamedParameter("Fabrikam"));
}
[Fact]
public void GetNamedParameter_ReturnsExpectedParameterForJsonObject()
{
// Arrange
var parameter = new OpenIddictParameter(
JsonSerializer.Deserialize<JsonElement>(@"{""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<ArgumentException>(() => parameter.GetParameter(name));
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => 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<JsonElement>(@"[""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<JsonElement>(@"{""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<JsonElement>(@"[""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<JsonElement>(@"{""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<JsonElement>(@"{""parameter"":null}"));
JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonElement>(@"[""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<string, string>
{
["parameter"] = "value"
};
var parameter = new OpenIddictParameter(
JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonElement>(@"[""Fabrikam"",""Contoso""]"));
JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonElement>(@"{""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<JsonElement>(@"[""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<JsonElement>(@"[""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<string, string>
{
["parameter"] = "value"
};
var parameter = new OpenIddictParameter(
JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonElement>(@"[""Contoso"",""Fabrikam""]")));
Assert.Null((string) new OpenIddictParameter(
JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonElement>(@"{""field"":""Fabrikam""}").GetProperty("field")));
Assert.Equal("false", (string) new OpenIddictParameter(
Assert.Equal(bool.FalseString, (string) new OpenIddictParameter(
JsonSerializer.Deserialize<JsonElement>(@"{""field"":false}").GetProperty("field")));
Assert.Equal("42", (string) new OpenIddictParameter(
JsonSerializer.Deserialize<JsonElement>(@"{""field"":42}").GetProperty("field")));

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

Loading…
Cancel
Save