diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 77eac58a..9fc60a72 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -562,7 +562,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A
The client identifier cannot be null or empty when using introspection.
- The client secret cannot be null or empty when using introspection.
+ 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.
Authorization entry validation cannot be enabled when using introspection.
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index 85fa2a61..03f0e652 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/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 }
};
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 55a10146..0a598771 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/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 }
};
diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
index 189bb0ec..89e96533 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
@@ -343,6 +343,245 @@ public sealed class OpenIddictValidationBuilder
.SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
}
+ ///
+ /// Registers signing credentials.
+ ///
+ /// The signing credentials.
+ /// The instance.
+ public OpenIddictValidationBuilder AddSigningCredentials(SigningCredentials credentials)
+ {
+ if (credentials is null)
+ {
+ throw new ArgumentNullException(nameof(credentials));
+ }
+
+ return Configure(options => options.SigningCredentials.Add(credentials));
+ }
+
+ ///
+ /// Registers a signing key.
+ ///
+ /// The security key.
+ /// The instance.
+ 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));
+ }
+
+ ///
+ /// Registers a signing certificate.
+ ///
+ /// The signing certificate.
+ /// The instance.
+ 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().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));
+ }
+
+ ///
+ /// Registers a signing certificate retrieved from an embedded resource.
+ ///
+ /// The assembly containing the certificate.
+ /// The name of the embedded resource.
+ /// The password used to open the certificate.
+ /// The instance.
+ 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
+
+ ///
+ /// Registers a signing certificate retrieved from an embedded resource.
+ ///
+ /// The assembly containing the certificate.
+ /// The name of the embedded resource.
+ /// The password used to open the certificate.
+ /// An enumeration of flags indicating how and where to store the private key of the certificate.
+ /// The instance.
+ 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);
+ }
+
+ ///
+ /// Registers a signing certificate extracted from a stream.
+ ///
+ /// The stream containing the certificate.
+ /// The password used to open the certificate.
+ /// The instance.
+ 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
+
+ ///
+ /// Registers a signing certificate extracted from a stream.
+ ///
+ /// The stream containing the certificate.
+ /// The password used to open the certificate.
+ ///
+ /// An enumeration of flags indicating how and where
+ /// to store the private key of the certificate.
+ ///
+ /// The instance.
+ 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));
+ }
+
+ ///
+ /// Registers a signing certificate retrieved from the X.509 user or machine store.
+ ///
+ /// The thumbprint of the certificate used to identify it in the X.509 store.
+ /// The instance.
+ 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()
+ .SingleOrDefault();
+ }
+ }
+
+ ///
+ /// Registers a signing certificate retrieved from the specified X.509 store.
+ ///
+ /// The thumbprint of the certificate used to identify it in the X.509 store.
+ /// The name of the X.509 store.
+ /// The location of the X.509 store.
+ /// The instance.
+ 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()
+ .SingleOrDefault() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0066)));
+ }
+
///
/// 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);
+ ///
+ /// 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, can be specified to issue assertions that never expire.
+ ///
+ /// The access token lifetime.
+ /// The instance.
+ public OpenIddictValidationBuilder SetClientAssertionLifetime(TimeSpan? lifetime)
+ => Configure(options => options.ClientAssertionLifetime = lifetime);
+
///
/// Sets a static OpenID Connect server configuration, that will be used to
/// resolve the metadata/introspection endpoints and the issuer signing keys.
diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
index fa28dac7..2c442173 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
@@ -74,7 +74,7 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions 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
+ };
}
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs
index 9e8ad3bf..8522de9e 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs
@@ -36,12 +36,20 @@ public static partial class OpenIddictValidationEvents
///
/// Gets or sets the token sent to the introspection endpoint.
///
- public string? Token { get; set; }
+ public string? Token
+ {
+ get => Request.Token;
+ set => Request.Token = value;
+ }
///
/// Gets or sets the token type sent to the introspection endpoint.
///
- public string? TokenTypeHint { get; set; }
+ public string? TokenTypeHint
+ {
+ get => Request.TokenTypeHint;
+ set => Request.TokenTypeHint = value;
+ }
}
///
diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
index 40d207ff..79cc51a3 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
@@ -12,6 +12,82 @@ namespace OpenIddict.Validation;
public static partial class OpenIddictValidationEvents
{
+ ///
+ /// Represents an event called when generating a token.
+ ///
+ public sealed class GenerateTokenContext : BaseValidatingContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public GenerateTokenContext(OpenIddictValidationTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request, or if it is not available.
+ ///
+ public OpenIddictRequest? Request
+ {
+ get => Transaction.Request;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets a boolean indicating whether a token entry
+ /// should be created to persist token metadata in a database.
+ ///
+ public bool CreateTokenEntry { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public bool IsReferenceToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the token payload
+ /// should be persisted alongside the token metadata in the database.
+ ///
+ public bool PersistTokenPayload { get; set; }
+
+ ///
+ /// Gets or sets the security principal used to create the token.
+ ///
+ public ClaimsPrincipal Principal { get; set; } = default!;
+
+ ///
+ /// Gets or sets the encryption credentials used to encrypt the token.
+ ///
+ public EncryptingCredentials? EncryptionCredentials { get; set; }
+
+ ///
+ /// Gets or sets the signing credentials used to sign the token.
+ ///
+ public SigningCredentials? SigningCredentials { get; set; }
+
+ ///
+ /// Gets or sets the security token handler used to serialize the security principal.
+ ///
+ public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!;
+
+ ///
+ /// Gets or sets the token returned to the client application.
+ ///
+ public string? Token { get; set; }
+
+ ///
+ /// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to create.
+ ///
+ public string TokenFormat { get; set; } = default!;
+
+ ///
+ /// Gets or sets the type of the token to create.
+ ///
+ public string TokenType { get; set; } = default!;
+ }
+
///
/// Represents an event called when validating a token.
///
diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs
index ccebfb28..ed8096ee 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs
@@ -288,6 +288,16 @@ public static partial class OpenIddictValidationEvents
set => Transaction.Request = value;
}
+ ///
+ /// Gets or sets the URI of the introspection endpoint, if applicable.
+ ///
+ public Uri? IntrospectionEndpoint { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an introspection request should be sent.
+ ///
+ public bool SendIntrospectionRequest { get; set; }
+
///
/// Gets or sets the principal extracted from the access token, if applicable.
///
@@ -333,6 +343,54 @@ public static partial class OpenIddictValidationEvents
/// Note: overriding the value of this property is generally not recommended.
///
public bool RejectAccessToken { get; set; }
+
+ ///
+ /// Gets or sets the request sent to the introspection endpoint, if applicable.
+ ///
+ public OpenIddictRequest? IntrospectionRequest { get; set; }
+
+ ///
+ /// Gets or sets the response returned by the introspection endpoint, if applicable.
+ ///
+ public OpenIddictResponse? IntrospectionResponse { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a client assertion
+ /// token should be generated (and optionally included in the request).
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool GenerateClientAssertion { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the generated client
+ /// assertion should be included as part of the request.
+ ///
+ ///
+ /// Note: overriding the value of this property is generally not recommended.
+ ///
+ public bool IncludeClientAssertion { get; set; }
+
+ ///
+ /// Gets or sets the generated client assertion, if applicable.
+ /// The client assertion will only be returned if
+ /// is set to .
+ ///
+ public string? ClientAssertion { get; set; }
+
+ ///
+ /// Gets or sets type of the generated client assertion, if applicable.
+ /// The client assertion type will only be returned if
+ /// is set to .
+ ///
+ public string? ClientAssertionType { get; set; }
+
+ ///
+ /// Gets or sets the principal containing the claims that will be
+ /// used to create the client assertion, if applicable.
+ ///
+ public ClaimsPrincipal? ClientAssertionPrincipal { get; set; }
}
///
diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
index 0107debe..e15247e1 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
@@ -45,10 +45,12 @@ public static class OpenIddictValidationExtensions
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
index 726a6dae..833aaa30 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
@@ -80,26 +80,60 @@ public static class OpenIddictValidationHandlerFilters
}
///
- /// 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.
///
- public sealed class RequireLocalValidation : IOpenIddictValidationHandlerFilter
+ public sealed class RequireClientAssertionGenerated : IOpenIddictValidationHandlerFilter
{
///
- public ValueTask IsActiveAsync(BaseContext context)
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
- return new(context.Options.ValidationType is OpenIddictValidationType.Direct);
+ return new(context.GenerateClientAssertion);
}
}
///
- /// 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.
///
- public sealed class RequireIntrospectionValidation : IOpenIddictValidationHandlerFilter
+ public sealed class RequireIntrospectionRequest : IOpenIddictValidationHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.SendIntrospectionRequest);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
+ ///
+ public sealed class RequireJsonWebTokenFormat : IOpenIddictValidationHandlerFilter
+ {
+ ///
+ public ValueTask IsActiveAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.TokenFormat is TokenFormats.Jwt);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if local validation is not used.
+ ///
+ public sealed class RequireLocalValidation : IOpenIddictValidationHandlerFilter
{
///
public ValueTask 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);
}
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
index 5227674b..209888c9 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
@@ -16,12 +16,6 @@ public static partial class OpenIddictValidationHandlers
public static class Introspection
{
public static ImmutableArray 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);
- ///
- /// Contains the logic responsible for attaching the client credentials to the introspection request.
- ///
- public sealed class AttachCredentials : IOpenIddictValidationHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
- = OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
- .SetOrder(int.MinValue + 100_000)
- .SetType(OpenIddictValidationHandlerType.BuiltIn)
- .Build();
-
- ///
- 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;
- }
- }
-
- ///
- /// Contains the logic responsible for attaching the token to the introspection request.
- ///
- public sealed class AttachToken : IOpenIddictValidationHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
- = OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
- .SetOrder(AttachCredentials.Descriptor.Order + 100_000)
- .SetType(OpenIddictValidationHandlerType.BuiltIn)
- .Build();
-
- ///
- 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;
- }
- }
-
///
/// Contains the logic responsible for validating the well-known parameters contained in the introspection response.
///
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
index 44bdd782..74e7a187 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
+++ b/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);
///
/// Contains the logic responsible for resolving the validation parameters used to validate tokens.
@@ -300,107 +304,6 @@ public static partial class OpenIddictValidationHandlers
}
}
- ///
- /// Contains the logic responsible for validating the tokens using OAuth 2.0 introspection.
- ///
- public sealed class IntrospectToken : IOpenIddictValidationHandler
- {
- private readonly OpenIddictValidationService _service;
-
- public IntrospectToken(OpenIddictValidationService service)
- => _service = service ?? throw new ArgumentNullException(nameof(service));
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
- = OpenIddictValidationHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
- .SetType(OpenIddictValidationHandlerType.BuiltIn)
- .Build();
-
- ///
- 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);
- }
- }
-
///
/// Contains the logic responsible for normalizing the scope claims stored in the tokens.
///
@@ -412,7 +315,7 @@ public static partial class OpenIddictValidationHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .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
}
}
}
+
+ ///
+ /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens.
+ ///
+ public sealed class AttachSecurityCredentials : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for generating a token using IdentityModel.
+ ///
+ public sealed class GenerateIdentityModelToken : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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(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;
+ }
+ }
}
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
index 5a79e798..b3fb5f63 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
+++ b/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
}
}
+ ///
+ /// Contains the logic responsible for resolving the URI of the introspection endpoint.
+ ///
+ public sealed class ResolveIntrospectionEndpoint : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ResolveServerConfiguration.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for determining whether an introspection request should be sent.
+ ///
+ public sealed class EvaluateIntrospectionRequest : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ResolveIntrospectionEndpoint.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.SendIntrospectionRequest = context.Options.ValidationType is OpenIddictValidationType.Introspection;
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for attaching the parameters to the introspection request, if applicable.
+ ///
+ public sealed class AttachIntrospectionRequestParameters : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for selecting the token types that should
+ /// be generated and optionally sent as part of the authentication demand.
+ ///
+ public sealed class EvaluateGeneratedClientAssertion : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachIntrospectionRequestParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for preparing and attaching the claims principal
+ /// used to generate the client assertion, if one is going to be sent.
+ ///
+ public sealed class PrepareClientAssertionPrincipal : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(EvaluateGeneratedClientAssertion.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for generating a client
+ /// assertion for the current authentication operation.
+ ///
+ public sealed class GenerateClientAssertion : IOpenIddictValidationHandler
+ {
+ private readonly IOpenIddictValidationDispatcher _dispatcher;
+
+ public GenerateClientAssertion(IOpenIddictValidationDispatcher dispatcher)
+ => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(PrepareClientAssertionPrincipal.Descriptor.Order + 1_000)
+ .SetType(OpenIddictValidationHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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
+ };
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for attaching the client credentials to the introspection request, if applicable.
+ ///
+ public sealed class AttachIntrospectionRequestClientCredentials : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(GenerateClientAssertion.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for sending the introspection request, if applicable.
+ ///
+ public sealed class SendIntrospectionRequest : IOpenIddictValidationHandler
+ {
+ private readonly OpenIddictValidationService _service;
+
+ public SendIntrospectionRequest(OpenIddictValidationService service)
+ => _service = service ?? throw new ArgumentNullException(nameof(service));
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachIntrospectionRequestClientCredentials.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Contains the logic responsible for validating the usage of the introspected token returned by the server, if applicable.
+ ///
+ public sealed class ValidateIntrospectedTokenUsage : IOpenIddictValidationHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
+ = OpenIddictValidationHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ 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;
+ }
+ }
+
///
/// Contains the logic responsible for ensuring a token was correctly resolved from the context.
///
@@ -186,7 +624,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder()
.AddFilter()
.UseScopedHandler()
- .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 }
};
diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
index 35b14532..81fca6e3 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
@@ -31,6 +31,30 @@ public sealed class OpenIddictValidationOptions
///
public List EncryptionCredentials { get; } = new();
+ ///
+ /// 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.
+ ///
+ ///
+ /// Note: OpenIddict automatically sorts the credentials based on the following algorithm:
+ ///
+ /// - Symmetric keys are always preferred when they can be used for the operation (e.g token signing).
+ /// - X.509 keys are always preferred to non-X.509 asymmetric keys.
+ /// - X.509 keys with the furthest expiration date are preferred.
+ /// - X.509 keys whose backing certificate is not yet valid are never preferred.
+ ///
+ ///
+ public List SigningCredentials { get; } = new();
+
+ ///
+ /// 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 to issue client assertions that never expire.
+ ///
+ public TimeSpan? ClientAssertionLifetime { get; set; } = TimeSpan.FromMinutes(5);
+
///
/// Gets or sets the JWT handler used to protect and unprotect tokens.
///
diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs
index 6d943706..843950d9 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationService.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs
@@ -382,16 +382,27 @@ public sealed class OpenIddictValidationService
}
///
- /// Sends an introspection request to the specified URI and returns the corresponding principal.
+ /// Sends the introspection request and retrieves the corresponding response.
///
- /// The URI of the remote metadata endpoint.
- /// The token to introspect.
- /// The token type to introspect, used as a hint by the authorization server.
+ /// The server configuration.
+ /// The token request.
+ /// The uri of the remote token endpoint.
/// The that can be used to abort the operation.
- /// The claims principal created from the claim retrieved from the remote server.
- internal async ValueTask IntrospectTokenAsync(
- Uri uri, string token, string? hint, CancellationToken cancellationToken = default)
+ /// The response and the principal extracted from the introspection response.
+ 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>();
- var configuration = await options.CurrentValue.ConfigurationManager
- .GetConfigurationAsync(cancellationToken)
- .WaitAsync(cancellationToken) ??
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
-
var dispatcher = scope.ServiceProvider.GetRequiredService();
var factory = scope.ServiceProvider.GetRequiredService();
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 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 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);
}
}