diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs index e4db2baa..82abc0f1 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs @@ -97,7 +97,37 @@ namespace OpenIddict.Validation public override Task RetrieveToken([NotNull] RetrieveTokenContext context) => _eventService.PublishAsync(new OpenIddictValidationEvents.RetrieveToken(context)); - public override Task ValidateToken([NotNull] ValidateTokenContext context) - => _eventService.PublishAsync(new OpenIddictValidationEvents.ValidateToken(context)); + public override async Task ValidateToken([NotNull] ValidateTokenContext context) + { + var options = (OpenIddictValidationOptions) context.Options; + if (options.EnableAuthorizationValidation) + { + // Note: the authorization manager is deliberately not injected using constructor injection + // to allow using the validation handler without having to register the OpenIddict core services. + var manager = context.HttpContext.RequestServices.GetService(); + if (manager == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling authorization validation.") + .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") + .ToString()); + } + + var identifier = context.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId); + if (!string.IsNullOrEmpty(identifier)) + { + var authorization = await manager.FindByIdAsync(identifier); + if (authorization == null || !await manager.IsValidAsync(authorization)) + { + context.Fail("Authentication failed because the authorization " + + "associated with the access token was not longer valid."); + + return; + } + } + } + + await _eventService.PublishAsync(new OpenIddictValidationEvents.ValidateToken(context)); + } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 4f1841a0..b1850ac7 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -172,6 +172,15 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Audiences.UnionWith(audiences)); } + /// + /// Enables authorization validation so that a database call is made for each API request + /// to ensure the authorization associated with the access token is still valid. + /// Note: enabling this option may have an impact on performance. + /// + /// The . + public OpenIddictValidationBuilder EnableAuthorizationValidation() + => Configure(options => options.EnableAuthorizationValidation = true); + /// /// Configures OpenIddict not to return the authentication error /// details as part of the standard WWW-Authenticate response header. diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 869ac26c..3b98216b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -22,6 +22,12 @@ namespace OpenIddict.Validation EventsType = typeof(OpenIddictValidationProvider); } + /// + /// Gets or sets a boolean indicating whether a database call is made + /// to validate the authorization associated with the received tokens. + /// + public bool EnableAuthorizationValidation { get; set; } + /// /// Gets or sets a boolean indicating whether reference tokens are used. /// diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs index 77fc072d..def6689d 100644 --- a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs @@ -15,6 +15,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using AspNet.Security.OAuth.Validation; +using AspNet.Security.OpenIdConnect.Extensions; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -239,6 +240,223 @@ namespace OpenIddict.Validation.Tests format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); } + [Fact] + public async Task ValidateToken_ThrowsAnExceptionWhenAuthorizationManagerIsNotRegistered() + { + // Arrange + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("valid-token")) + .Returns(delegate + { + var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); + + return new AuthenticationTicket( + new ClaimsPrincipal(identity), + OpenIddictValidationDefaults.AuthenticationScheme); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.RemoveAll(typeof(IOpenIddictAuthorizationManager)); + + builder.EnableAuthorizationValidation(); + + builder.Configure(options => + { + options.AccessTokenFormat = format.Object; + options.UseReferenceTokens = false; + }); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.SendAsync(request); + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The core services must be registered when enabling authorization validation.") + .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") + .ToString(), exception.Message); + } + + [Fact] + public async Task ValidateToken_ReturnsFailedResultForUnknownAuthorization() + { + // Arrange + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("valid-token")) + .Returns(delegate + { + var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + OpenIddictValidationDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620"); + + return ticket; + }); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny())) + .ReturnsAsync(value: null); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.EnableAuthorizationValidation(); + + builder.Configure(options => + { + options.AccessTokenFormat = format.Object; + options.UseReferenceTokens = false; + }); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_ReturnsFailedResultForInvalidAuthorization() + { + // Arrange + var authorization = new OpenIddictAuthorization(); + + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("valid-token")) + .Returns(delegate + { + var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + OpenIddictValidationDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620"); + + return ticket; + }); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(false); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.EnableAuthorizationValidation(); + + builder.Configure(options => + { + options.AccessTokenFormat = format.Object; + options.UseReferenceTokens = false; + }); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_ReturnsValidResultForValidAuthorization() + { + // Arrange + var authorization = new OpenIddictAuthorization(); + + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("valid-token")) + .Returns(delegate + { + var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + OpenIddictValidationDefaults.AuthenticationScheme); + + ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620"); + + return ticket; + }); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(true); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.EnableAuthorizationValidation(); + + builder.Configure(options => + { + options.AccessTokenFormat = format.Object; + options.UseReferenceTokens = false; + }); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); + } + private static TestServer CreateResourceServer(Action configuration = null) { var builder = new WebHostBuilder(); @@ -251,6 +469,7 @@ namespace OpenIddict.Validation.Tests services.AddOpenIddict() .AddCore(options => { + options.SetDefaultAuthorizationEntity(); options.SetDefaultTokenEntity(); options.Services.AddSingleton(CreateTokenManager()); }) @@ -320,6 +539,19 @@ namespace OpenIddict.Validation.Tests return new TestServer(builder); } + private static OpenIddictAuthorizationManager CreateAuthorizationManager( + Action>> configuration = null) + { + var manager = new Mock>( + Mock.Of(), + Mock.Of>>(), + Mock.Of>()); + + configuration?.Invoke(manager); + + return manager.Object; + } + private static OpenIddictTokenManager CreateTokenManager( Action>> configuration = null) { @@ -333,6 +565,8 @@ namespace OpenIddict.Validation.Tests return manager.Object; } + public class OpenIddictAuthorization { } + public class OpenIddictToken { } } } diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs index 27648534..734d3151 100644 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs @@ -100,6 +100,22 @@ namespace OpenIddict.Validation.Tests Assert.Equal(new[] { "Fabrikam", "Contoso" }, options.Audiences); } + [Fact] + public void EnableAuthorizationValidation_ValidationIsEnforced() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.EnableAuthorizationValidation(); + + var options = GetOptions(services); + + // Assert + Assert.True(options.EnableAuthorizationValidation); + } + [Fact] public void RemoveErrorDetails_IncludeErrorDetailsIsSetToFalse() {