diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs
index 2bc61ca8..fa7a628f 100644
--- a/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs
+++ b/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs
@@ -12,6 +12,14 @@ using Microsoft.AspNetCore.Identity;
namespace OpenIddict.Infrastructure {
public static class OpenIddictHelpers {
+ ///
+ /// Tries to find the given claim in the user claims.
+ ///
+ /// The type of the User entity.
+ /// The user manager.
+ /// The user.
+ /// The claim type.
+ /// The claim value, or null if it cannot be found.
public static async Task FindClaimAsync(
[NotNull] this UserManager manager,
[NotNull] TUser user, [NotNull] string type) where TUser : class {
@@ -33,5 +41,51 @@ namespace OpenIddict.Infrastructure {
where string.Equals(claim.Type, type, StringComparison.OrdinalIgnoreCase)
select claim.Value).FirstOrDefault();
}
+
+ ///
+ /// Determines whether an application is a confidential client.
+ ///
+ /// The type of the Application entity.
+ /// The application manager.
+ /// The application.
+ /// true if the application is a confidential client, false otherwise.
+ public static async Task IsConfidentialAsync(
+ [NotNull] this OpenIddictApplicationManager manager,
+ [NotNull] TApplication application) where TApplication : class {
+ if (manager == null) {
+ throw new ArgumentNullException(nameof(manager));
+ }
+
+ if (application == null) {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ var type = await manager.GetClientTypeAsync(application);
+
+ return string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Determines whether an application is a public client.
+ ///
+ /// The type of the Application entity.
+ /// The application manager.
+ /// The application.
+ /// true if the application is a public client, false otherwise.
+ public static async Task IsPublicAsync(
+ [NotNull] this OpenIddictApplicationManager manager,
+ [NotNull] TApplication application) where TApplication : class {
+ if (manager == null) {
+ throw new ArgumentNullException(nameof(manager));
+ }
+
+ if (application == null) {
+ throw new ArgumentNullException(nameof(application));
+ }
+
+ var type = await manager.GetClientTypeAsync(application);
+
+ return string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase);
+ }
}
}
diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs
index e7fd8264..324606d1 100644
--- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs
+++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs
@@ -139,6 +139,16 @@ namespace OpenIddict.Infrastructure {
return;
}
+ // Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
+ if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
+ !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) {
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "The 'offline_access' scope is not allowed.");
+
+ return;
+ }
+
// Note: the OpenID Connect server middleware supports the query, form_post and fragment response modes
// and doesn't reject unknown/custom modes until the ApplyAuthorizationResponse event is invoked.
// To ensure authorization requests are rejected early enough, an additional check is made by OpenIddict.
@@ -167,14 +177,46 @@ namespace OpenIddict.Infrastructure {
return;
}
- // Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
- if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
- !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) {
- context.Reject(
- error: OpenIdConnectConstants.Errors.InvalidRequest,
- description: "The 'offline_access' scope is not allowed.");
+ // Note: the OpenID Connect server middleware always ensures a
+ // code_challenge_method can't be specified without code_challenge.
+ if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) {
+ // Since the default challenge method (plain) is explicitly disallowed,
+ // reject the authorization request if the code_challenge_method is missing.
+ if (string.IsNullOrEmpty(context.Request.CodeChallengeMethod)) {
+ services.Logger.LogError("The authorization request was rejected because the " +
+ "required 'code_challenge_method' parameter was missing.");
- return;
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "The 'code_challenge_method' parameter must be specified.");
+
+ return;
+ }
+
+ // Disallow the use of the unsecure code_challenge_method=plain method.
+ // See https://tools.ietf.org/html/rfc7636#section-7.2 for more information.
+ if (context.Request.CodeChallengeMethod == OpenIdConnectConstants.CodeChallengeMethods.Plain) {
+ services.Logger.LogError("The authorization request was rejected because the " +
+ "'code_challenge_method' parameter was set to 'plain'.");
+
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "The specified response_type parameter is not allowed when using PKCE.");
+
+ return;
+ }
+
+ // Reject authorization requests that contain response_type=token when a code_challenge is specified.
+ if (context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token)) {
+ services.Logger.LogError("The authorization request was rejected because the " +
+ "specified response type was not compatible with PKCE.");
+
+ context.Reject(
+ error: OpenIdConnectConstants.Errors.InvalidRequest,
+ description: "The specified response_type parameter is not allowed when using PKCE.");
+
+ return;
+ }
}
// Retrieve the application details corresponding to the requested client_id.
@@ -205,9 +247,7 @@ namespace OpenIddict.Infrastructure {
// flow are rejected if the client identifier corresponds to a confidential application.
// Note: when using the authorization code grant, ValidateTokenRequest is responsible of
// rejecting the token request if the client_id corresponds to an unauthenticated confidential client.
- var type = await services.Applications.GetClientTypeAsync(application);
- if (!string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase) &&
- !context.Request.IsAuthorizationCodeFlow()) {
+ if (await services.Applications.IsPublicAsync(application) && !context.Request.IsAuthorizationCodeFlow()) {
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Confidential clients can only use response_type=code.");
diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs
index 6b62f4f4..82b4417a 100644
--- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs
+++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs
@@ -17,7 +17,11 @@ namespace OpenIddict.Infrastructure {
public override Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService>();
- Debug.Assert(services.Options.GrantTypes.Count != 0, "At least one flow should be enabled.");
+ // Note: though it's natively supported by the OpenID Connect server middleware,
+ // OpenIddict disallows the use of the unsecure code_challenge_method=plain method,
+ // which must be manually removed from the code_challenge_methods_supported property.
+ // See https://tools.ietf.org/html/rfc7636#section-7.2 for more information.
+ context.CodeChallengeMethods.Remove(OpenIdConnectConstants.CodeChallengeMethods.Plain);
// Note: the OpenID Connect server middleware automatically populates grant_types_supported
// by determining whether the authorization and token endpoints are enabled or not but
diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs
index 4bc34c42..bee83461 100644
--- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs
+++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs
@@ -161,8 +161,7 @@ namespace OpenIddict.Infrastructure {
return;
}
- var type = await services.Applications.GetClientTypeAsync(application);
- if (string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) {
+ if (await services.Applications.IsPublicAsync(application)) {
// Note: public applications are not allowed to use the client credentials grant.
if (context.Request.IsClientCredentialsGrantType()) {
services.Logger.LogError("The token request was rejected because the public client application '{ClientId}' " +
diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs
index 331f1f09..17b458e6 100644
--- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs
+++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs
@@ -55,8 +55,7 @@ namespace OpenIddict.Infrastructure {
}
// Reject non-confidential applications.
- var type = await services.Applications.GetClientTypeAsync(application);
- if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) {
+ if (!await services.Applications.IsConfidentialAsync(application)) {
services.Logger.LogError("The introspection request was rejected because the public application " +
"'{ClientId}' was not allowed to use this endpoint.", context.ClientId);
diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs
index 87d0a2ec..9c24f466 100644
--- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs
+++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs
@@ -55,8 +55,7 @@ namespace OpenIddict.Infrastructure {
}
// Reject revocation requests containing a client_secret if the client application is not confidential.
- var type = await services.Applications.GetClientTypeAsync(application);
- if (string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) {
+ if (await services.Applications.IsPublicAsync(application)) {
// Reject tokens requests containing a client_secret when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientSecret)) {
context.Reject(