diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs index 65038a22..d8b7ab19 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs @@ -633,9 +633,15 @@ namespace OpenIddict.EntityFramework // and thus prevent them from being concurrently modified outside this block. using var transaction = CreateTransaction(); + // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable + // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime. + // To work around this limitation, the threshold represented as a DateTimeOffset + // instance is manually converted to a UTC DateTime instance outside the query. + var date = threshold.UtcDateTime; + var authorizations = await (from authorization in Authorizations.Include(authorization => authorization.Tokens) - where authorization.CreationDate < threshold.UtcDateTime + where authorization.CreationDate < date where authorization.Status != Statuses.Valid || (authorization.Type == AuthorizationTypes.AdHoc && !authorization.Tokens.Any()) orderby authorization.Id diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs index 069c8b59..66598ec4 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs @@ -618,9 +618,15 @@ namespace OpenIddict.EntityFramework // and thus prevent them from being concurrently modified outside this block. using var transaction = CreateTransaction(); + // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable + // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime. + // To work around this limitation, the threshold represented as a DateTimeOffset + // instance is manually converted to a UTC DateTime instance outside the query. + var date = threshold.UtcDateTime; + var tokens = await (from token in Tokens - where token.CreationDate < threshold.UtcDateTime + where token.CreationDate < date where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || (token.Authorization != null && token.Authorization.Status != Statuses.Valid) || token.ExpirationDate < DateTime.UtcNow diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs index fc7da06a..265b2d8e 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs @@ -713,9 +713,15 @@ namespace OpenIddict.EntityFrameworkCore // and thus prevent them from being concurrently modified outside this block. using var transaction = await CreateTransactionAsync(); + // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable + // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime. + // To work around this limitation, the threshold represented as a DateTimeOffset + // instance is manually converted to a UTC DateTime instance outside the query. + var date = threshold.UtcDateTime; + var authorizations = await (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking() - where authorization.CreationDate < threshold.UtcDateTime + where authorization.CreationDate < date where authorization.Status != Statuses.Valid || (authorization.Type == AuthorizationTypes.AdHoc && !authorization.Tokens.Any()) orderby authorization.Id diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index 99cbd945..7ad98c28 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs @@ -681,9 +681,15 @@ namespace OpenIddict.EntityFrameworkCore // and thus prevent them from being concurrently modified outside this block. using var transaction = await CreateTransactionAsync(); + // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable + // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime. + // To work around this limitation, the threshold represented as a DateTimeOffset + // instance is manually converted to a UTC DateTime instance outside the query. + var date = threshold.UtcDateTime; + var tokens = await (from token in Tokens.AsTracking() - where token.CreationDate < threshold.UtcDateTime + where token.CreationDate < date where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) || (token.Authorization != null && token.Authorization.Status != Statuses.Valid) || token.ExpirationDate < DateTime.UtcNow diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index d5986036..fda3e1d3 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -1588,9 +1588,9 @@ namespace OpenIddict.Server Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - // If a code_challenge was provided, the request is always considered valid, - // whether the proof key for code exchange requirement is enforced or not. - if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) + // If a code_challenge was provided or if no authorization code is requested, the request is always + // considered valid, whether the proof key for code exchange requirement is enforced or not. + if (!string.IsNullOrEmpty(context.Request.CodeChallenge) || !context.Request.HasResponseType(ResponseTypes.Code)) { return; } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index f34329a3..7c670be5 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -1734,6 +1734,63 @@ namespace OpenIddict.Server.IntegrationTests Requirements.Features.ProofKeyForCodeExchange, It.IsAny()), Times.Never()); } + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenCodeIsNotRequestedWithPkceFeatureEnforced() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.HasRequirementAsync(application, + Requirements.Features.ProofKeyForCodeExchange, It.IsAny())) + .ReturnsAsync(true); + }); + + await using var server = await CreateServerAsync(options => + { + options.SetRevocationEndpointUris(Array.Empty()); + options.DisableAuthorizationStorage(); + options.DisableTokenStorage(); + options.DisableSlidingRefreshTokenExpiration(); + + options.Services.AddSingleton(manager); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Token + }); + + // Assert + Assert.Null(response.Code); + Assert.NotNull(response.AccessToken); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasRequirementAsync(application, + Requirements.Features.ProofKeyForCodeExchange, It.IsAny()), Times.Never()); + } + [Theory] [InlineData("custom_error", null, null)] [InlineData("custom_error", "custom_description", null)]