Browse Source

Introduce application requirements and add per-application PKCE enforcement support

pull/844/head
Kévin Chalet 7 years ago
parent
commit
552ac02176
  1. 4
      samples/Mvc.Server/Startup.cs
  2. 7
      src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs
  3. 20
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  4. 13
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  5. 20
      src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs
  6. 58
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  7. 6
      src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs
  8. 63
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  9. 6
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs
  10. 63
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  11. 6
      src/OpenIddict.MongoDb.Models/OpenIddictApplication.cs
  12. 51
      src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs
  13. 6
      src/OpenIddict.NHibernate.Models/OpenIddictApplication.cs
  14. 63
      src/OpenIddict.NHibernate/Stores/OpenIddictApplicationStore.cs
  15. 74
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  16. 89
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

4
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
}
};

7
src/OpenIddict.Abstractions/Descriptors/OpenIddictApplicationDescriptor.cs

@ -36,7 +36,7 @@ namespace OpenIddict.Abstractions
/// <summary>
/// Gets the permissions associated with the application.
/// </summary>
public ISet<string> Permissions { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public ISet<string> Permissions { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets the logout callback URLs
@ -50,6 +50,11 @@ namespace OpenIddict.Abstractions
/// </summary>
public ISet<Uri> RedirectUris { get; } = new HashSet<Uri>();
/// <summary>
/// Gets the requirements associated with the application.
/// </summary>
public ISet<string> Requirements { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the application type
/// associated with the application.

20
src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs

@ -242,6 +242,17 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask<ImmutableArray<string>> GetRedirectUrisAsync([NotNull] object application, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
ValueTask<ImmutableArray<string>> GetRequirementsAsync([NotNull] object application, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the specified permission has been granted to the application.
/// </summary>
@ -251,6 +262,15 @@ namespace OpenIddict.Abstractions
/// <returns><c>true</c> if the application has been granted the specified permission, <c>false</c> otherwise.</returns>
ValueTask<bool> HasPermissionAsync([NotNull] object application, [NotNull] string permission, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether the specified requirement has been enforced for the specified application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirement">The requirement.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the requirement has been enforced for the specified application, <c>false</c> otherwise.</returns>
ValueTask<bool> HasRequirementAsync([NotNull] object application, [NotNull] string requirement, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether an application is a confidential client.
/// </summary>

13
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";

20
src/OpenIddict.Abstractions/Stores/IOpenIddictApplicationStore.cs

@ -225,6 +225,17 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask<ImmutableArray<string>> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
ValueTask<ImmutableArray<string>> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Instantiates a new application.
/// </summary>
@ -342,6 +353,15 @@ namespace OpenIddict.Abstractions
ValueTask SetRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> addresses, CancellationToken cancellationToken);
/// <summary>
/// Sets the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirements">The requirements associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray<string> requirements, CancellationToken cancellationToken);
/// <summary>
/// Updates an existing application.
/// </summary>

58
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -584,6 +584,26 @@ namespace OpenIddict.Core
return Store.GetRedirectUrisAsync(application, cancellationToken);
}
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return Store.GetRequirementsAsync(application, cancellationToken);
}
/// <summary>
/// Determines whether the specified permission has been granted to the application.
/// </summary>
@ -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);
}
/// <summary>
/// Determines whether the specified requirement has been enforced for the specified application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirement">The requirement.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the requirement has been enforced for the specified application, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> 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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -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<ImmutableArray<string>> IOpenIddictApplicationManager.GetRedirectUrisAsync(object application, CancellationToken cancellationToken)
=> GetRedirectUrisAsync((TApplication) application, cancellationToken);
ValueTask<ImmutableArray<string>> IOpenIddictApplicationManager.GetRequirementsAsync(object application, CancellationToken cancellationToken)
=> GetRequirementsAsync((TApplication) application, cancellationToken);
ValueTask<bool> IOpenIddictApplicationManager.HasPermissionAsync(object application, string permission, CancellationToken cancellationToken)
=> HasPermissionAsync((TApplication) application, permission, cancellationToken);
ValueTask<bool> IOpenIddictApplicationManager.HasRequirementAsync(object application, string requirement, CancellationToken cancellationToken)
=> HasRequirementAsync((TApplication) application, requirement, cancellationToken);
ValueTask<bool> IOpenIddictApplicationManager.IsConfidentialAsync(object application, CancellationToken cancellationToken)
=> IsConfidentialAsync((TApplication) application, cancellationToken);

6
src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs

@ -93,6 +93,12 @@ namespace OpenIddict.EntityFramework.Models
/// </summary>
public virtual string RedirectUris { get; set; }
/// <summary>
/// Gets or sets the requirements associated with the
/// current application, serialized as a JSON array.
/// </summary>
public virtual string Requirements { get; set; }
/// <summary>
/// Gets the list of the tokens associated with this application.
/// </summary>

63
src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs

@ -604,6 +604,43 @@ namespace OpenIddict.EntityFramework
return new ValueTask<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Requirements))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(requirements);
}
/// <summary>
/// Instantiates a new application.
/// </summary>
@ -884,6 +921,32 @@ namespace OpenIddict.EntityFramework
return default;
}
/// <summary>
/// Sets the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirements">The requirements associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray<string> 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;
}
/// <summary>
/// Updates an existing application.
/// </summary>

6
src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs

@ -100,6 +100,12 @@ namespace OpenIddict.EntityFrameworkCore.Models
/// </summary>
public virtual string RedirectUris { get; set; }
/// <summary>
/// Gets or sets the requirements associated with the
/// current application, serialized as a JSON array.
/// </summary>
public virtual string Requirements { get; set; }
/// <summary>
/// Gets the list of the tokens associated with this application.
/// </summary>

63
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -651,6 +651,43 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Requirements))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(requirements);
}
/// <summary>
/// Instantiates a new application.
/// </summary>
@ -931,6 +968,32 @@ namespace OpenIddict.EntityFrameworkCore
return default;
}
/// <summary>
/// Sets the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirements">The requirements associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray<string> 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;
}
/// <summary>
/// Updates an existing application.
/// </summary>

6
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<string>();
/// <summary>
/// Gets or sets the requirements associated with the current application.
/// </summary>
[BsonElement("requirements"), BsonIgnoreIfDefault]
public virtual string[] Requirements { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the application type
/// associated with the current application.

51
src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs

@ -488,6 +488,30 @@ namespace OpenIddict.MongoDb
return new ValueTask<ImmutableArray<string>>(application.RedirectUris.ToImmutableArray());
}
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> 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<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(application.Requirements.ToImmutableArray());
}
/// <summary>
/// Instantiates a new application.
/// </summary>
@ -785,6 +809,33 @@ namespace OpenIddict.MongoDb
return default;
}
/// <summary>
/// Sets the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirements">The requirements associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary>
/// Updates an existing application.
/// </summary>

6
src/OpenIddict.NHibernate.Models/OpenIddictApplication.cs

@ -95,6 +95,12 @@ namespace OpenIddict.NHibernate.Models
/// </summary>
public virtual string RedirectUris { get; set; }
/// <summary>
/// Gets or sets the requirements associated with the
/// current application, serialized as a JSON array.
/// </summary>
public virtual string Requirements { get; set; }
/// <summary>
/// Gets or sets the list of the tokens associated with this application.
/// </summary>

63
src/OpenIddict.NHibernate/Stores/OpenIddictApplicationStore.cs

@ -580,6 +580,43 @@ namespace OpenIddict.NHibernate
return new ValueTask<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Retrieves the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the requirements associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRequirementsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Requirements))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(requirements);
}
/// <summary>
/// Instantiates a new application.
/// </summary>
@ -876,6 +913,32 @@ namespace OpenIddict.NHibernate
return default;
}
/// <summary>
/// Sets the requirements associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="requirements">The requirements associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual ValueTask SetRequirementsAsync([NotNull] TApplication application, ImmutableArray<string> 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;
}
/// <summary>
/// Updates an existing application.
/// </summary>

74
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
}
}
/// <summary>
/// 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.
/// </summary>
public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] 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;
}
}
}
/// <summary>
/// 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<RequireScopePermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateScopePermissions>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.Build();
/// <summary>

89
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
}
}
/// <summary>
/// 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.
/// </summary>
public class ValidateProofKeyForCodeExchangeRequirement : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireClientIdParameter>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] 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;
}
}
}
/// <summary>
/// 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<RequireDegradedModeDisabled>()
.AddFilter<RequireScopePermissionsEnabled>()
.UseScopedHandler<ValidateScopePermissions>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -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' " +

Loading…
Cancel
Save