diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 755354b0..b8675df7 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1116,6 +1116,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
The specified principal doesn't contain a valid claims-based identity.
+
+ The device grant must be allowed when enabling the device endpoint.
+
The security token is missing.
@@ -1467,6 +1470,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
Callback URLs must be valid absolute URLs.
+
+ The client application is not allowed to use the device code flow.
+
The '{0}' parameter shouldn't be null or empty at this point.
@@ -2029,6 +2035,12 @@ This may indicate that the hashed entry is corrupted or malformed.
The authorization request was rejected because the application '{ClientId}' was not allowed to use the '{ResponseType}' response type.
+
+ The device request was rejected because the application '{ClientId}' was not allowed to use the device code flow.
+
+
+ The device request was rejected because the application '{ClientId}' was not allowed to request the '{Scope}' scope.
+
Removes orphaned tokens and authorizations from the database.
diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
index ff6d1a8e..ccad6ce8 100644
--- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
+++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
@@ -110,6 +110,12 @@ namespace OpenIddict.Server
throw new InvalidOperationException(SR.GetResourceString(SR.ID0080));
}
+ // Ensure the device grant is allowed when the device endpoint is enabled.
+ if (options.DeviceEndpointUris.Count > 0 && !options.GrantTypes.Contains(GrantTypes.DeviceCode))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0287));
+ }
+
// Ensure the grant types/response types configuration is consistent.
foreach (var type in options.ResponseTypes)
{
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
index 6bebe7a1..d5c78349 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
@@ -41,11 +41,13 @@ namespace OpenIddict.Server
* Device request validation:
*/
ValidateClientIdParameter.Descriptor,
+ ValidateScopeParameter.Descriptor,
ValidateScopes.Descriptor,
ValidateClientId.Descriptor,
ValidateClientType.Descriptor,
ValidateClientSecret.Descriptor,
ValidateEndpointPermissions.Descriptor,
+ ValidateGrantTypePermissions.Descriptor,
ValidateScopePermissions.Descriptor,
/*
@@ -364,6 +366,43 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Contains the logic responsible of rejecting device requests that don't specify a valid scope parameter.
+ ///
+ public class ValidateScopeParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateDeviceRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject device requests that specify scope=offline_access if the refresh token flow is not enabled.
+ if (context.Request.HasScope(Scopes.OfflineAccess) && !context.Options.GrantTypes.Contains(GrantTypes.RefreshToken))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2035(Scopes.OfflineAccess));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible of rejecting authorization requests that use unregistered scopes.
/// Note: this handler partially works with the degraded mode but is not used when scope validation is disabled.
@@ -392,7 +431,7 @@ namespace OpenIddict.Server
new ValidateScopes(provider.GetService() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
- .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000)
+ .SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -486,7 +525,7 @@ namespace OpenIddict.Server
context.Reject(
error: Errors.InvalidClient,
- description: SR.GetResourceString(SR.ID2052));
+ description: SR.FormatID2052(Parameters.ClientId));
return;
}
@@ -685,6 +724,75 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Contains the logic responsible of rejecting device requests made by unauthorized applications.
+ /// Note: this handler is not used when the degraded mode is enabled or when grant type permissions are disabled.
+ ///
+ public class ValidateGrantTypePermissions : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+
+ public ValidateGrantTypePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidateGrantTypePermissions(IOpenIddictApplicationManager applicationManager)
+ => _applicationManager = applicationManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ValidateDeviceRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
+
+ var application = await _applicationManager.FindByClientIdAsync(context.ClientId);
+ if (application is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
+ }
+
+ // Reject the request if the application is not allowed to use the device code grant.
+ if (!await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.DeviceCode))
+ {
+ context.Logger.LogError(SR.GetResourceString(SR.ID6182), context.ClientId);
+
+ context.Reject(
+ error: Errors.UnauthorizedClient,
+ description: SR.GetResourceString(SR.ID2121));
+
+ return;
+ }
+
+ // Reject the request if the offline_access scope was request and
+ // if the application is not allowed to use the refresh token grant.
+ if (context.Request.HasScope(Scopes.OfflineAccess) &&
+ !await _applicationManager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken))
+ {
+ context.Logger.LogError(SR.GetResourceString(SR.ID6183), context.ClientId, Scopes.OfflineAccess);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2065(Scopes.OfflineAccess));
+
+ return;
+ }
+ }
+ }
+
///
/// Contains the logic responsible of rejecting device requests made by applications
/// that haven't been granted the appropriate grant type permission.
@@ -708,7 +816,7 @@ namespace OpenIddict.Server
.AddFilter()
.AddFilter()
.UseScopedHandler()
- .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000)
+ .SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
new file mode 100644
index 00000000..0262b309
--- /dev/null
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
@@ -0,0 +1,1330 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using OpenIddict.Abstractions;
+using Xunit;
+using static OpenIddict.Abstractions.OpenIddictConstants;
+using static OpenIddict.Server.OpenIddictServerEvents;
+using SR = OpenIddict.Abstractions.OpenIddictResources;
+
+namespace OpenIddict.Server.IntegrationTests
+{
+ public abstract partial class OpenIddictServerIntegrationTests
+ {
+ [Theory]
+ [InlineData(nameof(HttpMethod.Delete))]
+ [InlineData(nameof(HttpMethod.Get))]
+ [InlineData(nameof(HttpMethod.Head))]
+ [InlineData(nameof(HttpMethod.Options))]
+ [InlineData(nameof(HttpMethod.Put))]
+ [InlineData(nameof(HttpMethod.Trace))]
+ public async Task ExtractDeviceRequest_UnexpectedMethodReturnsAnError(string method)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.SendAsync(method, "/connect/device", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2084), response.ErrorDescription);
+ }
+
+ [Theory]
+ [InlineData("custom_error", null, null)]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, null, "custom_uri")]
+ [InlineData(null, null, null)]
+ public async Task ExtractDeviceRequest_AllowsRejectingRequest(string error, string description, string uri)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject(error, description, uri);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal(error ?? Errors.InvalidRequest, response.Error);
+ Assert.Equal(description, response.ErrorDescription);
+ Assert.Equal(uri, response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task ExtractDeviceRequest_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ExtractDeviceRequest_AllowsSkippingHandler()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.SkipRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_MissingClientIdCausesAnError()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
+ 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);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ }));
+
+ options.EnableDegradedMode();
+
+ options.Configure(options => options.GrantTypes.Remove(GrantTypes.RefreshToken));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Scope = Scopes.OfflineAccess
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2035(Scopes.OfflineAccess), response.ErrorDescription);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsRejectedWhenUnregisteredScopeIsSpecified()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ 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(CreateScopeManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByNamesAsync(
+ It.Is>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"),
+ It.IsAny()))
+ .Returns(AsyncEnumerable.Empty());
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Scope = "unregistered_scope"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidScope, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.Scope), response.ErrorDescription);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsValidatedWhenScopeRegisteredInOptionsIsSpecified()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ }));
+
+ options.EnableDegradedMode();
+ options.RegisterScopes("registered_scope");
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity());
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Scope = "registered_scope"
+ });
+
+ // Assert
+ Assert.Null(response.Error);
+ Assert.Null(response.ErrorDescription);
+ Assert.Null(response.ErrorUri);
+ Assert.NotNull(response.DeviceCode);
+ Assert.NotNull(response.UserCode);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+ var scope = new OpenIddictScope();
+
+ var manager = CreateScopeManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByNamesAsync(
+ It.Is>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"),
+ It.IsAny()))
+ .Returns(new[] { scope }.ToAsyncEnumerable());
+
+ mock.Setup(manager => manager.GetNameAsync(scope, It.IsAny()))
+ .ReturnsAsync("scope_registered_in_database");
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ }));
+
+ options.RegisterScopes("scope_registered_in_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());
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Scope = "scope_registered_in_database scope_registered_in_options"
+ });
+
+ // Assert
+ Assert.Null(response.Error);
+ Assert.Null(response.ErrorDescription);
+ Assert.Null(response.ErrorUri);
+ Assert.NotNull(response.DeviceCode);
+ Assert.NotNull(response.UserCode);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_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/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_ClientSecretCannotBeUsedByPublicClients()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+ });
+
+ 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",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_ClientSecretIsRequiredForNonPublicClients()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ 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",
+ ClientSecret = null
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsRejectedWhenClientCredentialsAreInvalid()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(false);
+
+ mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ 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",
+ ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2055), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.HasPermissionAsync(application,
+ Permissions.Endpoints.Device, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+
+ options.Configure(options => options.IgnoreEndpointPermissions = false);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal(Errors.UnauthorizedClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2056), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.Endpoints.Device, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.HasPermissionAsync(application,
+ Permissions.GrantTypes.DeviceCode, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+
+ options.Configure(options => options.IgnoreGrantTypePermissions = false);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal(Errors.UnauthorizedClient, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2121), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce());
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.GrantTypes.DeviceCode, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenPermissionIsNotGranted()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.HasPermissionAsync(application,
+ Permissions.GrantTypes.DeviceCode, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.HasPermissionAsync(application,
+ Permissions.GrantTypes.RefreshToken, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+
+ options.Configure(options => options.IgnoreGrantTypePermissions = false);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Scope = Scopes.OfflineAccess
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2065(Scopes.OfflineAccess), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.GrantTypes.RefreshToken, It.IsAny()), Times.Once());
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_RequestIsRejectedWhenScopePermissionIsNotGranted()
+ {
+ // Arrange
+ var application = new OpenIddictApplication();
+
+ var manager = CreateApplicationManager(mock =>
+ {
+ mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()))
+ .ReturnsAsync(application);
+
+ mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.HasPermissionAsync(application,
+ Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny()))
+ .ReturnsAsync(true);
+
+ mock.Setup(manager => manager.HasPermissionAsync(application,
+ Permissions.Prefixes.Scope + Scopes.Email, It.IsAny()))
+ .ReturnsAsync(false);
+ });
+
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.Services.AddSingleton(manager);
+
+ options.RegisterScopes(Scopes.Email, Scopes.Profile);
+ options.Configure(options => options.IgnoreScopePermissions = false);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Scope = "openid offline_access profile email"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2051), response.ErrorDescription);
+
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.Prefixes.Scope + Scopes.OpenId, It.IsAny()), Times.Never());
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.Prefixes.Scope + Scopes.OfflineAccess, It.IsAny()), Times.Never());
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny()), Times.Once());
+ Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
+ Permissions.Prefixes.Scope + Scopes.Email, It.IsAny()), Times.Once());
+ }
+
+ [Theory]
+ [InlineData("custom_error", null, null)]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, null, "custom_uri")]
+ [InlineData(null, null, null)]
+ public async Task ValidateDeviceRequest_AllowsRejectingRequest(string error, string description, string uri)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject(error, description, uri);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal(error ?? Errors.InvalidRequest, response.Error);
+ Assert.Equal(description, response.ErrorDescription);
+ Assert.Equal(uri, response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ValidateDeviceRequest_AllowsSkippingHandler()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.SkipRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string?) response["name"]);
+ }
+
+ [Theory]
+ [InlineData("custom_error", null, null)]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, null, "custom_uri")]
+ [InlineData(null, null, null)]
+ public async Task HandleDeviceRequest_AllowsRejectingRequest(string error, string description, string uri)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject(error, description, uri);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal(error ?? Errors.InvalidRequest, response.Error);
+ Assert.Equal(description, response.ErrorDescription);
+ Assert.Equal(uri, response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task HandleDeviceRequest_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task HandleDeviceRequest_AllowsSkippingHandler()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.SkipRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ApplyDeviceResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity());
+
+ return default;
+ }));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ApplyDeviceResponse_ResponseContainsCustomParameters()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity());
+
+ return default;
+ }));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Response["custom_parameter"] = "custom_value";
+ context.Response["parameter_with_multiple_values"] = new[]
+ {
+ "custom_value_1",
+ "custom_value_2"
+ };
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/device", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam"
+ });
+
+ // Assert
+ Assert.Equal("custom_value", (string?) response["custom_parameter"]);
+ Assert.Equal(new[] { "custom_value_1", "custom_value_2" }, (string[]?) response["parameter_with_multiple_values"]);
+ }
+
+ [Theory]
+ [InlineData(nameof(HttpMethod.Delete))]
+ [InlineData(nameof(HttpMethod.Head))]
+ [InlineData(nameof(HttpMethod.Options))]
+ [InlineData(nameof(HttpMethod.Put))]
+ [InlineData(nameof(HttpMethod.Trace))]
+ public async Task ExtractVerificationRequest_UnexpectedMethodReturnsAnError(string method)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync();
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.SendAsync(method, "/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.GetResourceString(SR.ID2084), response.ErrorDescription);
+ }
+
+ [Theory]
+ [InlineData("custom_error", null, null)]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, null, "custom_uri")]
+ [InlineData(null, null, null)]
+ public async Task ExtractVerificationRequest_AllowsRejectingRequest(string error, string description, string uri)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject(error, description, uri);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal(error ?? Errors.InvalidRequest, response.Error);
+ Assert.Equal(description, response.ErrorDescription);
+ Assert.Equal(uri, response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task ExtractVerificationRequest_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.GetAsync("/connect/verification");
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ExtractVerificationRequest_AllowsSkippingHandler()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.SkipRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.GetAsync("/connect/verification");
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string?) response["name"]);
+ }
+
+ [Theory]
+ [InlineData("custom_error", null, null)]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, null, "custom_uri")]
+ [InlineData(null, null, null)]
+ public async Task ValidateVerificationRequest_AllowsRejectingRequest(string error, string description, string uri)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject(error, description, uri);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal(error ?? Errors.InvalidRequest, response.Error);
+ Assert.Equal(description, response.ErrorDescription);
+ Assert.Equal(uri, response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task ValidateVerificationRequest_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ValidateVerificationRequest_AllowsSkippingHandler()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.SkipRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string?) response["name"]);
+ }
+
+ [Theory]
+ [InlineData("custom_error", null, null)]
+ [InlineData("custom_error", "custom_description", null)]
+ [InlineData("custom_error", "custom_description", "custom_uri")]
+ [InlineData(null, "custom_description", null)]
+ [InlineData(null, "custom_description", "custom_uri")]
+ [InlineData(null, null, "custom_uri")]
+ [InlineData(null, null, null)]
+ public async Task HandleVerificationRequest_AllowsRejectingRequest(string error, string description, string uri)
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject(error, description, uri);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal(error ?? Errors.InvalidRequest, response.Error);
+ Assert.Equal(description, response.ErrorDescription);
+ Assert.Equal(uri, response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task HandleVerificationRequest_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task HandleVerificationRequest_AllowsSkippingHandler()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.SkipRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Magnifique", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ApplyVerificationResponse_AllowsHandlingResponse()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject();
+
+ return default;
+ }));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Transaction.SetProperty("custom_response", new
+ {
+ name = "Bob le Bricoleur"
+ });
+
+ context.HandleRequest();
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("Bob le Bricoleur", (string?) response["name"]);
+ }
+
+ [Fact]
+ public async Task ApplyVerificationResponse_ResponseContainsCustomParameters()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Reject();
+
+ return default;
+ }));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Response["custom_parameter"] = "custom_value";
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/verification", new OpenIddictRequest());
+
+ // Assert
+ Assert.Equal("custom_value", (string?) response["custom_parameter"]);
+ }
+ }
+}
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
index 2b8b38a0..b369edac 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
@@ -411,6 +411,8 @@ namespace OpenIddict.Server.IntegrationTests
options.Configure(options => options.GrantTypes.Clear());
options.Configure(options => options.GrantTypes.Add(GrantTypes.Implicit));
options.Configure(options => options.ResponseTypes.Clear());
+ options.Configure(options => options.DeviceEndpointUris.Clear());
+ options.Configure(options => options.VerificationEndpointUris.Clear());
options.SetTokenEndpointUris(Array.Empty());
});
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
index 9649a195..32e92cbf 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
@@ -3527,14 +3527,17 @@ namespace OpenIddict.Server.IntegrationTests
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetConfigurationEndpointUris("/.well-known/openid-configuration")
.SetCryptographyEndpointUris("/.well-known/jwks")
+ .SetDeviceEndpointUris("/connect/device")
.SetIntrospectionEndpointUris("/connect/introspect")
.SetLogoutEndpointUris("/connect/logout")
.SetRevocationEndpointUris("/connect/revoke")
.SetTokenEndpointUris("/connect/token")
- .SetUserinfoEndpointUris("/connect/userinfo");
+ .SetUserinfoEndpointUris("/connect/userinfo")
+ .SetVerificationEndpointUris("/connect/verification");
options.AllowAuthorizationCodeFlow()
.AllowClientCredentialsFlow()
+ .AllowDeviceCodeFlow()
.AllowHybridFlow()
.AllowImplicitFlow()
.AllowNoneFlow()
@@ -3563,6 +3566,9 @@ namespace OpenIddict.Server.IntegrationTests
options.AddEventHandler(builder =>
builder.UseInlineHandler(context => default));
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context => default));
+
options.AddEventHandler(builder =>
builder.UseInlineHandler(context => default));
@@ -3574,6 +3580,15 @@ namespace OpenIddict.Server.IntegrationTests
options.AddEventHandler(builder =>
builder.UseInlineHandler(context => default));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context => default));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context => default));
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context => default));
});
}