diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 27c73cc1..aae19c60 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -38,6 +38,11 @@ namespace Mvc.Server { .EnableTokenEndpoint("/connect/token") .EnableUserinfoEndpoint("/connect/userinfo") + // Note: the Mvc.Client sample only uses the authorization code flow but you can enable + // the other flows if you need to support implicit, password or client credentials. + .AllowAuthorizationCodeFlow() + .AllowRefreshTokenFlow() + // During development, you can disable the HTTPS requirement. .DisableHttpsRequirement(); diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index da46d86b..3e11ba63 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -88,6 +88,43 @@ namespace OpenIddict.Infrastructure { return; } + // Reject code flow authorization requests if the authorization code flow is not enabled. + if (context.Request.IsAuthorizationCodeFlow() && + !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode)) { + services.Logger.LogError("The authorization request was rejected because " + + "the authorization code flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedResponseType, + description: "The specified response_type parameter is not allowed."); + + return; + } + + // Reject implicit flow authorization requests if the implicit flow is not enabled. + if (context.Request.IsImplicitFlow() && !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) { + services.Logger.LogError("The authorization request was rejected because the implicit flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedResponseType, + description: "The specified response_type parameter is not allowed."); + + return; + } + + // Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled. + if (context.Request.IsHybridFlow() && (!services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) { + services.Logger.LogError("The authorization request was rejected because the " + + "authorization code flow or the implicit flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedResponseType, + description: "The specified response_type parameter 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. @@ -116,6 +153,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; + } + // Retrieve the application details corresponding to the requested client_id. var application = await services.Applications.FindByClientIdAsync(context.ClientId); if (application == null) { diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs index 3b5fbf26..6b62f4f4 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Discovery.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System.Diagnostics; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; @@ -16,6 +17,25 @@ 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: the OpenID Connect server middleware automatically populates grant_types_supported + // by determining whether the authorization and token endpoints are enabled or not but + // OpenIddict uses a different approach and relies on a configurable "supported list". + context.GrantTypes.Clear(); + + // Copy the supported grant types list to the discovery document. + foreach (var type in services.Options.GrantTypes) { + Debug.Assert(type == OpenIdConnectConstants.GrantTypes.AuthorizationCode || + type == OpenIdConnectConstants.GrantTypes.ClientCredentials || + type == OpenIdConnectConstants.GrantTypes.Implicit || + type == OpenIdConnectConstants.GrantTypes.Password || + type == OpenIdConnectConstants.GrantTypes.RefreshToken, + "Unsupported or non-standard OAuth2/OIDC grant types should not be exposed."); + + context.GrantTypes.Add(type); + } + // Note: the "openid" scope is automatically // added by the OpenID Connect server middleware. context.Scopes.Add(OpenIdConnectConstants.Scopes.Profile); diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index e9a90e0f..4bc34c42 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -37,6 +37,69 @@ namespace OpenIddict.Infrastructure { return; } + // Reject token requests using grant_type=authorization_code + // if the authorization code flow support is not enabled. + if (context.Request.IsAuthorizationCodeGrantType() && + !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode)) { + services.Logger.LogError("The token request was rejected because the authorization code flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedGrantType, + description: "The specified grant_type parameter is not allowed."); + + return; + } + + // Reject token requests using grant_type=client_credentials + // if the client credentials flow support is not enabled. + else if (context.Request.IsClientCredentialsGrantType() && + !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials)) { + services.Logger.LogError("The token request was rejected because the client credentials flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedGrantType, + description: "The specified grant_type parameter is not allowed."); + + return; + } + + // Reject token requests using grant_type=password if the + // resource owner password credentials flow support is not enabled. + else if (context.Request.IsPasswordGrantType() && + !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password)) { + services.Logger.LogError("The token request was rejected because the resource " + + "owner password credentials flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedGrantType, + description: "The specified grant_type parameter is not allowed."); + + return; + } + + // Reject token requests using grant_type=refresh_token + // if the refresh token flow support is not enabled. + else if (context.Request.IsRefreshTokenGrantType() && + !services.Options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { + services.Logger.LogError("The token request was rejected because the refresh token flow was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedGrantType, + description: "The specified grant_type parameter is not allowed."); + + return; + } + + // Reject token 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 allows returning a refresh token with grant_type=client_credentials, // though it's usually not recommended by the OAuth2 specification. To encourage developers to make a new // grant_type=client_credentials request instead of using refresh tokens, OpenIddict uses a stricter policy diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index d5480154..8c102fb0 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using AspNet.Security.OpenIdConnect.Extensions; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -391,6 +392,60 @@ namespace Microsoft.AspNetCore.Builder { return Configure(options => options.SigningCredentials.AddCertificate(thumbprint, name, location)); } + /// + /// Enables authorization code flow support. For more information + /// about this specific OAuth2/OpenID Connect flow, visit + /// https://tools.ietf.org/html/rfc6749#section-4.1 and + /// http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. + /// + /// The . + public virtual OpenIddictBuilder AllowAuthorizationCodeFlow() { + return Configure(options => options.GrantTypes.Add( + OpenIdConnectConstants.GrantTypes.AuthorizationCode)); + } + + /// + /// Enables client credentials flow support. For more information about this + /// specific OAuth2 flow, visit https://tools.ietf.org/html/rfc6749#section-4.4. + /// + /// The . + public virtual OpenIddictBuilder AllowClientCredentialsFlow() { + return Configure(options => options.GrantTypes.Add( + OpenIdConnectConstants.GrantTypes.ClientCredentials)); + } + + /// + /// Enables implicit flow support. For more information + /// about this specific OAuth2/OpenID Connect flow, visit + /// https://tools.ietf.org/html/rfc6749#section-4.2 and + /// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth. + /// + /// The . + public virtual OpenIddictBuilder AllowImplicitFlow() { + return Configure(options => options.GrantTypes.Add( + OpenIdConnectConstants.GrantTypes.Implicit)); + } + + /// + /// Enables password flow support. For more information about this specific + /// OAuth2 flow, visit https://tools.ietf.org/html/rfc6749#section-4.3. + /// + /// The . + public virtual OpenIddictBuilder AllowPasswordFlow() { + return Configure(options => options.GrantTypes.Add( + OpenIdConnectConstants.GrantTypes.Password)); + } + + /// + /// Enables refresh token flow support. For more information about this + /// specific OAuth2 flow, visit https://tools.ietf.org/html/rfc6749#section-6. + /// + /// The . + public virtual OpenIddictBuilder AllowRefreshTokenFlow() { + return Configure(options => options.GrantTypes.Add( + OpenIdConnectConstants.GrantTypes.RefreshToken)); + } + /// /// Disables the HTTPS requirement during development. /// diff --git a/src/OpenIddict.Core/OpenIddictExtensions.cs b/src/OpenIddict.Core/OpenIddictExtensions.cs index 436605a8..8581232b 100644 --- a/src/OpenIddict.Core/OpenIddictExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictExtensions.cs @@ -6,6 +6,7 @@ using System; using System.Linq; +using AspNet.Security.OpenIdConnect.Extensions; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -92,6 +93,41 @@ namespace Microsoft.AspNetCore.Builder { options.Cache = app.ApplicationServices.GetRequiredService(); } + // Ensure at least one flow has been enabled. + if (options.GrantTypes.Count == 0) { + throw new InvalidOperationException("At least one OAuth2/OpenID Connect flow must be enabled."); + } + + // Ensure only supported grant types are listed to prevent + // unknown flows from being exposed in the discovery document. + if (options.GrantTypes.Any(type => type != OpenIdConnectConstants.GrantTypes.AuthorizationCode && + type != OpenIdConnectConstants.GrantTypes.ClientCredentials && + type != OpenIdConnectConstants.GrantTypes.Implicit && + type != OpenIdConnectConstants.GrantTypes.Password && + type != OpenIdConnectConstants.GrantTypes.RefreshToken)) { + throw new InvalidOperationException("Only supported flows can be enabled."); + } + + // Ensure the authorization endpoint has been enabled when + // the authorization code or implicit grants are supported. + if (!options.AuthorizationEndpointPath.HasValue && + (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) { + throw new InvalidOperationException("The authorization endpoint must be enabled to use " + + "the authorization code and implicit flows."); + } + + // Ensure the token endpoint has been enabled when the authorization code, + // client credentials, password or refresh token grants are supported. + else if (!options.TokenEndpointPath.HasValue && + (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))) { + throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " + + "client credentials, password and refresh token flows."); + } + // Get the modules registered by the application // and add the OpenID Connect server middleware. var modules = options.Modules.ToList(); diff --git a/src/OpenIddict.Core/OpenIddictOptions.cs b/src/OpenIddict.Core/OpenIddictOptions.cs index e6e18d3e..d8d377c4 100644 --- a/src/OpenIddict.Core/OpenIddictOptions.cs +++ b/src/OpenIddict.Core/OpenIddictOptions.cs @@ -30,6 +30,11 @@ namespace OpenIddict { /// public IDistributedCache Cache { get; set; } + /// + /// Gets the OAuth2/OpenID Connect flows enabled for this application. + /// + public ICollection GrantTypes { get; } = new HashSet(StringComparer.Ordinal); + /// /// Gets the list of the OpenIddict modules registered in the application. ///