diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs
index afbf1ca4..a25fa52c 100644
--- a/samples/Mvc.Server/Startup.cs
+++ b/samples/Mvc.Server/Startup.cs
@@ -194,6 +194,10 @@ namespace Mvc.Server
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles
+ },
+ Requirements =
+ {
+ OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange
}
};
diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
index 94864d2e..e98e9446 100644
--- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
+++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
@@ -36,7 +36,7 @@ namespace OpenIddict.Abstractions
///
/// Gets the permissions associated with the application.
///
- public ISet Permissions { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
+ public ISet Permissions { get; } = new HashSet(StringComparer.Ordinal);
///
/// Gets the logout callback URLs
@@ -50,6 +50,11 @@ namespace OpenIddict.Abstractions
///
public ISet RedirectUris { get; } = new HashSet();
+ ///
+ /// Gets the requirements associated with the application.
+ ///
+ public ISet Requirements { get; } = new HashSet(StringComparer.Ordinal);
+
///
/// Gets or sets the application type
/// associated with the application.
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
index 2a898bc9..74f3bb21 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
@@ -242,6 +242,17 @@ namespace OpenIddict.Abstractions
///
ValueTask> GetRedirectUrisAsync([NotNull] object application, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ ValueTask> GetRequirementsAsync([NotNull] object application, CancellationToken cancellationToken = default);
+
///
/// Determines whether the specified permission has been granted to the application.
///
@@ -251,6 +262,15 @@ namespace OpenIddict.Abstractions
/// true if the application has been granted the specified permission, false otherwise.
ValueTask HasPermissionAsync([NotNull] object application, [NotNull] string permission, CancellationToken cancellationToken = default);
+ ///
+ /// Determines whether the specified requirement has been enforced for the specified application.
+ ///
+ /// The application.
+ /// The requirement.
+ /// The that can be used to abort the operation.
+ /// true if the requirement has been enforced for the specified application, false otherwise.
+ ValueTask HasRequirementAsync([NotNull] object application, [NotNull] string requirement, CancellationToken cancellationToken = default);
+
///
/// Determines whether an application is a confidential client.
///
diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index 05f4c8e9..c9e6af1a 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
@@ -333,6 +333,19 @@ namespace OpenIddict.Abstractions
public const string Destinations = ".destinations";
}
+ public static class Requirements
+ {
+ public static class Features
+ {
+ public const string ProofKeyForCodeExchange = "ft:pkce";
+ }
+
+ public static class Prefixes
+ {
+ public const string Feature = "ft:";
+ }
+ }
+
public static class ResponseModes
{
public const string FormPost = "form_post";
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
index 982c1ff1..2859f277 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
@@ -225,6 +225,17 @@ namespace OpenIddict.Abstractions
///
ValueTask> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ ValueTask> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken);
+
///
/// Instantiates a new application.
///
@@ -342,6 +353,15 @@ namespace OpenIddict.Abstractions
ValueTask SetRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray addresses, CancellationToken cancellationToken);
+ ///
+ /// Sets the requirements associated with an application.
+ ///
+ /// The application.
+ /// The requirements associated with the application
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray requirements, CancellationToken cancellationToken);
+
///
/// Updates an existing application.
///
diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index d6ee2f1d..88542692 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -584,6 +584,26 @@ namespace OpenIddict.Core
return Store.GetRedirectUrisAsync(application, cancellationToken);
}
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ public virtual ValueTask> GetRequirementsAsync(
+ [NotNull] TApplication application, CancellationToken cancellationToken = default)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ return Store.GetRequirementsAsync(application, cancellationToken);
+ }
+
///
/// Determines whether the specified permission has been granted to the application.
///
@@ -604,7 +624,30 @@ namespace OpenIddict.Core
throw new ArgumentException("The permission name cannot be null or empty.", nameof(permission));
}
- return (await GetPermissionsAsync(application, cancellationToken)).Contains(permission, StringComparer.OrdinalIgnoreCase);
+ return (await GetPermissionsAsync(application, cancellationToken)).Contains(permission, StringComparer.Ordinal);
+ }
+
+ ///
+ /// Determines whether the specified requirement has been enforced for the specified application.
+ ///
+ /// The application.
+ /// The requirement.
+ /// The that can be used to abort the operation.
+ /// true if the requirement has been enforced for the specified application, false otherwise.
+ public virtual async ValueTask HasRequirementAsync(
+ [NotNull] TApplication application, [NotNull] string requirement, CancellationToken cancellationToken = default)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (string.IsNullOrEmpty(requirement))
+ {
+ throw new ArgumentException("The requirement name cannot be null or empty.", nameof(requirement));
+ }
+
+ return (await GetRequirementsAsync(application, cancellationToken)).Contains(requirement, StringComparer.Ordinal);
}
///
@@ -756,6 +799,7 @@ namespace OpenIddict.Core
descriptor.PostLogoutRedirectUris.Select(address => address.OriginalString)), cancellationToken);
await Store.SetRedirectUrisAsync(application, ImmutableArray.CreateRange(
descriptor.RedirectUris.Select(address => address.OriginalString)), cancellationToken);
+ await Store.SetRequirementsAsync(application, ImmutableArray.CreateRange(descriptor.Requirements), cancellationToken);
}
///
@@ -788,9 +832,10 @@ namespace OpenIddict.Core
descriptor.Type = await Store.GetClientTypeAsync(application, cancellationToken);
descriptor.Permissions.Clear();
descriptor.Permissions.UnionWith(await Store.GetPermissionsAsync(application, cancellationToken));
- descriptor.PostLogoutRedirectUris.Clear();
- descriptor.RedirectUris.Clear();
+ descriptor.Requirements.Clear();
+ descriptor.Requirements.UnionWith(await Store.GetRequirementsAsync(application, cancellationToken));
+ descriptor.PostLogoutRedirectUris.Clear();
foreach (var address in await Store.GetPostLogoutRedirectUrisAsync(application, cancellationToken))
{
// Ensure the address is not null or empty.
@@ -808,6 +853,7 @@ namespace OpenIddict.Core
descriptor.PostLogoutRedirectUris.Add(uri);
}
+ descriptor.RedirectUris.Clear();
foreach (var address in await Store.GetRedirectUrisAsync(application, cancellationToken))
{
// Ensure the address is not null or empty.
@@ -1416,9 +1462,15 @@ namespace OpenIddict.Core
ValueTask> IOpenIddictApplicationManager.GetRedirectUrisAsync(object application, CancellationToken cancellationToken)
=> GetRedirectUrisAsync((TApplication) application, cancellationToken);
+ ValueTask> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken)
+ => GetRequirementsAsync((TApplication) application, cancellationToken);
+
ValueTask IOpenIddictApplicationManager.HasPermissionAsync(object application, string permission, CancellationToken cancellationToken)
=> HasPermissionAsync((TApplication) application, permission, cancellationToken);
+ ValueTask IOpenIddictApplicationManager.HasRequirementAsync(object application, string requirement, CancellationToken cancellationToken)
+ => HasRequirementAsync((TApplication) application, requirement, cancellationToken);
+
ValueTask IOpenIddictApplicationManager.IsConfidentialAsync(object application, CancellationToken cancellationToken)
=> IsConfidentialAsync((TApplication) application, cancellationToken);
diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs
index b9962550..69b465c3 100644
--- a/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs
+++ b/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs
@@ -93,6 +93,12 @@ namespace OpenIddict.EntityFramework.Models
///
public virtual string RedirectUris { get; set; }
+ ///
+ /// Gets or sets the requirements associated with the
+ /// current application, serialized as a JSON array.
+ ///
+ public virtual string Requirements { get; set; }
+
///
/// Gets the list of the tokens associated with this application.
///
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
index f48f5b92..dc9c2a39 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
@@ -604,6 +604,43 @@ namespace OpenIddict.EntityFramework
return new ValueTask>(addresses);
}
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ public virtual ValueTask> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (string.IsNullOrEmpty(application.Requirements))
+ {
+ return new ValueTask>(ImmutableArray.Create());
+ }
+
+ // Note: parsing the stringified requirements is an expensive operation.
+ // To mitigate that, the resulting array is stored in the memory cache.
+ var key = string.Concat("b4808a89-8969-4512-895f-a909c62a8995", "\x1e", application.Requirements);
+ var requirements = Cache.GetOrCreate(key, entry =>
+ {
+ entry.SetPriority(CacheItemPriority.High)
+ .SetSlidingExpiration(TimeSpan.FromMinutes(1));
+
+ return JArray.Parse(application.Requirements)
+ .Select(element => (string) element)
+ .ToImmutableArray();
+ });
+
+ return new ValueTask>(requirements);
+ }
+
///
/// Instantiates a new application.
///
@@ -884,6 +921,32 @@ namespace OpenIddict.EntityFramework
return default;
}
+ ///
+ /// Sets the requirements associated with an application.
+ ///
+ /// The application.
+ /// The requirements associated with the application
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray requirements, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (requirements.IsDefaultOrEmpty)
+ {
+ application.Requirements = null;
+
+ return default;
+ }
+
+ application.Requirements = new JArray(requirements.ToArray()).ToString(Formatting.None);
+
+ return default;
+ }
+
///
/// Updates an existing application.
///
diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs
index c452d1a9..77580530 100644
--- a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs
+++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs
@@ -100,6 +100,12 @@ namespace OpenIddict.EntityFrameworkCore.Models
///
public virtual string RedirectUris { get; set; }
+ ///
+ /// Gets or sets the requirements associated with the
+ /// current application, serialized as a JSON array.
+ ///
+ public virtual string Requirements { get; set; }
+
///
/// Gets the list of the tokens associated with this application.
///
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
index fa1e2b7a..fc2cdbdc 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
@@ -651,6 +651,43 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask>(addresses);
}
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ public virtual ValueTask> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (string.IsNullOrEmpty(application.Requirements))
+ {
+ return new ValueTask>(ImmutableArray.Create());
+ }
+
+ // Note: parsing the stringified requirements is an expensive operation.
+ // To mitigate that, the resulting array is stored in the memory cache.
+ var key = string.Concat("b4808a89-8969-4512-895f-a909c62a8995", "\x1e", application.Requirements);
+ var requirements = Cache.GetOrCreate(key, entry =>
+ {
+ entry.SetPriority(CacheItemPriority.High)
+ .SetSlidingExpiration(TimeSpan.FromMinutes(1));
+
+ return JArray.Parse(application.Requirements)
+ .Select(element => (string) element)
+ .ToImmutableArray();
+ });
+
+ return new ValueTask>(requirements);
+ }
+
///
/// Instantiates a new application.
///
@@ -931,6 +968,32 @@ namespace OpenIddict.EntityFrameworkCore
return default;
}
+ ///
+ /// Sets the requirements associated with an application.
+ ///
+ /// The application.
+ /// The requirements associated with the application
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray requirements, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (requirements.IsDefaultOrEmpty)
+ {
+ application.Requirements = null;
+
+ return default;
+ }
+
+ application.Requirements = new JArray(requirements.ToArray()).ToString(Formatting.None);
+
+ return default;
+ }
+
///
/// Updates an existing application.
///
diff --git a/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs b/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs
index 750660b5..d5e44628 100644
--- a/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs
+++ b/src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs
@@ -83,6 +83,12 @@ namespace OpenIddict.MongoDb.Models
[BsonElement("redirect_uris"), BsonIgnoreIfDefault]
public virtual string[] RedirectUris { get; set; } = Array.Empty();
+ ///
+ /// Gets or sets the requirements associated with the current application.
+ ///
+ [BsonElement("requirements"), BsonIgnoreIfDefault]
+ public virtual string[] Requirements { get; set; } = Array.Empty();
+
///
/// Gets or sets the application type
/// associated with the current application.
diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs
index b8dbbbe6..000a2648 100644
--- a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs
+++ b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs
@@ -488,6 +488,30 @@ namespace OpenIddict.MongoDb
return new ValueTask>(application.RedirectUris.ToImmutableArray());
}
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ public virtual ValueTask> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (application.Requirements == null || application.Requirements.Length == 0)
+ {
+ return new ValueTask>(ImmutableArray.Create());
+ }
+
+ return new ValueTask>(application.Requirements.ToImmutableArray());
+ }
+
///
/// Instantiates a new application.
///
@@ -785,6 +809,33 @@ namespace OpenIddict.MongoDb
return default;
}
+ ///
+ /// Sets the requirements associated with an application.
+ ///
+ /// The application.
+ /// The requirements associated with the application
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application,
+ ImmutableArray requirements, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (requirements.IsDefaultOrEmpty)
+ {
+ application.Requirements = null;
+
+ return default;
+ }
+
+ application.Requirements = requirements.ToArray();
+
+ return default;
+ }
+
///
/// Updates an existing application.
///
diff --git a/src/OpenIddict.NHibernate.Models/OpenIddictApplication.cs b/src/OpenIddict.NHibernate.Models/OpenIddictApplication.cs
index f0b1b614..5664d785 100644
--- a/src/OpenIddict.NHibernate.Models/OpenIddictApplication.cs
+++ b/src/OpenIddict.NHibernate.Models/OpenIddictApplication.cs
@@ -95,6 +95,12 @@ namespace OpenIddict.NHibernate.Models
///
public virtual string RedirectUris { get; set; }
+ ///
+ /// Gets or sets the requirements associated with the
+ /// current application, serialized as a JSON array.
+ ///
+ public virtual string Requirements { get; set; }
+
///
/// Gets or sets the list of the tokens associated with this application.
///
diff --git a/src/OpenIddict.NHibernate/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.NHibernate/Stores/OpenIddictApplicationStore.cs
index e162e030..e202d34a 100644
--- a/src/OpenIddict.NHibernate/Stores/OpenIddictApplicationStore.cs
+++ b/src/OpenIddict.NHibernate/Stores/OpenIddictApplicationStore.cs
@@ -580,6 +580,43 @@ namespace OpenIddict.NHibernate
return new ValueTask>(addresses);
}
+ ///
+ /// Retrieves the requirements associated with an application.
+ ///
+ /// The application.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns all the requirements associated with the application.
+ ///
+ public virtual ValueTask> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (string.IsNullOrEmpty(application.Requirements))
+ {
+ return new ValueTask>(ImmutableArray.Create());
+ }
+
+ // Note: parsing the stringified requirements is an expensive operation.
+ // To mitigate that, the resulting array is stored in the memory cache.
+ var key = string.Concat("b4808a89-8969-4512-895f-a909c62a8995", "\x1e", application.Requirements);
+ var requirements = Cache.GetOrCreate(key, entry =>
+ {
+ entry.SetPriority(CacheItemPriority.High)
+ .SetSlidingExpiration(TimeSpan.FromMinutes(1));
+
+ return JArray.Parse(application.Requirements)
+ .Select(element => (string) element)
+ .ToImmutableArray();
+ });
+
+ return new ValueTask>(requirements);
+ }
+
///
/// Instantiates a new application.
///
@@ -876,6 +913,32 @@ namespace OpenIddict.NHibernate
return default;
}
+ ///
+ /// Sets the requirements associated with an application.
+ ///
+ /// The application.
+ /// The requirements associated with the application
+ /// The that can be used to abort the operation.
+ /// A that can be used to monitor the asynchronous operation.
+ public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray requirements, CancellationToken cancellationToken)
+ {
+ if (application == null)
+ {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ if (requirements.IsDefaultOrEmpty)
+ {
+ application.Requirements = null;
+
+ return default;
+ }
+
+ application.Requirements = new JArray(requirements.ToArray()).ToString(Formatting.None);
+
+ return default;
+ }
+
///
/// Updates an existing application.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
index df12d1de..883411da 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
@@ -55,6 +55,7 @@ namespace OpenIddict.Server
ValidateScopes.Descriptor,
ValidateEndpointPermissions.Descriptor,
ValidateGrantTypePermissions.Descriptor,
+ ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateScopePermissions.Descriptor,
/*
@@ -1443,6 +1444,77 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Contains the logic responsible of rejecting authorization requests made by
+ /// applications for which proof key for code exchange (PKCE) was enforced.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(new StringBuilder()
+ .AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
+ .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
+ .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
+ .Append("Alternatively, you can disable the built-in database-based server features by enabling ")
+ .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
+ .ToString());
+
+ public ValidateProofKeyForCodeExchangeRequirement([NotNull] IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ /// Processes the event.
+ ///
+ /// The context associated with the event to process.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async ValueTask HandleAsync([NotNull] ValidateAuthorizationRequestContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If a code_challenge was provided, the request is always considered valid,
+ // whether the proof key for code exchange requirement is enforced or not.
+ if (!string.IsNullOrEmpty(context.Request.CodeChallenge))
+ {
+ return;
+ }
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
+ if (application == null)
+ {
+ throw new InvalidOperationException("The client application details cannot be found in the database.");
+ }
+
+ if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
+ {
+ context.Logger.LogError("The authorization request was rejected because the " +
+ "required 'code_challenge' parameter was missing.");
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: "The mandatory 'code_challenge' parameter is missing.");
+
+ return;
+ }
+ }
+ }
+
///
/// Contains the logic responsible of rejecting authorization requests made by unauthorized applications.
/// Note: this handler is not used when the degraded mode is enabled or when scope permissions are disabled.
@@ -1470,7 +1542,7 @@ namespace OpenIddict.Server
.AddFilter()
.AddFilter()
.UseScopedHandler()
- .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
+ .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.Build();
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
index a07702b8..c047ba1e 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
@@ -52,6 +52,7 @@ namespace OpenIddict.Server
ValidateClientSecret.Descriptor,
ValidateEndpointPermissions.Descriptor,
ValidateGrantTypePermissions.Descriptor,
+ ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateScopePermissions.Descriptor,
ValidateToken.Descriptor,
ValidatePresenters.Descriptor,
@@ -1156,6 +1157,83 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Contains the logic responsible of rejecting token requests made by
+ /// applications for which proof key for code exchange (PKCE) was enforced.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidateProofKeyForCodeExchangeRequirement() => throw new InvalidOperationException(new StringBuilder()
+ .AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
+ .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
+ .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
+ .Append("Alternatively, you can disable the built-in database-based server features by enabling ")
+ .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
+ .ToString());
+
+ public ValidateProofKeyForCodeExchangeRequirement([NotNull] IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ /// Processes the event.
+ ///
+ /// The context associated with the event to process.
+ ///
+ /// A that can be used to monitor the asynchronous operation.
+ ///
+ public async ValueTask HandleAsync([NotNull] ValidateTokenRequestContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (!context.Request.IsAuthorizationCodeGrantType())
+ {
+ return;
+ }
+
+ // If a code_verifier was provided, the request is always considered valid,
+ // whether the proof key for code exchange requirement is enforced or not.
+ if (!string.IsNullOrEmpty(context.Request.CodeVerifier))
+ {
+ return;
+ }
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
+ if (application == null)
+ {
+ throw new InvalidOperationException("The client application details cannot be found in the database.");
+ }
+
+ if (await _applicationManager.HasRequirementAsync(application, Requirements.Features.ProofKeyForCodeExchange))
+ {
+ context.Logger.LogError("The token request was rejected because the " +
+ "required 'code_verifier' parameter was missing.");
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: "The mandatory 'code_verifier' parameter is missing.");
+
+ return;
+ }
+ }
+ }
+
///
/// Contains the logic responsible of rejecting token requests made by applications
/// that haven't been granted the appropriate grant type permission.
@@ -1185,7 +1263,7 @@ namespace OpenIddict.Server
.AddFilter()
.AddFilter()
.UseScopedHandler()
- .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
+ .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.Build();
///
@@ -1514,6 +1592,12 @@ namespace OpenIddict.Server
return default;
}
+ // Note: the ValidateProofKeyForCodeExchangeRequirement handler (invoked earlier) ensures
+ // a code_verifier is specified if the proof key for code exchange requirement was enforced
+ // for the client application. But unlike the aforementioned handler, ValidateCodeVerifier
+ // is active even if the degraded mode is enabled and ensures that a code_verifier is sent if a
+ // code_challenge was stored in the authorization code when the authorization request was handled.
+
// If a code challenge was initially sent in the authorization request and associated with the
// code, validate the code verifier to ensure the token request is sent by a legit caller.
var challenge = context.Principal.GetClaim(Claims.Private.CodeChallenge);
@@ -1522,8 +1606,7 @@ namespace OpenIddict.Server
return default;
}
- // Get the code verifier from the token request.
- // If it cannot be found, return an invalid_grant error.
+ // Get the code verifier from the token request. If it cannot be found, return an invalid_grant error.
if (string.IsNullOrEmpty(context.Request.CodeVerifier))
{
context.Logger.LogError("The token request was rejected because the required 'code_verifier' " +