From a65deb4463ccc264efb47e224663c8a64f7c9af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 31 Jul 2018 20:56:27 +0200 Subject: [PATCH 1/2] Update OpenIddictServerBuilder to allow calling SetAccessTokenLifetime/SetAuthorizationCodeLifetime/SetIdentityTokenLifetime/SetRefreshTokenLifetime with null values --- .../OpenIddictServerProvider.Helpers.cs | 4 +- .../OpenIddictServerBuilder.cs | 12 +- .../Internal/OpenIddictServerProviderTests.cs | 134 +++++++++++++++++- .../OpenIddictServerBuilderTests.cs | 64 +++++++++ 4 files changed, 207 insertions(+), 7 deletions(-) diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs index 749c0650..88821b2a 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -504,14 +504,14 @@ namespace OpenIddict.Server { // Note: the request cancellation token is deliberately not used here to ensure the caller // cannot prevent this operation from being executed by resetting the TCP connection. - var date = options.SystemClock.UtcNow + lifetime; + var date = options.SystemClock.UtcNow + lifetime.Value; await _tokenManager.ExtendAsync(token, date); _logger.LogInformation("The expiration date of the refresh token '{Identifier}' " + "was automatically updated: {Date}.", identifier, date); } - else + else if (await _tokenManager.GetExpirationDateAsync(token) != null) { // Note: the request cancellation token is deliberately not used here to ensure the caller // cannot prevent this operation from being executed by resetting the TCP connection. diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index abdd1823..9b12a209 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -701,29 +701,32 @@ namespace Microsoft.Extensions.DependencyInjection /// a new access token by making a grant_type=refresh_token token request /// or a prompt=none authorization request, depending on the selected flow. /// Using long-lived access tokens or tokens that never expire is not recommended. + /// While discouraged, null can be specified to issue tokens that never expire. /// /// The access token lifetime. /// The . - public OpenIddictServerBuilder SetAccessTokenLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetAccessTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.AccessTokenLifetime = lifetime); /// /// Sets the authorization code lifetime, after which client applications /// are unable to send a grant_type=authorization_code token request. /// Using short-lived authorization codes is strongly recommended. + /// While discouraged, null can be specified to issue codes that never expire. /// /// The authorization code lifetime. /// The . - public OpenIddictServerBuilder SetAuthorizationCodeLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetAuthorizationCodeLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.AuthorizationCodeLifetime = lifetime); /// /// Sets the identity token lifetime, after which client /// applications should refuse processing identity tokens. + /// While discouraged, null can be specified to issue tokens that never expire. /// /// The identity token lifetime. /// The . - public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetIdentityTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.IdentityTokenLifetime = lifetime); /// @@ -731,10 +734,11 @@ namespace Microsoft.Extensions.DependencyInjection /// a new authorization from the user. When sliding expiration is enabled, /// a new refresh token is always issued to the client application, /// which prolongs the validity period of the refresh token. + /// While discouraged, null can be specified to issue tokens that never expire. /// /// The refresh token lifetime. /// The . - public OpenIddictServerBuilder SetRefreshTokenLifetime(TimeSpan lifetime) + public OpenIddictServerBuilder SetRefreshTokenLifetime([CanBeNull] TimeSpan? lifetime) => Configure(options => options.RefreshTokenLifetime = lifetime); /// diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs index 4e432688..3654d496 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -1005,7 +1005,7 @@ namespace OpenIddict.Server.Tests new AuthenticationProperties(), OpenIddictServerDefaults.AuthenticationScheme); - ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "60FFF7EA-F98E-437B-937E-5073CC313103"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); @@ -1063,6 +1063,138 @@ namespace OpenIddict.Server.Tests It.IsAny()), Times.Never()); } + [Fact] + public async Task ProcessSigninResponse_DoesNotUpdateExpirationDateWhenAlreadyNull() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = null; + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, null, It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSigninResponse_SetsExpirationDateToNullWhenLifetimeIsNull() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, "60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); + + var format = new Mock>(); + + format.Setup(mock => mock.Protect(It.IsAny())) + .Returns("8xLOxBtZp8"); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(DateTimeOffset.Now + TimeSpan.FromDays(1))); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.Configure(options => + { + options.SystemClock = Mock.Of(mock => mock.UtcNow == + new DateTimeOffset(2017, 01, 05, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = null; + options.RefreshTokenFormat = format.Object; + }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(mock => mock.ExtendAsync(token, null, It.IsAny()), Times.Once()); + } + [Fact] public async Task ProcessSigninResponse_IgnoresErrorWhenExtendingLifetimeOfExistingTokenFailed() { diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index f30dbbbf..0d417a4a 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -601,6 +601,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.AccessTokenLifetime); } + [Fact] + public void SetAccessTokenLifetime_AccessTokenLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetAccessTokenLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.AccessTokenLifetime); + } + [Fact] public void SetAuthorizationCodeLifetime_DefaultAuthorizationCodeLifetimeIsReplaced() { @@ -617,6 +633,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.AuthorizationCodeLifetime); } + [Fact] + public void SetAuthorizationCodeLifetime_AuthorizationCodeLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetAuthorizationCodeLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.AuthorizationCodeLifetime); + } + [Fact] public void SetIdentityTokenLifetime_DefaultIdentityTokenLifetimeIsReplaced() { @@ -633,6 +665,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.IdentityTokenLifetime); } + [Fact] + public void SetIdentityTokenLifetime_IdentityTokenLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetIdentityTokenLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.IdentityTokenLifetime); + } + [Fact] public void SetRefreshTokenLifetime_DefaultRefreshTokenLifetimeIsReplaced() { @@ -649,6 +697,22 @@ namespace OpenIddict.Server.Tests Assert.Equal(TimeSpan.FromMinutes(42), options.RefreshTokenLifetime); } + [Fact] + public void SetRefreshTokenLifetime_RefreshTokenLifetimeCanBeSetToNull() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetRefreshTokenLifetime(null); + + var options = GetOptions(services); + + // Assert + Assert.Null(options.RefreshTokenLifetime); + } + [Fact] public void SetIssuer_AddressIsReplaced() { From ab34bdae2e5ca11fdb1b9e549febb010a5c581da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 1 Aug 2018 01:17:11 +0200 Subject: [PATCH 2/2] Update AddServer()/AddValidation() to throw an exception when the OpenID Connect server/OAuth validation handler are already registered and to prevent custom providers --- .../Internal/OpenIddictServerInitializer.cs | 10 ++ .../OpenIddictServerExtensions.cs | 12 +- .../OpenIddictValidationInitializer.cs | 11 ++ .../OpenIddictValidationExtensions.cs | 13 ++- .../OpenIddictServerInitializerTests.cs | 53 +++++++++ .../OpenIddictServerExtensionsTests.cs | 40 +++++++ .../OpenIddictValidationInitializerTests.cs | 103 ++++++++++++++++++ .../OpenIddictValidationExtensionsTests.cs | 41 +++++++ 8 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs b/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs index 4556ccd9..2c2e737e 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs @@ -63,6 +63,16 @@ namespace OpenIddict.Server throw new InvalidOperationException("A random number generator must be registered."); } + if (options.ProviderType == null || options.ProviderType != typeof(OpenIddictServerProvider)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("OpenIddict can only be used with its built-in server provider.") + .AppendLine("This error may indicate that 'OpenIddictServerOptions.ProviderType' was manually set.") + .Append("To execute custom request handling logic, consider registering an event handler using ") + .Append("the generic 'services.AddOpenIddict().AddServer().AddEventHandler()' method.") + .ToString()); + } + // When no distributed cache has been registered in the options, // try to resolve it from the dependency injection container. if (options.Cache == null) diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 5c515731..fff29084 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -70,8 +70,18 @@ namespace Microsoft.Extensions.DependencyInjection { // Note: this method is guaranteed to be idempotent. To prevent multiple schemes from being // registered (which would result in an exception being thrown), a manual check is made here. - if (options.SchemeMap.ContainsKey(OpenIddictServerDefaults.AuthenticationScheme)) + if (options.SchemeMap.TryGetValue(OpenIddictServerDefaults.AuthenticationScheme, out var handler)) { + // If the handler type doesn't correspond to the OpenIddict handler, throw an exception. + if (handler.HandlerType != typeof(OpenIddictServerHandler)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The OpenIddict server handler cannot be registered as an authentication scheme.") + .AppendLine("This may indicate that an instance of the OpenID Connect server was registered.") + .Append("Make sure that 'services.AddAuthentication().AddOpenIdConnectServer()' is not used.") + .ToString()); + } + return; } diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs index ab3e5b2c..bedfea03 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using System.Text; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; @@ -48,6 +49,16 @@ namespace OpenIddict.Validation throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name)); } + if (options.EventsType == null || options.EventsType != typeof(OpenIddictValidationProvider)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("OpenIddict can only be used with its built-in validation provider.") + .AppendLine("This error may indicate that 'OpenIddictValidationOptions.EventsType' was manually set.") + .Append("To execute custom request handling logic, consider registering an event handler using ") + .Append("the generic 'services.AddOpenIddict().AddValidation().AddEventHandler()' method.") + .ToString()); + } + if (options.DataProtectionProvider == null) { options.DataProtectionProvider = _dataProtectionProvider; diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 8c74f88e..80d7fc6c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -5,6 +5,7 @@ */ using System; +using System.Text; using AspNet.Security.OAuth.Validation; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; @@ -53,8 +54,18 @@ namespace Microsoft.Extensions.DependencyInjection { // Note: this method is guaranteed to be idempotent. To prevent multiple schemes from being // registered (which would result in an exception being thrown), a manual check is made here. - if (options.SchemeMap.ContainsKey(OpenIddictValidationDefaults.AuthenticationScheme)) + if (options.SchemeMap.TryGetValue(OpenIddictValidationDefaults.AuthenticationScheme, out var handler)) { + // If the handler type doesn't correspond to the OpenIddict handler, throw an exception. + if (handler.HandlerType != typeof(OpenIddictValidationHandler)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The OpenIddict validation handler cannot be registered as an authentication scheme.") + .AppendLine("This may indicate that an instance of the OAuth validation handler was registered.") + .Append("Make sure that 'services.AddAuthentication().AddOAuthValidation()' is not used.") + .ToString()); + } + return; } diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs index 72eacfbd..5f9385f5 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Client; using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -43,6 +44,58 @@ namespace OpenIddict.Server.Tests Assert.Equal("A random number generator must be registered.", exception.Message); } + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenProviderTypeIsNull() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => options.ProviderType = null); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal(new StringBuilder() + .AppendLine("OpenIddict can only be used with its built-in server provider.") + .AppendLine("This error may indicate that 'OpenIddictServerOptions.ProviderType' was manually set.") + .Append("To execute custom request handling logic, consider registering an event handler using ") + .Append("the generic 'services.AddOpenIddict().AddServer().AddEventHandler()' method.") + .ToString(), exception.Message); + } + + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenProviderTypeIsIncompatible() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => options.ProviderType = typeof(OpenIdConnectServerProvider)); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal(new StringBuilder() + .AppendLine("OpenIddict can only be used with its built-in server provider.") + .AppendLine("This error may indicate that 'OpenIddictServerOptions.ProviderType' was manually set.") + .Append("To execute custom request handling logic, consider registering an event handler using ") + .Append("the generic 'services.AddOpenIddict().AddServer().AddEventHandler()' method.") + .ToString(), exception.Message); + } + [Fact] public async Task PostConfigure_ThrowsAnExceptionWhenNoFlowIsEnabled() { diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs index 7968897a..baed90f9 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs @@ -204,5 +204,45 @@ namespace OpenIddict.Server.Tests Assert.Contains(options.Schemes, scheme => scheme.Name == OpenIddictServerDefaults.AuthenticationScheme && scheme.HandlerType == typeof(OpenIddictServerHandler)); } + + [Fact] + public void AddServer_ThrowsAnExceptionWhenSchemeIsAlreadyRegisteredWithDifferentHandlerType() + { + // Arrange + var services = new ServiceCollection(); + services.AddAuthentication() + .AddOpenIdConnectServer(); + + var builder = new OpenIddictBuilder(services); + + // Act + builder.AddServer(); + + // Assert + var provider = services.BuildServiceProvider(); + var exception = Assert.Throws(delegate + { + return provider.GetRequiredService>().Value; + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The OpenIddict server handler cannot be registered as an authentication scheme.") + .AppendLine("This may indicate that an instance of the OpenID Connect server was registered.") + .Append("Make sure that 'services.AddAuthentication().AddOpenIdConnectServer()' is not used.") + .ToString(), exception.Message); + } + + [Fact] + public void AddServer_CanBeSafelyInvokedMultipleTimes() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(services); + + // Act and assert + builder.AddServer(); + builder.AddServer(); + builder.AddServer(); + } } } diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs new file mode 100644 index 00000000..3710fe51 --- /dev/null +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs @@ -0,0 +1,103 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Text; +using System.Threading.Tasks; +using AspNet.Security.OAuth.Validation; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace OpenIddict.Validation.Tests +{ + public class OpenIddictValidationInitializerTests + { + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenEventsTypeIsNull() + { + // Arrange + var server = CreateResourceServer(builder => + { + builder.Configure(options => options.EventsType = null); + }); + + var client = server.CreateClient(); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal(new StringBuilder() + .AppendLine("OpenIddict can only be used with its built-in validation provider.") + .AppendLine("This error may indicate that 'OpenIddictValidationOptions.EventsType' was manually set.") + .Append("To execute custom request handling logic, consider registering an event handler using ") + .Append("the generic 'services.AddOpenIddict().AddValidation().AddEventHandler()' method.") + .ToString(), exception.Message); + } + + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenEventsTypeIsIncompatible() + { + // Arrange + var server = CreateResourceServer(builder => + { + builder.Configure(options => options.EventsType = typeof(OAuthValidationEvents)); + }); + + var client = server.CreateClient(); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal(new StringBuilder() + .AppendLine("OpenIddict can only be used with its built-in validation provider.") + .AppendLine("This error may indicate that 'OpenIddictValidationOptions.EventsType' was manually set.") + .Append("To execute custom request handling logic, consider registering an event handler using ") + .Append("the generic 'services.AddOpenIddict().AddValidation().AddEventHandler()' method.") + .ToString(), exception.Message); + } + + private static TestServer CreateResourceServer(Action configuration = null) + { + var builder = new WebHostBuilder(); + + builder.UseEnvironment("Testing"); + + builder.ConfigureLogging(options => options.AddDebug()); + + builder.ConfigureServices(services => + { + services.AddAuthentication(); + services.AddOptions(); + services.AddDistributedMemoryCache(); + + services.AddOpenIddict() + .AddValidation(options => configuration?.Invoke(options)); + }); + + builder.Configure(app => + { + app.UseAuthentication(); + + app.Run(context => context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme)); + }); + + return new TestServer(builder); + } + } +} diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs index eb016079..7fde8265 100644 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationExtensionsTests.cs @@ -5,6 +5,7 @@ */ using System; +using System.Text; using AspNet.Security.OAuth.Validation; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; @@ -165,5 +166,45 @@ namespace OpenIddict.Validation.Tests Assert.Contains(options.Schemes, scheme => scheme.Name == OpenIddictValidationDefaults.AuthenticationScheme && scheme.HandlerType == typeof(OpenIddictValidationHandler)); } + + [Fact] + public void AddValidation_ThrowsAnExceptionWhenSchemeIsAlreadyRegisteredWithDifferentHandlerType() + { + // Arrange + var services = new ServiceCollection(); + services.AddAuthentication() + .AddOAuthValidation(); + + var builder = new OpenIddictBuilder(services); + + // Act + builder.AddValidation(); + + // Assert + var provider = services.BuildServiceProvider(); + var exception = Assert.Throws(delegate + { + return provider.GetRequiredService>().Value; + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The OpenIddict validation handler cannot be registered as an authentication scheme.") + .AppendLine("This may indicate that an instance of the OAuth validation handler was registered.") + .Append("Make sure that 'services.AddAuthentication().AddOAuthValidation()' is not used.") + .ToString(), exception.Message); + } + + [Fact] + public void AddValidation_CanBeSafelyInvokedMultipleTimes() + { + // Arrange + var services = new ServiceCollection(); + var builder = new OpenIddictBuilder(services); + + // Act and assert + builder.AddValidation(); + builder.AddValidation(); + builder.AddValidation(); + } } }