From fd63da76dfe44d71c6d8c710f837f6cd194d66c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 17 Mar 2020 03:42:24 +0100 Subject: [PATCH] Allow returning custom challenge/sign-in/sign-out parameters via AuthenticationProperties.Parameters --- .../Controllers/AuthorizationController.cs | 1 - .../Primitives/OpenIddictMessage.cs | 3 +- .../OpenIddictServerAspNetCoreHandlers.cs | 75 ++++++- .../OpenIddictServerOwinHandlers.cs | 2 +- .../OpenIddictValidationAspNetCoreHandlers.cs | 2 +- .../OpenIddictValidationOwinHandlers.cs | 2 +- ...nIddictServerAspNetCoreIntegrationTests.cs | 201 +++++++++++++++++- .../OpenIddictServerIntegrationTests.cs | 32 --- .../OpenIddictServerOwinIntegrationTests.cs | 34 ++- 9 files changed, 305 insertions(+), 47 deletions(-) diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 526dce21..c28d8730 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs index ab969867..e75c6701 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs @@ -186,7 +186,8 @@ namespace OpenIddict.Abstractions = new Dictionary(StringComparer.Ordinal); /// - /// Adds a parameter. + /// Adds a parameter. Note: if a parameter with the + /// same name was already added, this method has no effect. /// /// The parameter name. /// The parameter value. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index c4d47886..4c06a6c3 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -43,7 +43,18 @@ namespace OpenIddict.Server.AspNetCore /* * Challenge processing: */ - AttachHostChallengeError.Descriptor) + AttachHostChallengeError.Descriptor, + AttachHostParameters.Descriptor, + + /* + * Sign-in processing: + */ + AttachHostParameters.Descriptor, + + /* + * Sign-out processing: + */ + AttachHostParameters.Descriptor) .AddRange(Authentication.DefaultHandlers) .AddRange(Device.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) @@ -328,6 +339,66 @@ namespace OpenIddict.Server.AspNetCore } } + /// + /// Contains the logic responsible of attaching custom parameters stored in the ASP.NET Core authentication properties. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class AttachHostParameters : IOpenIddictServerHandler where TContext : BaseContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue - 150_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var properties = context.Transaction.GetProperty(typeof(AuthenticationProperties).FullName); + if (properties == null) + { + return default; + } + + foreach (var parameter in properties.Parameters) + { + // Note: AddParameter() is used to ensure existing parameters are not overriden. + context.Response.AddParameter(parameter.Key, parameter.Value switch + { + OpenIddictParameter value => value, + JsonElement value => value, + bool value => value, + int value => value, + long value => value, + string value => value, + string[] value => value, + + _ => throw new InvalidOperationException(new StringBuilder() + .Append("Only strings, booleans, integers, arrays of strings and instances of type ") + .Append("'OpenIddictParameter' or 'JsonElement' can be returned as custom parameters.") + .ToString()) + }); + } + + return default; + } + } + /// /// Contains the logic responsible of extracting OpenID Connect requests from GET HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. @@ -801,7 +872,7 @@ namespace OpenIddict.Server.AspNetCore } /// - /// Contains the logic responsible of attaching an appropriate HTTP response code header. + /// Contains the logic responsible of attaching an appropriate HTTP status code. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class AttachHttpResponseCode : IOpenIddictServerHandler where TContext : BaseRequestContext diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 17ab836b..7d787c4c 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -804,7 +804,7 @@ namespace OpenIddict.Server.Owin } /// - /// Contains the logic responsible of attaching an appropriate HTTP response code header. + /// Contains the logic responsible of attaching an appropriate HTTP status code. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// public class AttachHttpResponseCode : IOpenIddictServerHandler where TContext : BaseRequestContext diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index 4e9d0f6a..9aac6042 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs @@ -277,7 +277,7 @@ namespace OpenIddict.Validation.AspNetCore } /// - /// Contains the logic responsible of attaching an appropriate HTTP response code header. + /// Contains the logic responsible of attaching an appropriate HTTP status code. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class AttachHttpResponseCode : IOpenIddictValidationHandler where TContext : BaseRequestContext diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index 277088b9..ece36bfb 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -279,7 +279,7 @@ namespace OpenIddict.Validation.Owin } /// - /// Contains the logic responsible of attaching an appropriate HTTP response code header. + /// Contains the logic responsible of attaching an appropriate HTTP status code. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// public class AttachHttpResponseCode : IOpenIddictValidationHandler where TContext : BaseRequestContext diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs index 5382cc93..110393b7 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs @@ -27,6 +27,77 @@ namespace OpenIddict.Server.AspNetCore.FunctionalTests { public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ProcessChallenge_ReturnsParametersFromAuthenticationProperties() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetTokenEndpointUris("/challenge/custom"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/challenge/custom", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.True((bool) response["boolean_parameter"]); + Assert.Equal(JsonValueKind.True, ((JsonElement) response["boolean_parameter"]).ValueKind); + Assert.Equal(42, (long) response["integer_parameter"]); + Assert.Equal(JsonValueKind.Number, ((JsonElement) response["integer_parameter"]).ValueKind); + Assert.Equal("Bob l'Eponge", (string) response["string_parameter"]); + Assert.Equal(JsonValueKind.String, ((JsonElement) response["string_parameter"]).ValueKind); + Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]) response["array_parameter"]); + Assert.Equal(JsonValueKind.Array, ((JsonElement) response["array_parameter"]).ValueKind); + Assert.Equal("value", (string) response["object_parameter"]?["parameter"]); + Assert.Equal(JsonValueKind.Object, ((JsonElement) response["object_parameter"]).ValueKind); + } + + [Fact] + public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetTokenEndpointUris("/challenge/custom"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/challenge/custom", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal("custom_error", response.Error); + Assert.Equal("custom_error_description", response.ErrorDescription); + Assert.Equal("custom_error_uri", response.ErrorUri); + } + [Theory] [InlineData("/", OpenIddictServerEndpointType.Unknown)] [InlineData("/connect", OpenIddictServerEndpointType.Unknown)] @@ -165,7 +236,7 @@ namespace OpenIddict.Server.AspNetCore.FunctionalTests [InlineData("/connect/revoke")] [InlineData("/connect/token")] [InlineData("/connect/userinfo")] - public async Task HandleRequestAsync_RejectsInsecureHttpRequests(string address) + public async Task ProcessRequest_RejectsInsecureHttpRequests(string address) { // Arrange var client = CreateClient(options => @@ -255,6 +326,76 @@ namespace OpenIddict.Server.AspNetCore.FunctionalTests Assert.Equal("Bob le Magnifique", (string) response["name"]); } + [Fact] + public async Task ProcessSignIn_ReturnsParametersFromAuthenticationProperties() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetTokenEndpointUris("/signin/custom"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/signin/custom", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.True((bool) response["boolean_parameter"]); + Assert.Equal(JsonValueKind.True, ((JsonElement) response["boolean_parameter"]).ValueKind); + Assert.Equal(42, (long) response["integer_parameter"]); + Assert.Equal(JsonValueKind.Number, ((JsonElement) response["integer_parameter"]).ValueKind); + Assert.Equal("Bob l'Eponge", (string) response["string_parameter"]); + Assert.Equal(JsonValueKind.String, ((JsonElement) response["string_parameter"]).ValueKind); + Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]) response["array_parameter"]); + Assert.Equal(JsonValueKind.Array, ((JsonElement) response["array_parameter"]).ValueKind); + Assert.Equal("value", (string) response["object_parameter"]?["parameter"]); + Assert.Equal(JsonValueKind.Object, ((JsonElement) response["object_parameter"]).ValueKind); + } + + [Fact] + public async Task ProcessSignOut_ReturnsParametersFromAuthenticationProperties() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetLogoutEndpointUris("/signout/custom"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/signout/custom", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj" + }); + + // Assert + Assert.True((bool) response["boolean_parameter"]); + Assert.Equal(42, (long) response["integer_parameter"]); + Assert.Equal("Bob l'Eponge", (string) response["string_parameter"]); + } + protected override OpenIddictServerIntegrationTestClient CreateClient(Action configuration = null) { var builder = new WebHostBuilder(); @@ -305,12 +446,49 @@ namespace OpenIddict.Server.AspNetCore.FunctionalTests return; } + else if (context.Request.Path == "/signin/custom") + { + var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + identity.AddClaim(Claims.Subject, "Bob le Bricoleur"); + + var principal = new ClaimsPrincipal(identity); + + var properties = new AuthenticationProperties( + items: new Dictionary(), + parameters: new Dictionary + { + ["boolean_parameter"] = true, + ["integer_parameter"] = 42, + ["string_parameter"] = "Bob l'Eponge", + ["array_parameter"] = JsonSerializer.Deserialize(@"[""Contoso"",""Fabrikam""]"), + ["object_parameter"] = JsonSerializer.Deserialize(@"{""parameter"":""value""}") + }); + + await context.SignInAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal, properties); + return; + } + else if (context.Request.Path == "/signout") { await context.SignOutAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); return; } + else if (context.Request.Path == "/signout/custom") + { + var properties = new AuthenticationProperties( + items: new Dictionary(), + parameters: new Dictionary + { + ["boolean_parameter"] = true, + ["integer_parameter"] = 42, + ["string_parameter"] = "Bob l'Eponge" + }); + + await context.SignOutAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties); + return; + } + else if (context.Request.Path == "/challenge") { await context.ChallengeAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); @@ -319,12 +497,21 @@ namespace OpenIddict.Server.AspNetCore.FunctionalTests else if (context.Request.Path == "/challenge/custom") { - var properties = new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = "custom_error", - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "custom_error_description", - [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = "custom_error_uri" - }); + var properties = new AuthenticationProperties( + items: new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = "custom_error", + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "custom_error_description", + [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = "custom_error_uri" + }, + parameters: new Dictionary + { + ["boolean_parameter"] = true, + ["integer_parameter"] = 42, + ["string_parameter"] = "Bob l'Eponge", + ["array_parameter"] = JsonSerializer.Deserialize(@"[""Contoso"",""Fabrikam""]"), + ["object_parameter"] = JsonSerializer.Deserialize(@"{""parameter"":""value""}") + }); await context.ChallengeAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, properties); return; diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 28115c67..a8ed0e43 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -931,38 +931,6 @@ namespace OpenIddict.Server.FunctionalTests Assert.Null(response.ErrorUri); } - [Fact] - public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties() - { - // Arrange - var client = CreateClient(options => - { - options.EnableDegradedMode(); - options.SetTokenEndpointUris("/challenge/custom"); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - }); - - // Act - var response = await client.PostAsync("/challenge/custom", new OpenIddictRequest - { - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w" - }); - - // Assert - Assert.Equal("custom_error", response.Error); - Assert.Equal("custom_error_description", response.ErrorDescription); - Assert.Equal("custom_error_uri", response.ErrorUri); - } - [Theory] [InlineData("custom_error", null, null)] [InlineData("custom_error", "custom_description", null)] diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs index 1372077e..713c7af5 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs @@ -26,6 +26,38 @@ namespace OpenIddict.Server.Owin.FunctionalTests { public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ProcessChallenge_ReturnsErrorFromAuthenticationProperties() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetTokenEndpointUris("/challenge/custom"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/challenge/custom", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal("custom_error", response.Error); + Assert.Equal("custom_error_description", response.ErrorDescription); + Assert.Equal("custom_error_uri", response.ErrorUri); + } + [Theory] [InlineData("/", OpenIddictServerEndpointType.Unknown)] [InlineData("/connect", OpenIddictServerEndpointType.Unknown)] @@ -164,7 +196,7 @@ namespace OpenIddict.Server.Owin.FunctionalTests [InlineData("/connect/revoke")] [InlineData("/connect/token")] [InlineData("/connect/userinfo")] - public async Task HandleRequestAsync_RejectsInsecureHttpRequests(string address) + public async Task ProcessRequest_RejectsInsecureHttpRequests(string address) { // Arrange var client = CreateClient(options =>