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(