From e0c748f046527c200ff869da8f933469f4858ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 10 Sep 2021 17:55:25 +0200 Subject: [PATCH] Introduce a simpler way to return additional parameters from the Handle*Request events that trigger a sign-in response --- .../OpenIddictServerOwinProperties.cs | 6 +- .../OpenIddictServerEvents.Authentication.cs | 18 +++ .../OpenIddictServerEvents.Device.cs | 47 +++++++ .../OpenIddictServerEvents.Discovery.cs | 3 +- .../OpenIddictServerEvents.Exchange.cs | 19 +++ .../OpenIddictServerEvents.Introspection.cs | 5 +- .../OpenIddictServerEvents.Revocation.cs | 6 - .../OpenIddictServerEvents.Userinfo.cs | 3 +- .../OpenIddictServerEvents.cs | 6 + ...OpenIddictServerHandlers.Authentication.cs | 8 ++ .../OpenIddictServerHandlers.Device.cs | 16 +++ .../OpenIddictServerHandlers.Exchange.cs | 8 ++ .../OpenIddictServerHandlers.cs | 8 ++ .../OpenIddictServerTransaction.cs | 3 +- .../OpenIddictValidationOwinProperties.cs | 6 +- .../OpenIddictValidationTransaction.cs | 3 +- ...ctServerIntegrationTests.Authentication.cs | 45 +++++++ ...OpenIddictServerIntegrationTests.Device.cs | 123 +++++++++++++++++- ...enIddictServerIntegrationTests.Exchange.cs | 44 +++++++ 19 files changed, 353 insertions(+), 24 deletions(-) diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs index 1f0b0bc1..20397037 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs @@ -35,7 +35,9 @@ namespace OpenIddict.Server.Owin IDictionary? items, IDictionary? parameters) : base(items) - => Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); + => Parameters = parameters is not null ? + new(parameters, StringComparer.Ordinal) : + new(StringComparer.Ordinal); /// /// Gets the collection of parameters passed to the authentication handler. @@ -44,7 +46,7 @@ namespace OpenIddict.Server.Owin /// Note: these properties are not intended for serialization or persistence, /// only for flowing data between call sites. /// - public IDictionary Parameters { get; } + public Dictionary Parameters { get; } /// /// Gets a parameter from the collection. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs index cf85ba36..7dd1ab7d 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.Security.Claims; using OpenIddict.Abstractions; using SR = OpenIddict.Abstractions.OpenIddictResources; @@ -118,11 +119,28 @@ namespace OpenIddict.Server set => Transaction.Request = value; } + /// + /// Gets the additional parameters returned to the client application. + /// + public Dictionary Parameters { get; private set; } + = new(StringComparer.Ordinal); + /// /// Allows OpenIddict to return a sign-in response using the specified principal. /// /// The claims principal. public void SignIn(ClaimsPrincipal principal) => Principal = principal; + + /// + /// Allows OpenIddict to return a sign-in response using the specified principal. + /// + /// The claims principal. + /// The additional parameters returned to the client application. + public void SignIn(ClaimsPrincipal principal, IDictionary parameters) + { + Principal = principal; + Parameters = new(parameters, StringComparer.Ordinal); + } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs index f494f7bb..e85ca151 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs @@ -4,6 +4,8 @@ * the license and the contributors participating to this project. */ +using System; +using System.Collections.Generic; using System.Security.Claims; using OpenIddict.Abstractions; @@ -81,6 +83,29 @@ namespace OpenIddict.Server get => Transaction.Request!; set => Transaction.Request = value; } + + /// + /// Gets the additional parameters returned to the client application. + /// + public Dictionary Parameters { get; private set; } + = new(StringComparer.Ordinal); + + /// + /// Allows OpenIddict to return a sign-in response using the specified principal. + /// + /// The claims principal. + public void SignIn(ClaimsPrincipal principal) => Principal = principal; + + /// + /// Allows OpenIddict to return a sign-in response using the specified principal. + /// + /// The claims principal. + /// The additional parameters returned to the client application. + public void SignIn(ClaimsPrincipal principal, IDictionary parameters) + { + Principal = principal; + Parameters = new(parameters, StringComparer.Ordinal); + } } /// @@ -198,11 +223,33 @@ namespace OpenIddict.Server set => Transaction.Request = value; } + /// + /// Gets the additional parameters returned to the caller. + /// + /// + /// Note: by default, this property is not used as empty responses are typically + /// returned for user verification requests. To return a different response, a + /// custom event handler must be registered to handle user verification responses. + /// + public Dictionary Parameters { get; private set; } + = new(StringComparer.Ordinal); + /// /// Allows OpenIddict to return a sign-in response using the specified principal. /// /// The claims principal. public void SignIn(ClaimsPrincipal principal) => Principal = principal; + + /// + /// Allows OpenIddict to return a sign-in response using the specified principal. + /// + /// The claims principal. + /// The additional parameters returned to the client application. + public void SignIn(ClaimsPrincipal principal, IDictionary parameters) + { + Principal = principal; + Parameters = new(parameters, StringComparer.Ordinal); + } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs index 3088cd2a..00484ac6 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs @@ -87,8 +87,7 @@ namespace OpenIddict.Server /// /// Gets the additional parameters returned to the client application. /// - public IDictionary Metadata { get; } = - new Dictionary(StringComparer.Ordinal); + public Dictionary Metadata { get; } = new(StringComparer.Ordinal); /// /// Gets or sets the authorization endpoint address. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs index 4bace5f1..64c7cc8c 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs @@ -4,6 +4,8 @@ * the license and the contributors participating to this project. */ +using System; +using System.Collections.Generic; using System.Security.Claims; using OpenIddict.Abstractions; @@ -88,11 +90,28 @@ namespace OpenIddict.Server set => Transaction.Request = value; } + /// + /// Gets the additional parameters returned to the client application. + /// + public Dictionary Parameters { get; private set; } + = new(StringComparer.Ordinal); + /// /// Allows OpenIddict to return a sign-in response using the specified principal. /// /// The claims principal. public void SignIn(ClaimsPrincipal principal) => Principal = principal; + + /// + /// Allows OpenIddict to return a sign-in response using the specified principal. + /// + /// The claims principal. + /// The additional parameters returned to the client application. + public void SignIn(ClaimsPrincipal principal, IDictionary parameters) + { + Principal = principal; + Parameters = new(parameters, StringComparer.Ordinal); + } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs index e899e844..5101bdb1 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs @@ -101,10 +101,9 @@ namespace OpenIddict.Server public ClaimsPrincipal Principal { get; set; } = default!; /// - /// Gets the additional claims returned to the caller. + /// Gets the additional claims returned to the client application. /// - public IDictionary Claims { get; } = - new Dictionary(StringComparer.Ordinal); + public Dictionary Claims { get; } = new(StringComparer.Ordinal); /// /// Gets the list of audiences returned to the caller diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs index da0e12d7..e8ecf9d9 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs @@ -99,12 +99,6 @@ namespace OpenIddict.Server /// Gets or sets the security principal extracted from the revoked token. /// public ClaimsPrincipal Principal { get; set; } = default!; - - /// - /// Gets the authentication ticket. - /// - public IDictionary Claims { get; } - = new Dictionary(StringComparer.Ordinal); } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs index 9143c88c..d7f4b602 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs @@ -98,8 +98,7 @@ namespace OpenIddict.Server /// /// Gets the additional claims returned to the client application. /// - public IDictionary Claims { get; } = - new Dictionary(StringComparer.Ordinal); + public Dictionary Claims { get; } = new(StringComparer.Ordinal); /// /// Gets or sets the value used for the "address" claim. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 2a0d167d..eba4369e 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Security.Claims; using Microsoft.Extensions.Logging; @@ -522,6 +523,11 @@ namespace OpenIddict.Server set => Transaction.Response = value; } + /// + /// Gets the additional parameters returned to the client application. + /// + public Dictionary Parameters { get; } = new(StringComparer.Ordinal); + /// /// Gets or sets a boolean indicating whether an access token /// should be generated (and optionally returned to the client). diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index ff339473..4bde2eb5 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -256,6 +256,14 @@ namespace OpenIddict.Server Response = new OpenIddictResponse() }; + if (notification.Parameters.Count > 0) + { + foreach (var parameter in notification.Parameters) + { + @event.Parameters.Add(parameter.Key, parameter.Value); + } + } + await _dispatcher.DispatchAsync(@event); if (@event.IsRequestHandled) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs index b183298f..64b07b25 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -253,6 +253,14 @@ namespace OpenIddict.Server Response = new OpenIddictResponse() }; + if (notification.Parameters.Count > 0) + { + foreach (var parameter in notification.Parameters) + { + @event.Parameters.Add(parameter.Key, parameter.Value); + } + } + await _dispatcher.DispatchAsync(@event); if (@event.IsRequestHandled) @@ -1051,6 +1059,14 @@ namespace OpenIddict.Server Response = new OpenIddictResponse() }; + if (notification.Parameters.Count > 0) + { + foreach (var parameter in notification.Parameters) + { + @event.Parameters.Add(parameter.Key, parameter.Value); + } + } + await _dispatcher.DispatchAsync(@event); if (@event.IsRequestHandled) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index f092a272..356cbab5 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -263,6 +263,14 @@ namespace OpenIddict.Server Response = new OpenIddictResponse() }; + if (notification.Parameters.Count > 0) + { + foreach (var parameter in notification.Parameters) + { + @event.Parameters.Add(parameter.Key, parameter.Value); + } + } + await _dispatcher.DispatchAsync(@event); if (@event.IsRequestHandled) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 7094826a..45b7ef8c 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -2928,6 +2928,14 @@ namespace OpenIddict.Server } } + if (context.Parameters.Count > 0) + { + foreach (var parameter in context.Parameters) + { + context.Response.SetParameter(parameter.Key, parameter.Value); + } + } + return default; static Uri? GetEndpointAbsoluteUri(Uri? issuer, Uri? endpoint) diff --git a/src/OpenIddict.Server/OpenIddictServerTransaction.cs b/src/OpenIddict.Server/OpenIddictServerTransaction.cs index 710d48f5..f71ff732 100644 --- a/src/OpenIddict.Server/OpenIddictServerTransaction.cs +++ b/src/OpenIddict.Server/OpenIddictServerTransaction.cs @@ -39,8 +39,7 @@ namespace OpenIddict.Server /// /// Gets the additional properties associated with the current request. /// - public IDictionary Properties { get; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Gets or sets the current OpenID Connect request. diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs index 6655081e..1dfb6230 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs @@ -35,7 +35,9 @@ namespace OpenIddict.Validation.Owin IDictionary? items, IDictionary? parameters) : base(items) - => Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); + => Parameters = parameters is not null ? + new(parameters, StringComparer.Ordinal) : + new(StringComparer.Ordinal); /// /// Gets the collection of parameters passed to the authentication handler. @@ -44,7 +46,7 @@ namespace OpenIddict.Validation.Owin /// Note: these properties are not intended for serialization or persistence, /// only for flowing data between call sites. /// - public IDictionary Parameters { get; } + public Dictionary Parameters { get; } /// /// Gets a parameter from the collection. diff --git a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs index 1c75b6c3..7377c3f7 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs @@ -39,8 +39,7 @@ namespace OpenIddict.Validation /// /// Gets the additional properties associated with the current request. /// - public IDictionary Properties { get; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Gets or sets the current OpenID Connect request. diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index 7c670be5..7ce6437e 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -2011,6 +2011,51 @@ namespace OpenIddict.Server.IntegrationTests Assert.Equal("Bob le Magnifique", (string?) response["name"]); } + [Fact] + public async Task HandleAuthorizationRequest_ResponseContainsCustomParameters() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + context.Parameters["custom_parameter"] = "custom_value"; + context.Parameters["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/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Token + }); + + // Assert + Assert.Null(response.Error); + Assert.Null(response.ErrorDescription); + Assert.Null(response.ErrorUri); + Assert.NotNull(response.AccessToken); + 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("code", ResponseModes.Query)] [InlineData("code id_token", ResponseModes.Fragment)] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs index 7b89a304..a689657e 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs @@ -17,6 +17,7 @@ using OpenIddict.Abstractions; using Xunit; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.IntegrationTests @@ -173,8 +174,6 @@ namespace OpenIddict.Server.IntegrationTests .ReturnsAsync(true); })); - options.EnableDegradedMode(); - options.Configure(options => options.GrantTypes.Remove(GrantTypes.RefreshToken)); }); @@ -251,8 +250,11 @@ namespace OpenIddict.Server.IntegrationTests .ReturnsAsync(true); })); - options.EnableDegradedMode(); options.RegisterScopes("registered_scope"); + options.SetRevocationEndpointUris(Array.Empty()); + options.DisableAuthorizationStorage(); + options.DisableTokenStorage(); + options.DisableSlidingRefreshTokenExpiration(); options.AddEventHandler(builder => builder.UseInlineHandler(context => @@ -885,6 +887,58 @@ namespace OpenIddict.Server.IntegrationTests Assert.Equal("Bob le Magnifique", (string?) response["name"]); } + [Fact] + public async Task HandleDeviceRequest_ResponseContainsCustomParameters() + { + // 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.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity()); + + context.Parameters["custom_parameter"] = "custom_value"; + context.Parameters["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.Null(response.Error); + Assert.Null(response.ErrorDescription); + Assert.Null(response.ErrorUri); + Assert.NotNull(response.DeviceCode); + Assert.Equal("custom_value", (string?) response["custom_parameter"]); + Assert.Equal(new[] { "custom_value_1", "custom_value_2" }, (string[]?) response["parameter_with_multiple_values"]); + } + [Fact] public async Task ApplyDeviceResponse_AllowsHandlingResponse() { @@ -1267,6 +1321,69 @@ namespace OpenIddict.Server.IntegrationTests Assert.Equal("Bob le Magnifique", (string?) response["name"]); } + [Fact] + public async Task HandleVerificationRequest_ResponseContainsCustomParameters() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("WDJB-MJHT", context.Token); + Assert.Equal(new[] { TokenTypeHints.UserCode }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity()) + .SetTokenType(TokenTypeHints.UserCode); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + context.Parameters["custom_parameter"] = "custom_value"; + context.Parameters["parameter_with_multiple_values"] = new[] + { + "custom_value_1", + "custom_value_2" + }; + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Transaction.SetProperty("custom_response", context.Response); + context.HandleRequest(); + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/verification", new OpenIddictRequest + { + UserCode = "WDJB-MJHT" + }); + + // Assert + Assert.Equal("custom_value", (string?) response["custom_parameter"]); + Assert.Equal(new[] { "custom_value_1", "custom_value_2" }, (string[]?) response["parameter_with_multiple_values"]); + } + [Fact] public async Task ApplyVerificationResponse_AllowsHandlingResponse() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index 7c2d2b68..4f2f4612 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -4024,6 +4024,50 @@ namespace OpenIddict.Server.IntegrationTests Assert.Equal("Bob le Magnifique", (string?) response["name"]); } + [Fact] + public async Task HandleTokenRequest_ResponseContainsCustomParameters() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + context.Parameters["custom_parameter"] = "custom_value"; + context.Parameters["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/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Null(response.Error); + Assert.Null(response.ErrorDescription); + Assert.Null(response.ErrorUri); + Assert.NotNull(response.AccessToken); + Assert.Equal("custom_value", (string?) response["custom_parameter"]); + Assert.Equal(new[] { "custom_value_1", "custom_value_2" }, (string[]?) response["parameter_with_multiple_values"]); + } + [Fact] public async Task ApplyTokenResponse_AllowsHandlingResponse() {