diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index a9fed92f..509d35e0 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -70,28 +70,7 @@ namespace Mvc.Server { // is redirected to the same page with a single parameter (request_id). // This allows flowing large OpenID Connect requests even when using // an external authentication provider like Google, Facebook or Twitter. - .EnableRequestCaching() - - // Register a new ephemeral key, that is discarded when the application - // shuts down. Tokens signed using this key are automatically invalidated. - // This method should only be used during development. - .AddEphemeralSigningKey(); - - // On production, using a X.509 certificate stored in the machine store is recommended. - // You can generate a self-signed certificate using Pluralsight's self-cert utility: - // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip - // - // services.AddOpenIddict() - // .AddSigningCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75"); - // - // Alternatively, you can also store the certificate as an embedded .pfx resource - // directly in this assembly or in a file published alongside this project: - // - // services.AddOpenIddict() - // .AddSigningCertificate( - // assembly: typeof(Startup).GetTypeInfo().Assembly, - // resource: "Mvc.Server.Certificate.pfx", - // password: "OpenIddict"); + .EnableRequestCaching(); services.AddTransient(); services.AddTransient(); diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index e68662d4..9111a8dc 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -7,6 +7,7 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.IO; +using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; using AspNet.Security.OpenIdConnect.Primitives; @@ -65,13 +66,6 @@ namespace Microsoft.AspNetCore.Builder { } } - // Ensure at least one signing certificate/key has been registered. - if (options.SigningCredentials.Count == 0) { - throw new InvalidOperationException("At least one signing key must be registered. Consider registering a X.509 " + - "certificate using 'services.AddOpenIddict().AddSigningCertificate()' or call " + - "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); - } - // 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."); @@ -79,18 +73,18 @@ namespace Microsoft.AspNetCore.Builder { // Ensure the authorization endpoint has been enabled when // the authorization code or implicit grants are supported. - if (!options.AuthorizationEndpointPath.HasValue && (options.IsAuthorizationCodeFlowEnabled() || - options.IsImplicitFlowEnabled())) { + 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. - if (!options.TokenEndpointPath.HasValue && (options.IsAuthorizationCodeFlowEnabled() || - options.IsClientCredentialsFlowEnabled() || - options.IsPasswordFlowEnabled() || - options.IsRefreshTokenFlowEnabled())) { + 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."); } @@ -99,6 +93,14 @@ namespace Microsoft.AspNetCore.Builder { throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); } + // Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled. + if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) && + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) { + throw new InvalidOperationException("At least one asymmetric signing key must be registered when enabling the implicit flow. "+ + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + } + return app.UseOpenIdConnectServer(options); } @@ -126,7 +128,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Registers a new ephemeral key used to sign the tokens issued by OpenIddict: the key + /// Registers a new ephemeral key used to sign the JWT tokens issued by OpenIddict: the key /// is discarded when the application shuts down and tokens signed using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. @@ -142,7 +144,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Registers a new ephemeral key used to sign the tokens issued by OpenIddict: the key + /// Registers a new ephemeral key used to sign the JWT tokens issued by OpenIddict: the key /// is discarded when the application shuts down and tokens signed using this key are /// automatically invalidated. This method should only be used during development. /// On production, using a X.509 certificate stored in the machine store is recommended. @@ -164,7 +166,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Registers a that is used to sign the tokens issued by OpenIddict. + /// Registers a that is used to sign the JWT tokens issued by OpenIddict. /// /// The services builder used by OpenIddict to register new services. /// The certificate used to sign the security tokens issued by the server. @@ -189,7 +191,7 @@ namespace Microsoft.AspNetCore.Builder { /// /// Registers a retrieved from an - /// embedded resource and used to sign the tokens issued by OpenIddict. + /// embedded resource and used to sign the JWT tokens issued by OpenIddict. /// /// The services builder used by OpenIddict to register new services. /// The assembly containing the certificate. @@ -220,7 +222,7 @@ namespace Microsoft.AspNetCore.Builder { /// /// Registers a extracted from a - /// stream and used to sign the tokens issued by OpenIddict. + /// stream and used to sign the JWT tokens issued by OpenIddict. /// /// The services builder used by OpenIddict to register new services. /// The stream containing the certificate. @@ -246,7 +248,7 @@ namespace Microsoft.AspNetCore.Builder { /// /// Registers a extracted from a - /// stream and used to sign the tokens issued by OpenIddict. + /// stream and used to sign the JWT tokens issued by OpenIddict. /// /// The services builder used by OpenIddict to register new services. /// The stream containing the certificate. @@ -276,7 +278,7 @@ namespace Microsoft.AspNetCore.Builder { /// /// Registers a retrieved from the X.509 - /// machine store and used to sign the tokens issued by OpenIddict. + /// machine store and used to sign the JWT tokens issued by OpenIddict. /// /// The services builder used by OpenIddict to register new services. /// The thumbprint of the certificate used to identify it in the X.509 store. @@ -296,7 +298,7 @@ namespace Microsoft.AspNetCore.Builder { /// /// Registers a retrieved from the given - /// X.509 store and used to sign the tokens issued by OpenIddict. + /// X.509 store and used to sign the JWT tokens issued by OpenIddict. /// /// The services builder used by OpenIddict to register new services. /// The thumbprint of the certificate used to identify it in the X.509 store. @@ -318,7 +320,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Registers a used to sign the tokens issued by OpenIddict. + /// Registers a used to sign the JWT tokens issued by OpenIddict. /// Note: using asymmetric keys is recommended on production. /// /// The services builder used by OpenIddict to register new services. @@ -765,70 +767,5 @@ namespace Microsoft.AspNetCore.Builder { return builder.Configure(options => options.AccessTokenHandler = new JwtSecurityTokenHandler()); } - - /// - /// Determines whether the authorization code flow has been enabled. - /// - /// The OpenIddict options. - /// true if the authorization code flow has been enabled, false otherwise. - public static bool IsAuthorizationCodeFlowEnabled([NotNull] this OpenIddictOptions options) { - if (options == null) { - throw new ArgumentNullException(nameof(options)); - } - - return options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode); - } - - /// - /// Determines whether the client credentials flow has been enabled. - /// - /// The OpenIddict options. - /// true if the client credentials flow has been enabled, false otherwise. - public static bool IsClientCredentialsFlowEnabled([NotNull] this OpenIddictOptions options) { - if (options == null) { - throw new ArgumentNullException(nameof(options)); - } - - return options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials); - } - - /// - /// Determines whether the implicit flow has been enabled. - /// - /// The OpenIddict options. - /// true if the implicit flow has been enabled, false otherwise. - public static bool IsImplicitFlowEnabled([NotNull] this OpenIddictOptions options) { - if (options == null) { - throw new ArgumentNullException(nameof(options)); - } - - return options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit); - } - - /// - /// Determines whether the password flow has been enabled. - /// - /// The OpenIddict options. - /// true if the password flow has been enabled, false otherwise. - public static bool IsPasswordFlowEnabled([NotNull] this OpenIddictOptions options) { - if (options == null) { - throw new ArgumentNullException(nameof(options)); - } - - return options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password); - } - - /// - /// Determines whether the refresh token flow has been enabled. - /// - /// The OpenIddict options. - /// true if the refresh token flow has been enabled, false otherwise. - public static bool IsRefreshTokenFlowEnabled([NotNull] this OpenIddictOptions options) { - if (options == null) { - throw new ArgumentNullException(nameof(options)); - } - - return options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken); - } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Authentication.cs b/src/OpenIddict/OpenIddictProvider.Authentication.cs index d9e4c6c0..94a3a5a1 100644 --- a/src/OpenIddict/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict/OpenIddictProvider.Authentication.cs @@ -11,7 +11,6 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Caching.Distributed; @@ -119,7 +118,8 @@ namespace OpenIddict { } // Reject code flow authorization requests if the authorization code flow is not enabled. - if (context.Request.IsAuthorizationCodeFlow() && !options.Value.IsAuthorizationCodeFlowEnabled()) { + if (context.Request.IsAuthorizationCodeFlow() && + !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode)) { logger.LogError("The authorization request was rejected because " + "the authorization code flow was not enabled."); @@ -131,7 +131,7 @@ namespace OpenIddict { } // Reject implicit flow authorization requests if the implicit flow is not enabled. - if (context.Request.IsImplicitFlow() && !options.Value.IsImplicitFlowEnabled()) { + if (context.Request.IsImplicitFlow() && !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) { logger.LogError("The authorization request was rejected because the implicit flow was not enabled."); context.Reject( @@ -142,8 +142,8 @@ namespace OpenIddict { } // Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled. - if (context.Request.IsHybridFlow() && (!options.Value.IsAuthorizationCodeFlowEnabled() || - !options.Value.IsImplicitFlowEnabled())) { + if (context.Request.IsHybridFlow() && (!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) { logger.LogError("The authorization request was rejected because the " + "authorization code flow or the implicit flow was not enabled."); @@ -155,7 +155,8 @@ namespace OpenIddict { } // Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled. - if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && !options.Value.IsRefreshTokenFlowEnabled()) { + if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && + !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "The 'offline_access' scope is not allowed."); diff --git a/src/OpenIddict/OpenIddictProvider.Discovery.cs b/src/OpenIddict/OpenIddictProvider.Discovery.cs index eb5cedee..9bbc1f7f 100644 --- a/src/OpenIddict/OpenIddictProvider.Discovery.cs +++ b/src/OpenIddict/OpenIddictProvider.Discovery.cs @@ -39,9 +39,8 @@ namespace OpenIddict { context.Scopes.Add(OpenIdConnectConstants.Scopes.Phone); context.Scopes.Add(OpenIddictConstants.Scopes.Roles); - // Only add the "offline_access" scope if the refresh - // token flow is enabled in the OpenIddict options. - if (options.Value.IsRefreshTokenFlowEnabled()) { + // Only add the "offline_access" scope if the refresh token grant is enabled. + if (context.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { context.Scopes.Add(OpenIdConnectConstants.Scopes.OfflineAccess); } diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index 599ac573..fa057b74 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -10,7 +10,6 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -37,7 +36,8 @@ namespace OpenIddict { } // Reject token requests that specify scope=offline_access if the refresh token flow is not enabled. - if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && !options.Value.IsRefreshTokenFlowEnabled()) { + if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && + !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "The 'offline_access' scope is not allowed."); diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index 6b20410b..9e5db918 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -44,32 +44,11 @@ namespace OpenIddict.Tests { "or in the dependency injection container when enabling request caching support.", exception.Message); } - [Fact] - public void UseOpenIddict_ThrowsAnExceptionWhenNoSigningCredentialsIsRegistered() { - // Arrange - var services = new ServiceCollection(); - services.AddOpenIddict(); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("At least one signing key must be registered. Consider registering a X.509 " + - "certificate using 'services.AddOpenIddict().AddSigningCertificate()' or call " + - "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); - } - [Fact] public void UseOpenIddict_ThrowsAnExceptionWhenNoFlowIsEnabled() { // Arrange var services = new ServiceCollection(); - - services.AddOpenIddict() - .AddSigningCertificate( - assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Tests.Certificate.pfx", - password: "OpenIddict"); + services.AddOpenIddict(); var builder = new ApplicationBuilder(services.BuildServiceProvider()); @@ -87,10 +66,6 @@ namespace OpenIddict.Tests { var services = new ServiceCollection(); services.AddOpenIddict() - .AddSigningCertificate( - assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Tests.Certificate.pfx", - password: "OpenIddict") .Configure(options => options.GrantTypes.Add(flow)) .Configure(options => options.AuthorizationEndpointPath = PathString.Empty); @@ -113,10 +88,6 @@ namespace OpenIddict.Tests { var services = new ServiceCollection(); services.AddOpenIddict() - .AddSigningCertificate( - assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Tests.Certificate.pfx", - password: "OpenIddict") .EnableAuthorizationEndpoint("/connect/authorize") .Configure(options => options.GrantTypes.Add(flow)) .Configure(options => options.TokenEndpointPath = PathString.Empty); @@ -136,10 +107,6 @@ namespace OpenIddict.Tests { var services = new ServiceCollection(); services.AddOpenIddict() - .AddSigningCertificate( - assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Tests.Certificate.pfx", - password: "OpenIddict") .EnableAuthorizationEndpoint("/connect/authorize") .EnableRevocationEndpoint("/connect/revocation") .AllowImplicitFlow() @@ -153,6 +120,25 @@ namespace OpenIddict.Tests { Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); } + [Fact] + public void UseOpenIddict_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled() { + // Arrange + var services = new ServiceCollection(); + + services.AddOpenIddict() + .EnableAuthorizationEndpoint("/connect/authorize") + .AllowImplicitFlow(); + + var builder = new ApplicationBuilder(services.BuildServiceProvider()); + + // Act and assert + var exception = Assert.Throws(() => builder.UseOpenIddict()); + + Assert.Equal("At least one asymmetric signing key must be registered when enabling the implicit flow. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + } + [Fact] public void Configure_OptionsAreCorrectlyAmended() { // Arrange @@ -740,80 +726,5 @@ namespace OpenIddict.Tests { // Assert Assert.IsType(typeof(JwtSecurityTokenHandler), options.Value.AccessTokenHandler); } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsAuthorizationCodeFlowEnabled_ReturnsAppropriateResult(bool enabled) { - // Arrange - var options = new OpenIddictOptions(); - - if (enabled) { - options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.AuthorizationCode); - } - - // Act and assert - Assert.Equal(enabled, options.IsAuthorizationCodeFlowEnabled()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsClientCredentialsFlowEnabled_ReturnsAppropriateResult(bool enabled) { - // Arrange - var options = new OpenIddictOptions(); - - if (enabled) { - options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.ClientCredentials); - } - - // Act and assert - Assert.Equal(enabled, options.IsClientCredentialsFlowEnabled()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsImplicitFlowEnabled_ReturnsAppropriateResult(bool enabled) { - // Arrange - var options = new OpenIddictOptions(); - - if (enabled) { - options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.Implicit); - } - - // Act and assert - Assert.Equal(enabled, options.IsImplicitFlowEnabled()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsPasswordFlowEnabled_ReturnsAppropriateResult(bool enabled) { - // Arrange - var options = new OpenIddictOptions(); - - if (enabled) { - options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.Password); - } - - // Act and assert - Assert.Equal(enabled, options.IsPasswordFlowEnabled()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void IsRefreshTokenFlowEnabled_ReturnsAppropriateResult(bool enabled) { - // Arrange - var options = new OpenIddictOptions(); - - if (enabled) { - options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.RefreshToken); - } - - // Act and assert - Assert.Equal(enabled, options.IsRefreshTokenFlowEnabled()); - } } } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index b0caca19..6c2ef445 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -121,8 +121,7 @@ namespace OpenIddict.Tests { app.Run(context => { var request = context.GetOpenIdConnectRequest(); - - if (context.Request.Path == AuthorizationEndpoint || context.Request.Path == TokenEndpoint) { + if (request.IsAuthorizationRequest() || request.IsTokenRequest()) { var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Magnifique"); @@ -138,11 +137,11 @@ namespace OpenIddict.Tests { return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); } - else if (context.Request.Path == LogoutEndpoint) { + else if (request.IsLogoutRequest()) { return context.Authentication.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme); } - else if (context.Request.Path == UserinfoEndpoint) { + else if (request.IsUserinfoRequest()) { context.Response.Headers[HeaderNames.ContentType] = "application/json"; return context.Response.WriteAsync(JsonConvert.SerializeObject(new {