From 0fc2bfb6b957e02faeaf1761c407d8bf4a83379a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 6 Sep 2026 13:28:58 +0200 Subject: [PATCH] Eagerly reject grant_type=urn:ietf:params:oauth:grant-type:device_code requests that don't specify a client_id --- .../OpenIddictServerHandlers.Exchange.cs | 14 +- .../OpenIddictServerHandlers.cs | 16 +- ...enIddictServerIntegrationTests.Exchange.cs | 94 +++----- ...ictServerIntegrationTests.Introspection.cs | 56 ----- ...IddictServerIntegrationTests.Revocation.cs | 57 ----- .../OpenIddictServerIntegrationTests.cs | 228 ++++++++++++++++++ 6 files changed, 283 insertions(+), 182 deletions(-) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 3a1568c3..845d9b15 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -412,17 +412,21 @@ public static partial class OpenIddictServerHandlers { ArgumentNullException.ThrowIfNull(context); - if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsClientCredentialsGrantType()) + if (!context.Request.IsAuthorizationCodeGrantType() && + !context.Request.IsClientCredentialsGrantType() && + !context.Request.IsDeviceCodeGrantType()) { return ValueTask.CompletedTask; } - // Reject grant_type=authorization_code and grant_type=client_credentials requests that - // don't specify a client_id or a client_assertion, as the client identity MUST be sent + // Reject grant_type=authorization_code, grant_type=client_credentials and + // grant_type=urn:ietf:params:oauth:grant-type:device_code requests that don't + // specify a client_id or a client_assertion, as the client identity MUST be sent // by the client application (even when using mTLS OAuth 2.0 client authentication). // - // See https://tools.ietf.org/html/rfc6749#section-4.1.3 - // and https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information. + // See https://tools.ietf.org/html/rfc6749#section-4.1.3, + // https://tools.ietf.org/html/rfc6749#section-4.4.1 and + // https://tools.ietf.org/html/rfc8628#section-3.4 for more information. if (string.IsNullOrEmpty(context.Request.ClientId) && string.IsNullOrEmpty(context.Request.ClientAssertion)) { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index e2c622f9..0a598dfc 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -951,11 +951,12 @@ public static partial class OpenIddictServerHandlers case OpenIddictServerEndpointType.Revocation when context.Options.AcceptAnonymousClients: return; - // Note: the authorization code and client credentials grant types never - // allow anonymous clients, even if the corresponding option is enabled. + // Note: the authorization code, device code and client credentials grant types + // never allow anonymous clients, even if the corresponding option is enabled. case OpenIddictServerEndpointType.Token when context.Options.AcceptAnonymousClients && !context.Request.IsAuthorizationCodeGrantType() && - !context.Request.IsClientCredentialsGrantType(): + !context.Request.IsClientCredentialsGrantType() && + !context.Request.IsDeviceCodeGrantType(): return; // Note: despite being conceptually similar to the token endpoint, the pushed authorization @@ -963,6 +964,15 @@ public static partial class OpenIddictServerHandlers // for both regular authorization requests and pushed authorization requests. // // See https://datatracker.ietf.org/doc/html/rfc9126#section-2.1 for more information. + + // Note: similarly, the device authorization endpoint doesn't allow anonymous clients, + // as a client_id parameter is always required by the specification if the client + // doesn't authenticate using a different method. + // + // See https://datatracker.ietf.org/doc/html/rfc9126#section-3.1 for more information. + case OpenIddictServerEndpointType.DeviceAuthorization: + case OpenIddictServerEndpointType.PushedAuthorization: + break; } context.Logger.LogInformation(6220, SR.GetResourceString(SR.ID6220), Parameters.ClientId); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index c03e7524..bcfc9427 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -193,6 +193,27 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); } + [Fact] + public async Task ValidateTokenRequest_MissingClientIdCausesAnErrorForDeviceCodeRequests() + { + // Arrange + await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = null, + DeviceCode = "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", + GrantType = GrantTypes.DeviceCode + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); + } + [Fact] public async Task ValidateTokenRequest_MissingCodeCausesAnError() { @@ -943,6 +964,17 @@ public abstract partial class OpenIddictServerIntegrationTests builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); }); + options.Services.AddSingleton(CreateApplicationManager(mock => + { + var application = new OpenIddictApplication(); + + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) + .ReturnsAsync(true); + })); + options.Services.AddSingleton(manager); }); @@ -951,6 +983,7 @@ public abstract partial class OpenIddictServerIntegrationTests // Act var response = await client.PostAsync("/connect/token", new OpenIddictRequest { + ClientId = "Fabrikam", GrantType = GrantTypes.DeviceCode, DeviceCode = "g43LaWCUrz2RaLILz2L1bg1bOpMSv1hGrH12IIkB9H4" }); @@ -2499,67 +2532,6 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal(SR.FormatID8000(SR.ID2057), response.ErrorUri); } - [Fact] - public async Task ValidateTokenRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - options.Configure(options => options.AcceptAnonymousClients = false); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = null, - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); - } - - [Fact] - public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(mock => - { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - } - [Fact] public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index 2d67d10d..43ee0e52 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs @@ -250,62 +250,6 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal(SR.FormatID8000(SR.ID2087), response.ErrorUri); } - [Fact] - public async Task ValidateIntrospectionRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.Configure(options => options.AcceptAnonymousClients = false); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest - { - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); - } - - [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(mock => - { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest - { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" - }); - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); - } - [Fact] public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs index 8dce3ff0..fe16602a 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs @@ -247,63 +247,6 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal(SR.FormatID8000(SR.ID2087), response.ErrorUri); } - [Fact] - public async Task ValidateRevocationRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.Configure(options => options.AcceptAnonymousClients = false); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); - } - - [Fact] - public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCannotBeFound() - { - // Arrange - var manager = CreateApplicationManager(mock => - { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - } - [Fact] public async Task ValidateRevocationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 466d4240..4c32c490 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -903,6 +903,234 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.NotNull(response.AccessToken); } + [Fact] + public async Task ProcessAuthentication_MissingClientIdInDeviceRequestCausesAnError() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.Configure(options => options.AcceptAnonymousClients = false); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/device", new OpenIddictRequest + { + ClientId = null + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); + } + + [Fact] + public async Task ProcessAuthentication_InvalidClientIdInDeviceRequestCausesAnError() + { + // Arrange + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/device", new OpenIddictRequest + { + ClientId = "Fabrikam" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessAuthentication_MissingClientIdInIntrospectionRequestCausesAnError() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.Configure(options => options.AcceptAnonymousClients = false); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); + } + + [Fact] + public async Task ProcessAuthentication_InvalidClientIdInIntrospectionRequestCausesAnError() + { + // Arrange + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessAuthentication_MissingClientIdInRevocationRequestCausesAnError() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.Configure(options => options.AcceptAnonymousClients = false); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); + } + + [Fact] + public async Task ProcessAuthentication_InvalidClientIdInRevocationRequestCausesAnError() + { + // Arrange + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessAuthentication_MissingClientIdInTokenRequestCausesAnError() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.Configure(options => options.AcceptAnonymousClients = false); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = null, + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); + } + + [Fact] + public async Task ProcessAuthentication_InvalidClientIdInTokenRequestCausesAnError() + { + // Arrange + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + } + [Theory] [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)] [InlineData(OpenIddictServerEndpointType.Introspection)]