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' " +