Browse Source

Eagerly reject grant_type=urn:ietf:params:oauth:grant-type:device_code requests that don't specify a client_id

pull/2544/head
Kévin Chalet 3 days ago
parent
commit
0fc2bfb6b9
  1. 14
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  2. 16
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  3. 94
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  4. 56
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
  5. 57
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
  6. 228
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

14
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))
{

16
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);

94
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<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.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<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
}
[Fact]
public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected()
{

56
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<CancellationToken>()))
.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<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{

57
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<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
}
[Fact]
public async Task ValidateRevocationRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
{

228
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<CancellationToken>()))
.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<CancellationToken>()), 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<CancellationToken>()))
.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<CancellationToken>()), 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<CancellationToken>()))
.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<CancellationToken>()), 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<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
}
[Theory]
[InlineData(OpenIddictServerEndpointType.DeviceAuthorization)]
[InlineData(OpenIddictServerEndpointType.Introspection)]

Loading…
Cancel
Save