diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 45d6733f..a0af0b54 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -1611,9 +1611,24 @@ namespace OpenIddict.Server // is active even if the degraded mode is enabled and ensures that a code_verifier is sent if a // code_challenge was stored in the authorization code when the authorization request was handled. + // Validate that the token request did not include the code_verifier parameter if the + // authorization request was not provided with both code_challenge and code_challenge_method. + var challenge = context.Principal.GetClaim(Claims.Private.CodeChallenge); + if (!string.IsNullOrEmpty(context.Request.CodeVerifier) && string.IsNullOrEmpty(challenge)) + { + context.Logger.LogError("The token request was rejected because a 'code_verifier' " + + "parameter was presented but 'code_challenge' and/or 'code_challenge_method' " + + "was not a part of the authorization request."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'code_verifier' parameter is uncalled for in this request."); + + return default; + } + // If a code challenge was initially sent in the authorization request and associated with the // code, validate the code verifier to ensure the token request is sent by a legit caller. - var challenge = context.Principal.GetClaim(Claims.Private.CodeChallenge); if (string.IsNullOrEmpty(challenge)) { return default; diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index a1f91427..08b67d2b 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -562,6 +562,46 @@ namespace OpenIddict.Server.FunctionalTests "redirection endpoint the authorization code was initially sent to.", response.ErrorDescription); } + [Fact] + public async Task ValidateTokenRequest_RequestCausesErrorWhenSendingCodeVerifier() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + CodeVerifier = "AbCd97394879834759873497549237098273498072304987523948673248972349857982345", + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'code_verifier' parameter is uncalled for in this request.", response.ErrorDescription); + } + [Fact] public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenCodeVerifierIsMissing() {