From 46dceca9390a1ae9de14b7a67cac3493b5394cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 7 Aug 2017 21:37:55 +0200 Subject: [PATCH] Introduce OpenIddictBuilder.AddDevelopmentSigningCertificate() --- src/OpenIddict/OpenIddictExtensions.cs | 44 +++++++++++- src/OpenIddict/OpenIddictInitializer.cs | 17 ++++- test/OpenIddict.Tests/OpenIddict.Tests.csproj | 4 ++ .../OpenIddictExtensionsTests.cs | 69 +++++++++++++++++++ .../OpenIddictInitializerTests.cs | 35 +++++++++- 5 files changed, 161 insertions(+), 8 deletions(-) diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index 9f8adb9d..0df50a88 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -61,11 +61,11 @@ namespace Microsoft.AspNetCore.Builder // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton, - OpenIdConnectServerInitializer>()); + OpenIddictInitializer>()); builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton, - OpenIddictInitializer>()); + OpenIdConnectServerInitializer>()); // Register the OpenID Connect server handler in the authentication options, // so it can be discovered by the default authentication handler provider. @@ -89,6 +89,46 @@ namespace Microsoft.AspNetCore.Builder return builder; } + /// + /// Registers (and generates if necessary) a user-specific development + /// certificate used to sign the JWT tokens issued by OpenIddict. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddDevelopmentSigningCertificate( + [NotNull] this OpenIddictBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.Configure(options => options.SigningCredentials.AddDevelopmentCertificate()); + } + + /// + /// Registers (and generates if necessary) a user-specific development + /// certificate used to sign the JWT tokens issued by OpenIddict. + /// + /// The services builder used by OpenIddict to register new services. + /// The subject name associated with the certificate. + /// The . + public static OpenIddictBuilder AddDevelopmentSigningCertificate( + [NotNull] this OpenIddictBuilder builder, [NotNull] X500DistinguishedName subject) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (subject == null) + { + throw new ArgumentNullException(nameof(subject)); + } + + return builder.Configure(options => options.SigningCredentials.AddDevelopmentCertificate(subject)); + } + /// /// 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 diff --git a/src/OpenIddict/OpenIddictInitializer.cs b/src/OpenIddict/OpenIddictInitializer.cs index 3a713893..00e09f93 100644 --- a/src/OpenIddict/OpenIddictInitializer.cs +++ b/src/OpenIddict/OpenIddictInitializer.cs @@ -88,13 +88,24 @@ namespace OpenIddict throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); } + if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) + { + throw new InvalidOperationException( + "At least one signing key must be registered when using JWT as the access token format. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + } + // 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."); + 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 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); } } } diff --git a/test/OpenIddict.Tests/OpenIddict.Tests.csproj b/test/OpenIddict.Tests/OpenIddict.Tests.csproj index adca7c03..def91b32 100644 --- a/test/OpenIddict.Tests/OpenIddict.Tests.csproj +++ b/test/OpenIddict.Tests/OpenIddict.Tests.csproj @@ -39,4 +39,8 @@ $(DefineConstants);SUPPORTS_ECDSA + + $(DefineConstants);SUPPORTS_CERTIFICATE_GENERATION + + diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index 9b7e4e1a..6b1e416e 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -36,6 +36,75 @@ namespace OpenIddict.Tests Assert.Equal(TimeSpan.FromDays(1), options.AccessTokenLifetime); } + [Fact] + public void AddDevelopmentSigningCertificate_ThrowsAnExceptionForNullBuilder() + { + // Arrange + var builder = (OpenIddictBuilder) null; + + // Act and assert + var exception = Assert.Throws(delegate + { + builder.AddDevelopmentSigningCertificate(); + }); + + Assert.Equal("builder", exception.ParamName); + } + + [Fact] + public void AddDevelopmentSigningCertificate_ThrowsAnExceptionForNullSubject() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + // Act and assert + var exception = Assert.Throws(delegate + { + builder.AddDevelopmentSigningCertificate(subject: null); + }); + + Assert.Equal("subject", exception.ParamName); + } + +#if SUPPORTS_CERTIFICATE_GENERATION + [Fact] + public void AddDevelopmentSigningCertificate_CanGenerateCertificate() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + // Act + builder.AddDevelopmentSigningCertificate(); + + var options = GetOptions(services); + + // Assert + Assert.Equal(1, options.SigningCredentials.Count); + Assert.Equal(SecurityAlgorithms.RsaSha256, options.SigningCredentials[0].Algorithm); + Assert.NotNull(options.SigningCredentials[0].Kid); + } +#else + [Fact] + public void AddDevelopmentSigningCertificate_ThrowsAnExceptionOnUnsupportedPlatforms() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + builder.AddDevelopmentSigningCertificate(); + + // Act and assert + var exception = Assert.Throws(delegate + { + return GetOptions(services); + }); + + Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); + } +#endif + [Fact] public void AddEphemeralSigningKey_SigningKeyIsCorrectlyAdded() { diff --git a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs index 6f527ae2..6ce41c53 100644 --- a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs +++ b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs @@ -111,6 +111,33 @@ namespace OpenIddict.Tests Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); } + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfAnAccessTokenHandlerIsSet() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .EnableTokenEndpoint("/connect/token") + .AllowAuthorizationCodeFlow() + .UseJsonWebTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal( + "At least one signing key must be registered when using JWT as the access token format. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + } + [Fact] public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled() { @@ -129,9 +156,11 @@ namespace OpenIddict.Tests return client.GetAsync("/"); }); - 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); + 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 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); } private static TestServer CreateAuthorizationServer(Action configuration = null)