Browse Source

Revamp the validation handler and add client assertions support

pull/1901/head
Kévin Chalet 2 years ago
parent
commit
4b9029e235
  1. 2
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 32
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  3. 24
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  4. 250
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  5. 31
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  6. 12
      src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs
  7. 76
      src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
  8. 58
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  9. 4
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  10. 48
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  11. 66
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  12. 236
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  13. 445
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  14. 24
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  15. 68
      src/OpenIddict.Validation/OpenIddictValidationService.cs

2
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -562,7 +562,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A
<value>The client identifier cannot be null or empty when using introspection.</value>
</data>
<data name="ID0132" xml:space="preserve">
<value>The client secret cannot be null or empty when using introspection.</value>
<value>The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used to produce client assertions if the authorization server supports this client authentication method.</value>
</data>
<data name="ID0133" xml:space="preserve">
<value>Authorization entry validation cannot be enabled when using introspection.</value>

32
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -605,14 +605,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.StateTokenPrincipal is not null ||
string.IsNullOrEmpty(context.StateToken))
if (string.IsNullOrEmpty(context.StateToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.StateTokenPrincipal,
Token = context.StateToken,
ValidTokenTypes = { TokenTypeHints.StateToken }
};
@ -1486,14 +1486,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.FrontchannelIdentityTokenPrincipal is not null ||
string.IsNullOrEmpty(context.FrontchannelIdentityToken))
if (string.IsNullOrEmpty(context.FrontchannelIdentityToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.FrontchannelIdentityTokenPrincipal,
Token = context.FrontchannelIdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -2011,14 +2011,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.FrontchannelAccessTokenPrincipal is not null ||
string.IsNullOrEmpty(context.FrontchannelAccessToken))
if (string.IsNullOrEmpty(context.FrontchannelAccessToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.FrontchannelAccessTokenPrincipal,
Token = context.FrontchannelAccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@ -2086,14 +2086,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.AuthorizationCodePrincipal is not null ||
string.IsNullOrEmpty(context.AuthorizationCode))
if (string.IsNullOrEmpty(context.AuthorizationCode))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.AuthorizationCodePrincipal,
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@ -2822,14 +2822,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.BackchannelIdentityTokenPrincipal is not null ||
string.IsNullOrEmpty(context.BackchannelIdentityToken))
if (string.IsNullOrEmpty(context.BackchannelIdentityToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.BackchannelIdentityTokenPrincipal,
Token = context.BackchannelIdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -3311,14 +3311,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.BackchannelAccessTokenPrincipal is not null ||
string.IsNullOrEmpty(context.BackchannelAccessToken))
if (string.IsNullOrEmpty(context.BackchannelAccessToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.BackchannelAccessTokenPrincipal,
Token = context.BackchannelAccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@ -3386,14 +3386,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.RefreshTokenPrincipal is not null ||
string.IsNullOrEmpty(context.RefreshToken))
if (string.IsNullOrEmpty(context.RefreshToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.RefreshTokenPrincipal,
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@ -3738,14 +3738,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.UserinfoTokenPrincipal is not null ||
string.IsNullOrEmpty(context.UserinfoToken))
if (string.IsNullOrEmpty(context.UserinfoToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.UserinfoTokenPrincipal,
Token = context.UserinfoToken,
ValidTokenTypes = { TokenTypeHints.UserinfoToken }
};

24
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -544,13 +544,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.ClientAssertionPrincipal is not null || string.IsNullOrEmpty(context.ClientAssertion))
if (string.IsNullOrEmpty(context.ClientAssertion))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.ClientAssertionPrincipal,
Token = context.ClientAssertion,
TokenFormat = context.ClientAssertionType switch
{
@ -1268,13 +1269,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.AccessTokenPrincipal is not null || string.IsNullOrEmpty(context.AccessToken))
if (string.IsNullOrEmpty(context.AccessToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.AccessTokenPrincipal,
Token = context.AccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@ -1340,13 +1342,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.AuthorizationCodePrincipal is not null || string.IsNullOrEmpty(context.AuthorizationCode))
if (string.IsNullOrEmpty(context.AuthorizationCode))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.AuthorizationCodePrincipal,
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@ -1412,13 +1415,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.DeviceCodePrincipal is not null || string.IsNullOrEmpty(context.DeviceCode))
if (string.IsNullOrEmpty(context.DeviceCode))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.DeviceCodePrincipal,
Token = context.DeviceCode,
ValidTokenTypes = { TokenTypeHints.DeviceCode }
};
@ -1484,13 +1488,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.GenericTokenPrincipal is not null || string.IsNullOrEmpty(context.GenericToken))
if (string.IsNullOrEmpty(context.GenericToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.GenericTokenPrincipal,
Token = context.GenericToken,
TokenTypeHint = context.GenericTokenTypeHint,
@ -1574,7 +1579,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.IdentityTokenPrincipal is not null || string.IsNullOrEmpty(context.IdentityToken))
if (string.IsNullOrEmpty(context.IdentityToken))
{
return;
}
@ -1584,6 +1589,7 @@ public static partial class OpenIddictServerHandlers
// Don't validate the lifetime of id_tokens used as id_token_hints.
DisableLifetimeValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.Logout,
Principal = context.IdentityTokenPrincipal,
Token = context.IdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -1649,13 +1655,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.RefreshTokenPrincipal is not null || string.IsNullOrEmpty(context.RefreshToken))
if (string.IsNullOrEmpty(context.RefreshToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.RefreshTokenPrincipal,
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@ -1721,13 +1728,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.UserCodePrincipal is not null || string.IsNullOrEmpty(context.UserCode))
if (string.IsNullOrEmpty(context.UserCode))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.UserCodePrincipal,
Token = context.UserCode,
ValidTokenTypes = { TokenTypeHints.UserCode }
};

250
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -343,6 +343,245 @@ public sealed class OpenIddictValidationBuilder
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
/// <summary>
/// Registers signing credentials.
/// </summary>
/// <param name="credentials">The signing credentials.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCredentials(SigningCredentials credentials)
{
if (credentials is null)
{
throw new ArgumentNullException(nameof(credentials));
}
return Configure(options => options.SigningCredentials.Add(credentials));
}
/// <summary>
/// Registers a signing key.
/// </summary>
/// <param name="key">The security key.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningKey(SecurityKey key)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
// If the signing key is an asymmetric security key, ensure it has a private key.
if (key is AsymmetricSecurityKey asymmetricSecurityKey &&
asymmetricSecurityKey.PrivateKeyStatus is PrivateKeyStatus.DoesNotExist)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0067));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.RsaSha256));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
}
#if SUPPORTS_ECDSA
// Note: ECDSA algorithms are bound to specific curves and must be treated separately.
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha384));
}
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
{
return AddSigningCredentials(new SigningCredentials(key, SecurityAlgorithms.EcdsaSha512));
}
#else
if (key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) ||
key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) ||
key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512))
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069));
}
#endif
throw new InvalidOperationException(SR.GetResourceString(SR.ID0068));
}
/// <summary>
/// Registers a signing certificate.
/// </summary>
/// <param name="certificate">The signing certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(X509Certificate2 certificate)
{
if (certificate is null)
{
throw new ArgumentNullException(nameof(certificate));
}
// If the certificate is a X.509v3 certificate that specifies at least
// one key usage, ensure that the certificate key can be used for signing.
if (certificate.Version >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0070));
}
}
if (!certificate.HasPrivateKey)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0061));
}
return AddSigningKey(new X509SecurityKey(certificate));
}
/// <summary>
/// Registers a signing certificate retrieved from an embedded resource.
/// </summary>
/// <param name="assembly">The assembly containing the certificate.</param>
/// <param name="resource">The name of the embedded resource.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(Assembly assembly, string resource, string? password)
#if SUPPORTS_EPHEMERAL_KEY_SETS
// Note: ephemeral key sets are currently not supported on macOS.
=> AddSigningCertificate(assembly, resource, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
X509KeyStorageFlags.MachineKeySet :
X509KeyStorageFlags.EphemeralKeySet);
#else
=> AddSigningCertificate(assembly, resource, password, X509KeyStorageFlags.MachineKeySet);
#endif
/// <summary>
/// Registers a signing certificate retrieved from an embedded resource.
/// </summary>
/// <param name="assembly">The assembly containing the certificate.</param>
/// <param name="resource">The name of the embedded resource.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <param name="flags">An enumeration of flags indicating how and where to store the private key of the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(
Assembly assembly, string resource,
string? password, X509KeyStorageFlags flags)
{
if (assembly is null)
{
throw new ArgumentNullException(nameof(assembly));
}
if (string.IsNullOrEmpty(resource))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0062), nameof(resource));
}
using var stream = assembly.GetManifestResourceStream(resource) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0064));
return AddSigningCertificate(stream, password, flags);
}
/// <summary>
/// Registers a signing certificate extracted from a stream.
/// </summary>
/// <param name="stream">The stream containing the certificate.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(Stream stream, string? password)
#if SUPPORTS_EPHEMERAL_KEY_SETS
// Note: ephemeral key sets are currently not supported on macOS.
=> AddSigningCertificate(stream, password, RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
X509KeyStorageFlags.MachineKeySet :
X509KeyStorageFlags.EphemeralKeySet);
#else
=> AddSigningCertificate(stream, password, X509KeyStorageFlags.MachineKeySet);
#endif
/// <summary>
/// Registers a signing certificate extracted from a stream.
/// </summary>
/// <param name="stream">The stream containing the certificate.</param>
/// <param name="password">The password used to open the certificate.</param>
/// <param name="flags">
/// An enumeration of flags indicating how and where
/// to store the private key of the certificate.
/// </param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(Stream stream, string? password, X509KeyStorageFlags flags)
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
using var buffer = new MemoryStream();
stream.CopyTo(buffer);
return AddSigningCertificate(new X509Certificate2(buffer.ToArray(), password, flags));
}
/// <summary>
/// Registers a signing certificate retrieved from the X.509 user or machine store.
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(string thumbprint)
{
if (string.IsNullOrEmpty(thumbprint))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0065), nameof(thumbprint));
}
return AddSigningCertificate(
GetCertificate(StoreLocation.CurrentUser, thumbprint) ??
GetCertificate(StoreLocation.LocalMachine, thumbprint) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
static X509Certificate2? GetCertificate(StoreLocation location, string thumbprint)
{
using var store = new X509Store(StoreName.My, location);
store.Open(OpenFlags.ReadOnly);
return store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.SingleOrDefault();
}
}
/// <summary>
/// Registers a signing certificate retrieved from the specified X.509 store.
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <param name="name">The name of the X.509 store.</param>
/// <param name="location">The location of the X.509 store.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder AddSigningCertificate(string thumbprint, StoreName name, StoreLocation location)
{
if (string.IsNullOrEmpty(thumbprint))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0065), nameof(thumbprint));
}
using var store = new X509Store(name, location);
store.Open(OpenFlags.ReadOnly);
return AddSigningCertificate(
store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, validOnly: false)
.OfType<X509Certificate2>()
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
/// <summary>
/// Registers the specified values as valid audiences. Setting the audiences is recommended
/// when the authorization server issues access tokens for multiple distinct resource servers.
@ -384,6 +623,17 @@ public sealed class OpenIddictValidationBuilder
public OpenIddictValidationBuilder EnableTokenEntryValidation()
=> Configure(options => options.EnableTokenEntryValidation = true);
/// <summary>
/// Sets the client assertion lifetime, after which backchannel requests
/// using an expired client assertion should be automatically rejected by the server.
/// Using long-lived client assertion or assertions that never expire is not recommended.
/// While discouraged, <see langword="null"/> can be specified to issue assertions that never expire.
/// </summary>
/// <param name="lifetime">The access token lifetime.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/> instance.</returns>
public OpenIddictValidationBuilder SetClientAssertionLifetime(TimeSpan? lifetime)
=> Configure(options => options.ClientAssertionLifetime = lifetime);
/// <summary>
/// Sets a static OpenID Connect server configuration, that will be used to
/// resolve the metadata/introspection endpoints and the issuer signing keys.

31
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -74,7 +74,7 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
throw new InvalidOperationException(SR.GetResourceString(SR.ID0131));
}
if (string.IsNullOrEmpty(options.ClientSecret))
if (options.SigningCredentials.Count is 0 && string.IsNullOrEmpty(options.ClientSecret))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0132));
}
@ -138,9 +138,38 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
// Sort the handlers collection using the order associated with each handler.
options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order));
// Sort the encryption and signing credentials.
options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key));
options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key));
// Attach the encryption credentials to the token validation parameters.
options.TokenValidationParameters.TokenDecryptionKeys =
from credentials in options.EncryptionCredentials
select credentials.Key;
static int Compare(SecurityKey left, SecurityKey right) => (left, right) switch
{
// If the two keys refer to the same instances, return 0.
(SecurityKey first, SecurityKey second) when ReferenceEquals(first, second) => 0,
// If one of the keys is a symmetric key, prefer it to the other one.
(SymmetricSecurityKey, SymmetricSecurityKey) => 0,
(SymmetricSecurityKey, SecurityKey) => -1,
(SecurityKey, SymmetricSecurityKey) => 1,
// If one of the keys is backed by a X.509 certificate, don't prefer it if it's not valid yet.
(X509SecurityKey first, SecurityKey) when first.Certificate.NotBefore > DateTime.Now => 1,
(SecurityKey, X509SecurityKey second) when second.Certificate.NotBefore > DateTime.Now => 1,
// If the two keys are backed by a X.509 certificate, prefer the one with the furthest expiration date.
(X509SecurityKey first, X509SecurityKey second) => -first.Certificate.NotAfter.CompareTo(second.Certificate.NotAfter),
// If one of the keys is backed by a X.509 certificate, prefer the X.509 security key.
(X509SecurityKey, SecurityKey) => -1,
(SecurityKey, X509SecurityKey) => 1,
// If the two keys are not backed by a X.509 certificate, none should be preferred to the other.
(SecurityKey, SecurityKey) => 0
};
}
}

12
src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs

@ -36,12 +36,20 @@ public static partial class OpenIddictValidationEvents
/// <summary>
/// Gets or sets the token sent to the introspection endpoint.
/// </summary>
public string? Token { get; set; }
public string? Token
{
get => Request.Token;
set => Request.Token = value;
}
/// <summary>
/// Gets or sets the token type sent to the introspection endpoint.
/// </summary>
public string? TokenTypeHint { get; set; }
public string? TokenTypeHint
{
get => Request.TokenTypeHint;
set => Request.TokenTypeHint = value;
}
}
/// <summary>

76
src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs

@ -12,6 +12,82 @@ namespace OpenIddict.Validation;
public static partial class OpenIddictValidationEvents
{
/// <summary>
/// Represents an event called when generating a token.
/// </summary>
public sealed class GenerateTokenContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="GenerateTokenContext"/> class.
/// </summary>
public GenerateTokenContext(OpenIddictValidationTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request, or <see langword="null"/> if it is not available.
/// </summary>
public OpenIddictRequest? Request
{
get => Transaction.Request;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets a boolean indicating whether a token entry
/// should be created to persist token metadata in a database.
/// </summary>
public bool CreateTokenEntry { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a reference token should be used
/// and, if applicable, returned to the caller instead of the actual token payload.
/// </summary>
public bool IsReferenceToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the token payload
/// should be persisted alongside the token metadata in the database.
/// </summary>
public bool PersistTokenPayload { get; set; }
/// <summary>
/// Gets or sets the security principal used to create the token.
/// </summary>
public ClaimsPrincipal Principal { get; set; } = default!;
/// <summary>
/// Gets or sets the encryption credentials used to encrypt the token.
/// </summary>
public EncryptingCredentials? EncryptionCredentials { get; set; }
/// <summary>
/// Gets or sets the signing credentials used to sign the token.
/// </summary>
public SigningCredentials? SigningCredentials { get; set; }
/// <summary>
/// Gets or sets the security token handler used to serialize the security principal.
/// </summary>
public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!;
/// <summary>
/// Gets or sets the token returned to the client application.
/// </summary>
public string? Token { get; set; }
/// <summary>
/// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to create.
/// </summary>
public string TokenFormat { get; set; } = default!;
/// <summary>
/// Gets or sets the type of the token to create.
/// </summary>
public string TokenType { get; set; } = default!;
}
/// <summary>
/// Represents an event called when validating a token.
/// </summary>

58
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -288,6 +288,16 @@ public static partial class OpenIddictValidationEvents
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the URI of the introspection endpoint, if applicable.
/// </summary>
public Uri? IntrospectionEndpoint { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an introspection request should be sent.
/// </summary>
public bool SendIntrospectionRequest { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the access token, if applicable.
/// </summary>
@ -333,6 +343,54 @@ public static partial class OpenIddictValidationEvents
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectAccessToken { get; set; }
/// <summary>
/// Gets or sets the request sent to the introspection endpoint, if applicable.
/// </summary>
public OpenIddictRequest? IntrospectionRequest { get; set; }
/// <summary>
/// Gets or sets the response returned by the introspection endpoint, if applicable.
/// </summary>
public OpenIddictResponse? IntrospectionResponse { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a client assertion
/// token should be generated (and optionally included in the request).
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool GenerateClientAssertion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated client
/// assertion should be included as part of the request.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool IncludeClientAssertion { get; set; }
/// <summary>
/// Gets or sets the generated client assertion, if applicable.
/// The client assertion will only be returned if
/// <see cref="IncludeClientAssertion"/> is set to <see langword="true"/>.
/// </summary>
public string? ClientAssertion { get; set; }
/// <summary>
/// Gets or sets type of the generated client assertion, if applicable.
/// The client assertion type will only be returned if
/// <see cref="IncludeClientAssertion"/> is set to <see langword="true"/>.
/// </summary>
public string? ClientAssertionType { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that will be
/// used to create the client assertion, if applicable.
/// </summary>
public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
}
/// <summary>

4
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -45,10 +45,12 @@ public static class OpenIddictValidationExtensions
builder.Services.TryAddSingleton<RequireAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
builder.Services.TryAddSingleton<RequireClientAssertionGenerated>();
builder.Services.TryAddSingleton<RequireIntrospectionRequest>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireLocalValidation>();
builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireIntrospectionValidation>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

48
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -80,26 +80,60 @@ public static class OpenIddictValidationHandlerFilters
}
/// <summary>
/// Represents a filter that excludes the associated handlers if local validation is not used.
/// Represents a filter that excludes the associated handlers if no client assertion is generated.
/// </summary>
public sealed class RequireLocalValidation : IOpenIddictValidationHandlerFilter<BaseContext>
public sealed class RequireClientAssertionGenerated : IOpenIddictValidationHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.Options.ValidationType is OpenIddictValidationType.Direct);
return new(context.GenerateClientAssertion);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if introspection is not used.
/// Represents a filter that excludes the associated handlers if no introspection request is expected to be sent.
/// </summary>
public sealed class RequireIntrospectionValidation : IOpenIddictValidationHandlerFilter<BaseContext>
public sealed class RequireIntrospectionRequest : IOpenIddictValidationHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.SendIntrospectionRequest);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
/// </summary>
public sealed class RequireJsonWebTokenFormat : IOpenIddictValidationHandlerFilter<GenerateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.TokenFormat is TokenFormats.Jwt);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if local validation is not used.
/// </summary>
public sealed class RequireLocalValidation : IOpenIddictValidationHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
@ -109,7 +143,7 @@ public static class OpenIddictValidationHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new(context.Options.ValidationType is OpenIddictValidationType.Introspection);
return new(context.Options.ValidationType is OpenIddictValidationType.Direct);
}
}

66
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -16,12 +16,6 @@ public static partial class OpenIddictValidationHandlers
public static class Introspection
{
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Introspection response handling:
*/
AttachCredentials.Descriptor,
AttachToken.Descriptor,
/*
* Introspection response handling:
*/
@ -32,66 +26,6 @@ public static partial class OpenIddictValidationHandlers
ValidateTokenUsage.Descriptor,
PopulateClaims.Descriptor);
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the introspection request.
/// </summary>
public sealed 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)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
{
if (context is 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 for attaching the token to the introspection request.
/// </summary>
public sealed class AttachToken : IOpenIddictValidationHandler<PrepareIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>()
.UseSingletonHandler<AttachToken>()
.SetOrder(AttachCredentials.Descriptor.Order + 100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.Token = context.Token;
context.Request.TokenTypeHint = context.TokenTypeHint;
return default;
}
}
/// <summary>
/// Contains the logic responsible for validating the well-known parameters contained in the introspection response.
/// </summary>

236
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -10,7 +10,6 @@ using System.Globalization;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.Validation;
@ -25,7 +24,6 @@ public static partial class OpenIddictValidationHandlers
ResolveTokenValidationParameters.Descriptor,
ValidateReferenceTokenIdentifier.Descriptor,
ValidateIdentityModelToken.Descriptor,
IntrospectToken.Descriptor,
NormalizeScopeClaims.Descriptor,
MapInternalClaims.Descriptor,
RestoreTokenEntryProperties.Descriptor,
@ -33,7 +31,13 @@ public static partial class OpenIddictValidationHandlers
ValidateExpirationDate.Descriptor,
ValidateAudience.Descriptor,
ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor);
ValidateAuthorizationEntry.Descriptor,
/*
* Token generation:
*/
AttachSecurityCredentials.Descriptor,
GenerateIdentityModelToken.Descriptor);
/// <summary>
/// Contains the logic responsible for resolving the validation parameters used to validate tokens.
@ -300,107 +304,6 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the tokens using OAuth 2.0 introspection.
/// </summary>
public sealed class IntrospectToken : IOpenIddictValidationHandler<ValidateTokenContext>
{
private readonly OpenIddictValidationService _service;
public IntrospectToken(OpenIddictValidationService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireIntrospectionValidation>()
.UseSingletonHandler<IntrospectToken>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal is not null)
{
return;
}
Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010));
// Ensure the introspection endpoint is present and is a valid absolute URI.
if (context.Configuration.IntrospectionEndpoint is not { IsAbsoluteUri: true } ||
!context.Configuration.IntrospectionEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.IntrospectionEndpoint));
}
ClaimsPrincipal principal;
try
{
principal = await _service.IntrospectTokenAsync(context.Configuration.IntrospectionEndpoint,
context.Token, context.ValidTokenTypes.Count switch
{
// Infer the token type hint sent to the authorization server to help speed up
// the token resolution lookup. If multiple types are accepted, no hint is sent.
1 => context.ValidTokenTypes.ElementAt(0),
_ => null
}) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0141));
}
catch (ProtocolException exception)
{
context.Logger.LogDebug(exception, SR.GetResourceString(SR.ID6155));
context.Reject(
error: exception.Error,
description: exception.ErrorDescription,
uri: exception.ErrorUri);
return;
}
// 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 one of the expected types and prevents token substitution attacks.
//
// If a "token_usage" claim can be extracted from the principal, use it to determine
// whether the token details returned by the authorization server correspond to a
// token whose type is considered acceptable based on the valid types collection.
//
// If the valid types collection is empty, all types of tokens are considered valid.
var usage = principal.GetClaim(Claims.TokenUsage);
if (!string.IsNullOrEmpty(usage) && context.ValidTokenTypes.Count > 0 &&
!context.ValidTokenTypes.Contains(usage))
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2110),
uri: SR.FormatID8000(SR.ID2110));
return;
}
// Note: at this point, the "token_usage" claim value is guaranteed to correspond
// to a known value as it is checked when validating the introspection response.
//
// If no value could be resolved, the token is assumed to be an access token.
context.Principal = principal.SetTokenType(usage ?? TokenTypeHints.AccessToken);
context.Logger.LogTrace(SR.GetResourceString(SR.ID6154), context.Token, context.Principal.Claims);
}
}
/// <summary>
/// Contains the logic responsible for normalizing the scope claims stored in the tokens.
/// </summary>
@ -412,7 +315,7 @@ public static partial class OpenIddictValidationHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<NormalizeScopeClaims>()
.SetOrder(IntrospectToken.Descriptor.Order + 1_000)
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -884,5 +787,128 @@ public static partial class OpenIddictValidationHandlers
}
}
}
/// <summary>
/// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens.
/// </summary>
public sealed class AttachSecurityCredentials : IOpenIddictValidationHandler<GenerateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.UseSingletonHandler<AttachSecurityCredentials>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.SecurityTokenHandler = context.Options.JsonWebTokenHandler;
context.SigningCredentials = context.Options.SigningCredentials.First();
return default;
}
}
/// <summary>
/// Contains the logic responsible for generating a token using IdentityModel.
/// </summary>
public sealed class GenerateIdentityModelToken : IOpenIddictValidationHandler<GenerateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.AddFilter<RequireJsonWebTokenFormat>()
.UseSingletonHandler<GenerateIdentityModelToken>()
.SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If a token was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Token))
{
return default;
}
if (context.Principal is not { Identity: ClaimsIdentity })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
}
// Clone the principal and exclude the private claims mapped to standard JWT claims.
var principal = context.Principal.Clone(claim => claim.Type switch
{
Claims.Private.CreationDate or Claims.Private.ExpirationDate or
Claims.Private.Issuer or Claims.Private.TokenType => false,
Claims.Private.Audience when context.TokenType is TokenTypeHints.ClientAssertion => false,
_ => true
});
Debug.Assert(principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var claims = new Dictionary<string, object>(StringComparer.Ordinal);
// For client assertions, set the public audience claims
// using the private audience claims from the security principal.
if (context.TokenType is TokenTypeHints.ClientAssertion)
{
var audiences = context.Principal.GetAudiences();
if (audiences.Any())
{
claims.Add(Claims.Audience, audiences.Length switch
{
1 => audiences.ElementAt(0),
_ => audiences
});
}
}
var descriptor = new SecurityTokenDescriptor
{
Claims = claims,
EncryptingCredentials = context.EncryptionCredentials,
Expires = context.Principal.GetExpirationDate()?.UtcDateTime,
IssuedAt = context.Principal.GetCreationDate()?.UtcDateTime,
Issuer = context.Principal.GetClaim(Claims.Private.Issuer),
SigningCredentials = context.SigningCredentials,
Subject = (ClaimsIdentity) principal.Identity,
TokenType = context.TokenType switch
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
// For client assertions, use the generic "JWT" type.
TokenTypeHints.ClientAssertion => JsonWebTokenTypes.Jwt,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
}
};
context.Token = context.SecurityTokenHandler.CreateToken(descriptor);
context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType, context.Token, principal.Claims);
return default;
}
}
}
}

445
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -6,8 +6,12 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.Validation;
@ -21,6 +25,15 @@ public static partial class OpenIddictValidationHandlers
EvaluateValidatedTokens.Descriptor,
ValidateRequiredTokens.Descriptor,
ResolveServerConfiguration.Descriptor,
ResolveIntrospectionEndpoint.Descriptor,
EvaluateIntrospectionRequest.Descriptor,
AttachIntrospectionRequestParameters.Descriptor,
EvaluateGeneratedClientAssertion.Descriptor,
PrepareClientAssertionPrincipal.Descriptor,
GenerateClientAssertion.Descriptor,
AttachIntrospectionRequestClientCredentials.Descriptor,
SendIntrospectionRequest.Descriptor,
ValidateIntrospectedTokenUsage.Descriptor,
ValidateAccessToken.Descriptor,
/*
@ -169,6 +182,431 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the URI of the introspection endpoint.
/// </summary>
public sealed class ResolveIntrospectionEndpoint : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ResolveIntrospectionEndpoint>()
.SetOrder(ResolveServerConfiguration.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If the URI of the introspection endpoint wasn't explicitly set
// at this stage, try to extract it from the server configuration.
context.IntrospectionEndpoint ??= context.Configuration.IntrospectionEndpoint switch
{
{ IsAbsoluteUri: true } uri when uri.IsWellFormedOriginalString() => uri,
_ => null
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for determining whether an introspection request should be sent.
/// </summary>
public sealed class EvaluateIntrospectionRequest : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<EvaluateIntrospectionRequest>()
.SetOrder(ResolveIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.SendIntrospectionRequest = context.Options.ValidationType is OpenIddictValidationType.Introspection;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters to the introspection request, if applicable.
/// </summary>
public sealed class AttachIntrospectionRequestParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachIntrospectionRequestParameters>()
.SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Attach a new request instance if necessary.
context.IntrospectionRequest ??= new OpenIddictRequest();
context.IntrospectionRequest.Token = context.AccessToken;
context.IntrospectionRequest.TokenTypeHint = TokenTypeHints.AccessToken;
return default;
}
}
/// <summary>
/// Contains the logic responsible for selecting the token types that should
/// be generated and optionally sent as part of the authentication demand.
/// </summary>
public sealed class EvaluateGeneratedClientAssertion : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<EvaluateGeneratedClientAssertion>()
.SetOrder(AttachIntrospectionRequestParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
(context.GenerateClientAssertion,
context.IncludeClientAssertion) = context.Options.SigningCredentials.Count switch
{
// If a introspection request is going to be sent and if at least one signing key
// was attached to the validation options, generate and include a client assertion
// token if the configuration indicates the server supports private_key_jwt.
> 0 when context.Configuration.IntrospectionEndpointAuthMethodsSupported.Contains(
ClientAuthenticationMethods.PrivateKeyJwt) => (true, true),
_ => (false, false)
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the client assertion, if one is going to be sent.
/// </summary>
public sealed class PrepareClientAssertionPrincipal : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionGenerated>()
.UseSingletonHandler<PrepareClientAssertionPrincipal>()
.SetOrder(EvaluateGeneratedClientAssertion.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Configuration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// Create a new principal that will be used to store the client assertion claims.
var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType));
principal.SetCreationDate(DateTimeOffset.UtcNow);
var lifetime = context.Options.ClientAssertionLifetime;
if (lifetime.HasValue)
{
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
}
// Use the issuer URI as the audience. Applications that need to
// use a different value can register a custom event handler.
principal.SetAudiences(context.Configuration.Issuer.OriginalString);
// Use the client_id as both the subject and the issuer, as required by the specifications.
principal.SetClaim(Claims.Private.Issuer, context.Options.ClientId)
.SetClaim(Claims.Subject, context.Options.ClientId);
// Use a random GUID as the JWT unique identifier.
principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString());
context.ClientAssertionPrincipal = principal;
return default;
}
}
/// <summary>
/// Contains the logic responsible for generating a client
/// assertion for the current authentication operation.
/// </summary>
public sealed class GenerateClientAssertion : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictValidationDispatcher _dispatcher;
public GenerateClientAssertion(IOpenIddictValidationDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionGenerated>()
.UseScopedHandler<GenerateClientAssertion>()
.SetOrder(PrepareClientAssertionPrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new GenerateTokenContext(context.Transaction)
{
CreateTokenEntry = false,
IsReferenceToken = false,
PersistTokenPayload = false,
Principal = context.ClientAssertionPrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.ClientAssertion
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
context.ClientAssertion = notification.Token;
context.ClientAssertionType = notification.TokenFormat switch
{
TokenFormats.Jwt => ClientAssertionTypes.JwtBearer,
TokenFormats.Saml2 => ClientAssertionTypes.Saml2Bearer,
_ => null
};
}
}
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the introspection request, if applicable.
/// </summary>
public sealed class AttachIntrospectionRequestClientCredentials : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachIntrospectionRequestClientCredentials>()
.SetOrder(GenerateClientAssertion.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.IntrospectionRequest is not null, SR.GetResourceString(SR.ID4008));
// Always attach the client_id to the request, even if an assertion is sent.
context.IntrospectionRequest.ClientId = context.Options.ClientId;
// Note: client authentication methods are mutually exclusive so the client_assertion
// and client_secret parameters MUST never be sent at the same time. For more information,
// see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.
if (context.IncludeClientAssertion)
{
context.IntrospectionRequest.ClientAssertion = context.ClientAssertion;
context.IntrospectionRequest.ClientAssertionType = context.ClientAssertionType;
}
// Note: the client_secret may be null at this point (e.g for a public
// client or if a custom authentication method is used by the application).
else
{
context.IntrospectionRequest.ClientSecret = context.Options.ClientSecret;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for sending the introspection request, if applicable.
/// </summary>
public sealed class SendIntrospectionRequest : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly OpenIddictValidationService _service;
public SendIntrospectionRequest(OpenIddictValidationService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<SendIntrospectionRequest>()
.SetOrder(AttachIntrospectionRequestClientCredentials.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.IntrospectionRequest is not null, SR.GetResourceString(SR.ID4008));
// Ensure the introspection endpoint is present and is a valid absolute URI.
if (context.IntrospectionEndpoint is not { IsAbsoluteUri: true } ||
!context.IntrospectionEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.IntrospectionEndpoint));
}
try
{
(context.IntrospectionResponse, context.AccessTokenPrincipal) =
await _service.SendIntrospectionRequestAsync(
context.Configuration, context.IntrospectionRequest,
context.IntrospectionEndpoint);
}
catch (ProtocolException exception)
{
context.Logger.LogDebug(exception, SR.GetResourceString(SR.ID6155));
context.Reject(
error: exception.Error,
description: exception.ErrorDescription,
uri: exception.ErrorUri);
return;
}
context.Logger.LogTrace(SR.GetResourceString(SR.ID6154), context.AccessToken, context.AccessTokenPrincipal.Claims);
}
}
/// <summary>
/// Contains the logic responsible for validating the usage of the introspected token returned by the server, if applicable.
/// </summary>
public sealed class ValidateIntrospectedTokenUsage : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<ValidateIntrospectedTokenUsage>()
.SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// 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 one of the expected types and prevents token substitution attacks.
//
// If a "token_usage" claim can be extracted from the principal, use it to determine whether
// the token details returned by the authorization server correspond to an access token.
var usage = context.AccessTokenPrincipal.GetClaim(Claims.TokenUsage);
if (!string.IsNullOrEmpty(usage) && usage is not TokenTypeHints.AccessToken)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2110),
uri: SR.FormatID8000(SR.ID2110));
return default;
}
// Note: if no token usage could be resolved, the token is assumed to be an access token.
context.AccessTokenPrincipal = context.AccessTokenPrincipal.SetTokenType(usage ?? TokenTypeHints.AccessToken);
return default;
}
}
/// <summary>
/// Contains the logic responsible for ensuring a token was correctly resolved from the context.
/// </summary>
@ -186,7 +624,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAccessTokenValidated>()
.UseScopedHandler<ValidateAccessToken>()
.SetOrder(ResolveServerConfiguration.Descriptor.Order + 1_000)
.SetOrder(ValidateIntrospectedTokenUsage.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -198,13 +636,16 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.AccessTokenPrincipal is not null || string.IsNullOrEmpty(context.AccessToken))
if (string.IsNullOrEmpty(context.AccessToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
// When using introspection, the principal is already available as it is extracted
// from the introspection response returned by the authorization server.
Principal = context.AccessTokenPrincipal,
Token = context.AccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};

24
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -31,6 +31,30 @@ public sealed class OpenIddictValidationOptions
/// </remarks>
public List<EncryptingCredentials> EncryptionCredentials { get; } = new();
/// <summary>
/// Gets the list of signing credentials used by the OpenIddict validation services.
/// Multiple credentials can be added to support key rollover, but if X.509 keys
/// are used, at least one of them must have a valid creation/expiration date.
/// Note: the signing credentials are not used to protect/unprotect tokens issued
/// by ASP.NET Core Data Protection, that uses its own key ring, configured separately.
/// </summary>
/// <remarks>
/// Note: OpenIddict automatically sorts the credentials based on the following algorithm:
/// <list type="bullet">
/// <item><description>Symmetric keys are always preferred when they can be used for the operation (e.g token signing).</description></item>
/// <item><description>X.509 keys are always preferred to non-X.509 asymmetric keys.</description></item>
/// <item><description>X.509 keys with the furthest expiration date are preferred.</description></item>
/// <item><description>X.509 keys whose backing certificate is not yet valid are never preferred.</description></item>
/// </list>
/// </remarks>
public List<SigningCredentials> SigningCredentials { get; } = new();
/// <summary>
/// Gets or sets the period of time client assertions remain valid after being issued. The default value is 5 minutes.
/// While not recommended, this property can be set to <see langword="null"/> to issue client assertions that never expire.
/// </summary>
public TimeSpan? ClientAssertionLifetime { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets or sets the JWT handler used to protect and unprotect tokens.
/// </summary>

68
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -382,16 +382,27 @@ public sealed class OpenIddictValidationService
}
/// <summary>
/// Sends an introspection request to the specified URI and returns the corresponding principal.
/// Sends the introspection request and retrieves the corresponding response.
/// </summary>
/// <param name="uri">The URI of the remote metadata endpoint.</param>
/// <param name="token">The token to introspect.</param>
/// <param name="hint">The token type to introspect, used as a hint by the authorization server.</param>
/// <param name="configuration">The server configuration.</param>
/// <param name="request">The token request.</param>
/// <param name="uri">The uri of the remote token endpoint.</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>
internal async ValueTask<ClaimsPrincipal> IntrospectTokenAsync(
Uri uri, string token, string? hint, CancellationToken cancellationToken = default)
/// <returns>The response and the principal extracted from the introspection response.</returns>
internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync(
OpenIddictConfiguration configuration, OpenIddictRequest request,
Uri? uri = null, CancellationToken cancellationToken = default)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (uri is null)
{
throw new ArgumentNullException(nameof(uri));
@ -402,11 +413,6 @@ public sealed class OpenIddictValidationService
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri));
}
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0156), nameof(token));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
@ -418,33 +424,25 @@ public sealed class OpenIddictValidationService
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
var configuration = await options.CurrentValue.ConfigurationManager
.GetConfigurationAsync(cancellationToken)
.WaitAsync(cancellationToken) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync();
var request = new OpenIddictRequest();
request = await PrepareIntrospectionRequestAsync();
request = await ApplyIntrospectionRequestAsync();
var response = await ExtractIntrospectionResponseAsync();
return await HandleIntrospectionResponseAsync() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0157));
return await HandleIntrospectionResponseAsync();
async ValueTask<OpenIddictRequest> PrepareIntrospectionRequestAsync()
{
var context = new PrepareIntrospectionRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Request = request,
Token = token,
TokenTypeHint = hint
Request = request
};
await dispatcher.DispatchAsync(context);
@ -452,7 +450,7 @@ public sealed class OpenIddictValidationService
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0158(context.Error, context.ErrorDescription, context.ErrorUri),
SR.FormatID0320(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
@ -463,6 +461,7 @@ public sealed class OpenIddictValidationService
{
var context = new ApplyIntrospectionRequestContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Request = request
@ -473,11 +472,11 @@ public sealed class OpenIddictValidationService
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0159(context.Error, context.ErrorDescription, context.ErrorUri),
SR.FormatID0321(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6190), context.RemoteUri, context.Request);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6192), context.RemoteUri, context.Request);
return context.Request;
}
@ -486,6 +485,7 @@ public sealed class OpenIddictValidationService
{
var context = new ExtractIntrospectionResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Request = request
@ -496,26 +496,26 @@ public sealed class OpenIddictValidationService
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0160(context.Error, context.ErrorDescription, context.ErrorUri),
SR.FormatID0322(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007));
context.Logger.LogInformation(SR.GetResourceString(SR.ID6191), context.RemoteUri, context.Response);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6193), context.RemoteUri, context.Response);
return context.Response;
}
async ValueTask<ClaimsPrincipal> HandleIntrospectionResponseAsync()
async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> HandleIntrospectionResponseAsync()
{
var context = new HandleIntrospectionResponseContext(transaction)
{
CancellationToken = cancellationToken,
RemoteUri = uri,
Configuration = configuration,
Request = request,
Response = response,
Token = token
Response = response
};
await dispatcher.DispatchAsync(context);
@ -523,13 +523,13 @@ public sealed class OpenIddictValidationService
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0161(context.Error, context.ErrorDescription, context.ErrorUri),
SR.FormatID0323(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
return context.Principal;
return (context.Response, context.Principal);
}
}

Loading…
Cancel
Save