diff --git a/OpenIddict.sln b/OpenIddict.sln index b95c00d9..e51df046 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -130,7 +130,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.WebIntegr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{4CF2AFFA-A31B-4925-ADF4-062E9BDD1381}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Client.WebIntegration.Generators", "gen\OpenIddict.Client.WebIntegration.Generators\OpenIddict.Client.WebIntegration.Generators.csproj", "{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.WebIntegration.Generators", "gen\OpenIddict.Client.WebIntegration.Generators\OpenIddict.Client.WebIntegration.Generators.csproj", "{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.IntegrationTests", "test\OpenIddict.Validation.IntegrationTests\OpenIddict.Validation.IntegrationTests.csproj", "{704CAA3A-B58B-4FAC-B623-A796321AF601}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.AspNetCore.IntegrationTests", "test\OpenIddict.Validation.AspNetCore.IntegrationTests\OpenIddict.Validation.AspNetCore.IntegrationTests.csproj", "{6A72F5DA-F792-41CC-BBDB-1A65AAC5E39A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.Owin.IntegrationTests", "test\OpenIddict.Validation.Owin.IntegrationTests\OpenIddict.Validation.Owin.IntegrationTests.csproj", "{4A71A841-60F5-4E2A-A212-FA3450F7AEA5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Client.IntegrationTests", "test\OpenIddict.Client.IntegrationTests\OpenIddict.Client.IntegrationTests.csproj", "{16BDABB5-387F-421E-95C6-0E3A2311B7E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Client.AspNetCore.IntegrationTests", "test\OpenIddict.Client.AspNetCore.IntegrationTests\OpenIddict.Client.AspNetCore.IntegrationTests.csproj", "{CC731B63-4D5C-4587-8F28-B40F4EEAC735}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Client.Owin.IntegrationTests", "test\OpenIddict.Client.Owin.IntegrationTests\OpenIddict.Client.Owin.IntegrationTests.csproj", "{2F3E9EED-446B-46C3-BC52-ED66C280E0A3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -314,6 +326,30 @@ Global {24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Release|Any CPU.Build.0 = Release|Any CPU + {704CAA3A-B58B-4FAC-B623-A796321AF601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {704CAA3A-B58B-4FAC-B623-A796321AF601}.Debug|Any CPU.Build.0 = Debug|Any CPU + {704CAA3A-B58B-4FAC-B623-A796321AF601}.Release|Any CPU.ActiveCfg = Release|Any CPU + {704CAA3A-B58B-4FAC-B623-A796321AF601}.Release|Any CPU.Build.0 = Release|Any CPU + {6A72F5DA-F792-41CC-BBDB-1A65AAC5E39A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A72F5DA-F792-41CC-BBDB-1A65AAC5E39A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A72F5DA-F792-41CC-BBDB-1A65AAC5E39A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A72F5DA-F792-41CC-BBDB-1A65AAC5E39A}.Release|Any CPU.Build.0 = Release|Any CPU + {4A71A841-60F5-4E2A-A212-FA3450F7AEA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A71A841-60F5-4E2A-A212-FA3450F7AEA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A71A841-60F5-4E2A-A212-FA3450F7AEA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A71A841-60F5-4E2A-A212-FA3450F7AEA5}.Release|Any CPU.Build.0 = Release|Any CPU + {16BDABB5-387F-421E-95C6-0E3A2311B7E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16BDABB5-387F-421E-95C6-0E3A2311B7E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16BDABB5-387F-421E-95C6-0E3A2311B7E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16BDABB5-387F-421E-95C6-0E3A2311B7E0}.Release|Any CPU.Build.0 = Release|Any CPU + {CC731B63-4D5C-4587-8F28-B40F4EEAC735}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC731B63-4D5C-4587-8F28-B40F4EEAC735}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC731B63-4D5C-4587-8F28-B40F4EEAC735}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC731B63-4D5C-4587-8F28-B40F4EEAC735}.Release|Any CPU.Build.0 = Release|Any CPU + {2F3E9EED-446B-46C3-BC52-ED66C280E0A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F3E9EED-446B-46C3-BC52-ED66C280E0A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F3E9EED-446B-46C3-BC52-ED66C280E0A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F3E9EED-446B-46C3-BC52-ED66C280E0A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -363,6 +399,12 @@ Global {E4D77737-4C73-4520-99E8-8A9E586C69A1} = {D544447C-D701-46BB-9A5B-C76C612A596B} {867317E3-364E-4F4D-9D6D-A206E1F72B9F} = {D544447C-D701-46BB-9A5B-C76C612A596B} {24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2} = {4CF2AFFA-A31B-4925-ADF4-062E9BDD1381} + {704CAA3A-B58B-4FAC-B623-A796321AF601} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {6A72F5DA-F792-41CC-BBDB-1A65AAC5E39A} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {4A71A841-60F5-4E2A-A212-FA3450F7AEA5} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {16BDABB5-387F-421E-95C6-0E3A2311B7E0} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {CC731B63-4D5C-4587-8F28-B40F4EEAC735} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} + {2F3E9EED-446B-46C3-BC52-ED66C280E0A3} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 6a818b7d..e258ac1b 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -330,7 +330,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachChallengeParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomChallengeParameters.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -385,7 +385,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachSignInParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomSignInParameters.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -448,7 +448,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachSignOutParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomSignOutParameters.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 159a36b0..c10ce5f2 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -331,7 +331,7 @@ public static partial class OpenIddictServerOwinHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachChallengeParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomChallengeParameters.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -405,7 +405,7 @@ public static partial class OpenIddictServerOwinHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachSignInParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomSignInParameters.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -487,7 +487,7 @@ public static partial class OpenIddictServerOwinHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachSignOutParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomSignOutParameters.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 1f45060f..023b0826 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -41,7 +41,7 @@ public static partial class OpenIddictServerHandlers AttachDefaultChallengeError.Descriptor, RejectDeviceCodeEntry.Descriptor, RejectUserCodeEntry.Descriptor, - AttachChallengeParameters.Descriptor, + AttachCustomChallengeParameters.Descriptor, /* * Sign-in processing: @@ -76,17 +76,19 @@ public static partial class OpenIddictServerHandlers GenerateIdentityToken.Descriptor, AttachSignInParameters.Descriptor, + AttachCustomSignInParameters.Descriptor, /* * Sign-out processing: */ ValidateSignOutDemand.Descriptor, - AttachSignOutParameters.Descriptor, + AttachCustomSignOutParameters.Descriptor, /* * Error processing: */ - AttachErrorParameters.Descriptor) + AttachErrorParameters.Descriptor, + AttachCustomErrorParameters.Descriptor) .AddRange(Authentication.DefaultHandlers) .AddRange(Device.DefaultHandlers) @@ -1065,17 +1067,18 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for attaching the appropriate parameters to the challenge response. + /// Contains the logic responsible for attaching the parameters + /// populated from user-defined handlers to the challenge response. /// - public class AttachChallengeParameters : IOpenIddictServerHandler + public class AttachCustomChallengeParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(RejectUserCodeEntry.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(100_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -2932,14 +2935,6 @@ public static partial class OpenIddictServerHandlers } } - 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) @@ -2981,6 +2976,42 @@ public static partial class OpenIddictServerHandlers } } + /// + /// Contains the logic responsible for attaching the parameters + /// populated from user-defined handlers to the sign-in response. + /// + public class AttachCustomSignInParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessSignInContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Parameters.Count > 0) + { + foreach (var parameter in context.Parameters) + { + context.Response.SetParameter(parameter.Key, parameter.Value); + } + } + + return default; + } + } + /// /// Contains the logic responsible for ensuring that the sign-out demand /// is compatible with the type of the endpoint that handled the request. @@ -3015,17 +3046,18 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for attaching the appropriate parameters to the sign-out response. + /// Contains the logic responsible for attaching the parameters + /// populated from user-defined handlers to the sign-out response. /// - public class AttachSignOutParameters : IOpenIddictServerHandler + public class AttachCustomSignOutParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateSignOutDemand.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(100_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -3076,6 +3108,34 @@ public static partial class OpenIddictServerHandlers context.Response.ErrorDescription = context.ErrorDescription; context.Response.ErrorUri = context.ErrorUri; + return default; + } + } + + /// + /// Contains the logic responsible for attaching the parameters + /// populated from user-defined handlers to the error response. + /// + public class AttachCustomErrorParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessErrorContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + if (context.Parameters.Count > 0) { foreach (var parameter in context.Parameters) diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index 5343057e..efed4f58 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs @@ -56,12 +56,7 @@ public static partial class OpenIddictValidationAspNetCoreHandlers AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, - ProcessChallengeErrorResponse.Descriptor, - - /* - * Error processing: - */ - AttachErrorParameters.Descriptor); + ProcessChallengeErrorResponse.Descriptor); /// /// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. @@ -335,7 +330,7 @@ public static partial class OpenIddictValidationAspNetCoreHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachChallengeParameters.Descriptor.Order - 500) + .SetOrder(AttachCustomChallengeParameters.Descriptor.Order - 500) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index 63c4f56b..671d1310 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -52,12 +52,7 @@ public static partial class OpenIddictValidationOwinHandlers SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, - ProcessChallengeErrorResponse.Descriptor, - - /* - * Error processing: - */ - AttachErrorParameters.Descriptor); + ProcessChallengeErrorResponse.Descriptor); /// /// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index b33b6d9b..6e8154aa 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -25,7 +25,13 @@ public static partial class OpenIddictValidationHandlers * Challenge processing: */ AttachDefaultChallengeError.Descriptor, - AttachChallengeParameters.Descriptor) + AttachCustomChallengeParameters.Descriptor, + + /* + * Error processing: + */ + AttachErrorParameters.Descriptor, + AttachCustomErrorParameters.Descriptor) .AddRange(Discovery.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) @@ -258,17 +264,18 @@ public static partial class OpenIddictValidationHandlers } /// - /// Contains the logic responsible for attaching the appropriate parameters to the challenge response. + /// Contains the logic responsible for attaching the parameters + /// populated from user-defined handlers to the sign-out response. /// - public class AttachChallengeParameters : IOpenIddictValidationHandler + public class AttachCustomChallengeParameters : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(AttachDefaultChallengeError.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -319,6 +326,34 @@ public static partial class OpenIddictValidationHandlers context.Response.ErrorDescription = context.ErrorDescription; context.Response.ErrorUri = context.ErrorUri; + return default; + } + } + + /// + /// Contains the logic responsible for attaching the parameters + /// populated from user-defined handlers to the error response. + /// + public class AttachCustomErrorParameters : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessErrorContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + if (context.Parameters.Count > 0) { foreach (var parameter in context.Parameters) diff --git a/test/OpenIddict.Client.AspNetCore.IntegrationTests/OpenIddict.Client.AspNetCore.IntegrationTests.csproj b/test/OpenIddict.Client.AspNetCore.IntegrationTests/OpenIddict.Client.AspNetCore.IntegrationTests.csproj new file mode 100644 index 00000000..dcc9bd54 --- /dev/null +++ b/test/OpenIddict.Client.AspNetCore.IntegrationTests/OpenIddict.Client.AspNetCore.IntegrationTests.csproj @@ -0,0 +1,26 @@ + + + + net461;net472;net48;netcoreapp3.1;net6.0 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj b/test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj new file mode 100644 index 00000000..3506d18e --- /dev/null +++ b/test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + net461;net472;net48;netcoreapp3.1;net6.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenIddict.Client.Owin.IntegrationTests/OpenIddict.Client.Owin.IntegrationTests.csproj b/test/OpenIddict.Client.Owin.IntegrationTests/OpenIddict.Client.Owin.IntegrationTests.csproj new file mode 100644 index 00000000..11487c53 --- /dev/null +++ b/test/OpenIddict.Client.Owin.IntegrationTests/OpenIddict.Client.Owin.IntegrationTests.csproj @@ -0,0 +1,26 @@ + + + + net461;net472;net48 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 46d5a1d4..127e9c62 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -7,6 +7,7 @@ */ using System.Security.Claims; +using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; @@ -17,6 +18,10 @@ using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.OpenIddictServerHandlers.Protection; +#if SUPPORTS_JSON_NODES +using System.Text.Json.Nodes; +#endif + namespace OpenIddict.Server.IntegrationTests; public abstract partial class OpenIddictServerIntegrationTests @@ -802,6 +807,69 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal("Bob le Bricoleur", (string?) response["name"]); } + [Fact] + public async Task ProcessChallenge_ReturnsCustomParameters() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + options.SetTokenEndpointUris("/challenge"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Parameters["boolean_parameter"] = true; + context.Parameters["integer_parameter"] = 42; + context.Parameters["string_parameter"] = "Bob l'Eponge"; + context.Parameters["array_parameter"] = JsonSerializer.Deserialize(@"[""Contoso"",""Fabrikam""]"); + context.Parameters["object_parameter"] = JsonSerializer.Deserialize(@"{""parameter"":""value""}"); +#if SUPPORTS_JSON_NODES + context.Parameters["node_array_parameter"] = new JsonArray("Contoso", "Fabrikam"); + context.Parameters["node_object_parameter"] = new JsonObject { ["parameter"] = "value" }; +#endif + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/challenge", 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); + +#if SUPPORTS_JSON_NODES + Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]?) response["node_array_parameter"]); + Assert.IsType((JsonNode?) response["node_array_parameter"]); + Assert.Equal("value", (string?) response["node_object_parameter"]?["parameter"]); + Assert.IsType((JsonNode?) response["node_object_parameter"]); +#endif + } + [Fact] public async Task ProcessSignIn_UnknownEndpointCausesAnException() { @@ -3094,6 +3162,69 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal("Bob le Bricoleur", (string?) response["name"]); } + [Fact] + public async Task ProcessSignIn_ReturnsCustomParameters() + { + // 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"); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Parameters["boolean_parameter"] = true; + context.Parameters["integer_parameter"] = 42; + context.Parameters["string_parameter"] = "Bob l'Eponge"; + context.Parameters["array_parameter"] = JsonSerializer.Deserialize(@"[""Contoso"",""Fabrikam""]"); + context.Parameters["object_parameter"] = JsonSerializer.Deserialize(@"{""parameter"":""value""}"); +#if SUPPORTS_JSON_NODES + context.Parameters["node_array_parameter"] = new JsonArray("Contoso", "Fabrikam"); + context.Parameters["node_object_parameter"] = new JsonObject { ["parameter"] = "value" }; +#endif + 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.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); + +#if SUPPORTS_JSON_NODES + Assert.Equal(new[] { "Contoso", "Fabrikam" }, (string[]?) response["node_array_parameter"]); + Assert.IsType((JsonNode?) response["node_array_parameter"]); + Assert.Equal("value", (string?) response["node_object_parameter"]?["parameter"]); + Assert.IsType((JsonNode?) response["node_object_parameter"]); +#endif + } + [Fact] public async Task ProcessSignOut_UnknownEndpointCausesAnException() { @@ -3221,6 +3352,48 @@ public abstract partial class OpenIddictServerIntegrationTests Assert.Equal("Bob le Bricoleur", (string?) response["name"]); } + [Fact] + public async Task ProcessSignOut_ReturnsCustomParameters() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SignOut(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Parameters["boolean_parameter"] = true; + context.Parameters["integer_parameter"] = 42; + context.Parameters["string_parameter"] = "Bob l'Eponge"; + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/logout", 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 virtual void ConfigureServices(IServiceCollection services) { services.AddOpenIddict() diff --git a/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddict.Validation.AspNetCore.IntegrationTests.csproj b/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddict.Validation.AspNetCore.IntegrationTests.csproj new file mode 100644 index 00000000..9dcfb924 --- /dev/null +++ b/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddict.Validation.AspNetCore.IntegrationTests.csproj @@ -0,0 +1,26 @@ + + + + net461;net472;net48;netcoreapp3.1;net6.0 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTestServer.cs b/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTestServer.cs new file mode 100644 index 00000000..67ee8e90 --- /dev/null +++ b/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTestServer.cs @@ -0,0 +1,65 @@ +/* + * 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.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.TestHost; +using OpenIddict.Validation.IntegrationTests; + +#if SUPPORTS_GENERIC_HOST +using Microsoft.Extensions.Hosting; +#endif + +namespace OpenIddict.Validation.AspNetCore.IntegrationTests; + +/// +/// Represents a test host used by the validation integration tests. +/// +public class OpenIddictValidationAspNetCoreIntegrationTestServer : OpenIddictValidationIntegrationTestServer +{ +#if SUPPORTS_GENERIC_HOST + public OpenIddictValidationAspNetCoreIntegrationTestServer(IHost host) + { + Host = host; + Server = host.GetTestServer(); + } + + /// + /// Gets the generic host used by this instance. + /// + public IHost Host { get; } +#else + public OpenIddictValidationAspNetCoreIntegrationTestServer(TestServer server) + => Server = server; +#endif + + /// + /// Gets the ASP.NET Core test server used by this instance. + /// + public TestServer Server { get; } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The caller is responsible for disposing the test client.")] + public override ValueTask CreateClientAsync() + => new(new OpenIddictValidationIntegrationTestClient(Server.CreateClient())); + + public override +#if SUPPORTS_GENERIC_HOST + async +#endif + ValueTask DisposeAsync() + { + // Dispose of the underlying test server. + Server.Dispose(); + +#if SUPPORTS_GENERIC_HOST + // Stop and dispose of the underlying generic host. + await Host.StopAsync(); + Host.Dispose(); +#else + return default; +#endif + } +} diff --git a/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTests.cs b/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTests.cs new file mode 100644 index 00000000..094c1545 --- /dev/null +++ b/test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTests.cs @@ -0,0 +1,234 @@ +/* + * 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.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenIddict.Validation.IntegrationTests; +using Xunit; +using Xunit.Abstractions; +using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; + +#if SUPPORTS_JSON_NODES +using System.Text.Json.Nodes; +#endif + +namespace OpenIddict.Validation.AspNetCore.IntegrationTests; + +public partial class OpenIddictValidationAspNetCoreIntegrationTests : OpenIddictValidationIntegrationTests +{ + public OpenIddictValidationAspNetCoreIntegrationTests(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + [Fact] + public async Task ProcessAuthentication_CreationDateIsMappedToIssuedUtc() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetCreationDate(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + var properties = new AuthenticationProperties(response.GetParameters() + .ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); + + Assert.Equal(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.IssuedUtc); + } + + [Fact] + public async Task ProcessAuthentication_ExpirationDateIsMappedToIssuedUtc() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetExpirationDate(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + var properties = new AuthenticationProperties(response.GetParameters() + .ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); + + Assert.Equal(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.ExpiresUtc); + } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The caller is responsible for disposing the test server.")] + protected override +#if SUPPORTS_GENERIC_HOST + async +#endif + ValueTask CreateServerAsync(Action? configuration = null) + { +#if SUPPORTS_GENERIC_HOST + var builder = new HostBuilder(); +#else + var builder = new WebHostBuilder(); +#endif + builder.UseEnvironment("Testing"); + + builder.ConfigureLogging(options => options.AddXUnit(OutputHelper)); + + builder.ConfigureServices(ConfigureServices); + builder.ConfigureServices(services => + { + services.AddOpenIddict() + .AddValidation(options => + { + options.UseAspNetCore(); + + configuration?.Invoke(options); + }); + }); + +#if SUPPORTS_GENERIC_HOST + builder.ConfigureWebHost(options => + { + options.UseTestServer(); + options.Configure(ConfigurePipeline); + }); +#else + builder.Configure(ConfigurePipeline); +#endif + +#if SUPPORTS_GENERIC_HOST + var host = await builder.StartAsync(); + + return new OpenIddictValidationAspNetCoreIntegrationTestServer(host); +#else + var server = new TestServer(builder); + + return new(new OpenIddictValidationAspNetCoreIntegrationTestServer(server)); +#endif + + void ConfigurePipeline(IApplicationBuilder app) + { + app.Use(next => async context => + { + await next(context); + + var feature = context.Features.Get(); + var response = feature?.Transaction?.GetProperty("custom_response"); + if (response is not null) + { + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(response)); + } + }); + + app.UseAuthentication(); + + app.Use(next => async context => + { + if (context.Request.Path == "/authenticate") + { + var result = await context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + if (result?.Principal is null) + { + await context.ChallengeAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + return; + } + + var claims = result.Principal.Claims.GroupBy(claim => claim.Type) + .Select(group => new KeyValuePair( + group.Key, group.Select(claim => claim.Value).ToArray())); + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(claims))); + return; + } + + else if (context.Request.Path == "/authenticate/properties") + { + var result = await context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + if (result?.Properties is null) + { + return; + } + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(result.Properties.Items))); + return; + } + + else if (context.Request.Path == "/challenge") + { + await context.ChallengeAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + return; + } + + await next(context); + }); + + app.Run(context => + { + context.Response.ContentType = "application/json"; + return context.Response.WriteAsync(JsonSerializer.Serialize(new + { + name = "Bob le Magnifique" + })); + }); + } + } +} diff --git a/test/OpenIddict.Validation.IntegrationTests/Certificate.cer b/test/OpenIddict.Validation.IntegrationTests/Certificate.cer new file mode 100644 index 00000000..4ab63b3a Binary files /dev/null and b/test/OpenIddict.Validation.IntegrationTests/Certificate.cer differ diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj b/test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj new file mode 100644 index 00000000..f4b3a46b --- /dev/null +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj @@ -0,0 +1,35 @@ + + + + net461;net472;net48;netcoreapp3.1;net6.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs new file mode 100644 index 00000000..68f9b060 --- /dev/null +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs @@ -0,0 +1,509 @@ +/* + * 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.Net.Http.Json; +using System.Text; +using System.Text.Encodings.Web; +using AngleSharp.Html.Parser; +using Microsoft.Extensions.Primitives; + +namespace OpenIddict.Validation.IntegrationTests; + +/// +/// Exposes methods that allow sending OpenID Connect +/// requests and extracting the corresponding responses. +/// +public class OpenIddictValidationIntegrationTestClient : IAsyncDisposable +{ + /// + /// Initializes a new instance of the OpenID Connect client. + /// + public OpenIddictValidationIntegrationTestClient() + : this(new HttpClient()) + { + } + + /// + /// Initializes a new instance of the OpenID Connect client. + /// + /// The HTTP client used to communicate with the OpenID Connect server. + public OpenIddictValidationIntegrationTestClient(HttpClient client) + : this(client, new HtmlParser()) + { + } + + /// + /// Initializes a new instance of the OpenID Connect client. + /// + /// The HTTP client used to communicate with the OpenID Connect server. + /// The HTML parser used to parse the responses returned by the OpenID Connect server. + public OpenIddictValidationIntegrationTestClient(HttpClient client, HtmlParser parser) + { + HttpClient = client ?? throw new ArgumentNullException(nameof(client)); + HtmlParser = parser ?? throw new ArgumentNullException(nameof(parser)); + } + + /// + /// Gets the underlying HTTP client used to + /// communicate with the OpenID Connect server. + /// + public HttpClient HttpClient { get; } + + /// + /// Gets the underlying HTML parser used to parse the + /// responses returned by the OpenID Connect server. + /// + public HtmlParser HtmlParser { get; } + + /// + /// Sends an empty OpenID Connect request to the given endpoint using GET + /// and converts the returned response to an OpenID Connect response. + /// + /// The endpoint to which the request is sent. + /// The OpenID Connect response returned by the server. + public Task GetAsync(string uri) + => GetAsync(uri, new OpenIddictRequest()); + + /// + /// Sends an empty OpenID Connect request to the given endpoint using GET + /// and converts the returned response to an OpenID Connect response. + /// + /// The endpoint to which the request is sent. + /// The OpenID Connect response returned by the server. + public Task GetAsync(Uri uri) + => GetAsync(uri, new OpenIddictRequest()); + + /// + /// Sends a generic OpenID Connect request to the given endpoint using GET + /// and converts the returned response to an OpenID Connect response. + /// + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public Task GetAsync(string uri, OpenIddictRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); + } + + return GetAsync(new Uri(uri, UriKind.RelativeOrAbsolute), request); + } + + /// + /// Sends a generic OpenID Connect request to the given endpoint using GET + /// and converts the returned response to an OpenID Connect response. + /// + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public Task GetAsync(Uri uri, OpenIddictRequest request) + => SendAsync(HttpMethod.Get, uri, request); + + /// + /// Sends a generic OpenID Connect request to the given endpoint using POST + /// and converts the returned response to an OpenID Connect response. + /// + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public Task PostAsync(string uri, OpenIddictRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); + } + + return PostAsync(new Uri(uri, UriKind.RelativeOrAbsolute), request); + } + + /// + /// Sends a generic OpenID Connect request to the given endpoint using POST + /// and converts the returned response to an OpenID Connect response. + /// + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public Task PostAsync(Uri uri, OpenIddictRequest request) + => SendAsync(HttpMethod.Post, uri, request); + + /// + /// Sends a generic OpenID Connect request to the given endpoint and + /// converts the returned response to an OpenID Connect response. + /// + /// The HTTP method used to send the OpenID Connect request. + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public Task SendAsync(string method, string uri, OpenIddictRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(method)) + { + throw new ArgumentException("The HTTP method cannot be null or empty.", nameof(method)); + } + + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); + } + + return SendAsync(new HttpMethod(method), uri, request); + } + + /// + /// Sends a generic OpenID Connect request to the given endpoint and + /// converts the returned response to an OpenID Connect response. + /// + /// The HTTP method used to send the OpenID Connect request. + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public Task SendAsync(HttpMethod method, string uri, OpenIddictRequest request) + { + if (method is null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrEmpty(uri)) + { + throw new ArgumentException("The URL cannot be null or empty.", nameof(uri)); + } + + return SendAsync(method, new Uri(uri, UriKind.RelativeOrAbsolute), request); + } + + /// + /// Sends a generic OpenID Connect request to the given endpoint and + /// converts the returned response to an OpenID Connect response. + /// + /// The HTTP method used to send the OpenID Connect request. + /// The endpoint to which the request is sent. + /// The OpenID Connect request to send. + /// The OpenID Connect response returned by the server. + public virtual async Task SendAsync(HttpMethod method, Uri uri, OpenIddictRequest request) + { + if (method is null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (uri is null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (HttpClient.BaseAddress is null && !uri.IsAbsoluteUri) + { + throw new ArgumentException("The address cannot be a relative URI when no base address " + + "is associated with the HTTP client.", nameof(uri)); + } + + using var message = CreateRequestMessage(request, method, uri); + using var response = await HttpClient.SendAsync(message); + + return await GetResponseAsync(response); + } + + private HttpRequestMessage CreateRequestMessage(OpenIddictRequest request, HttpMethod method, Uri uri) + { + // Note: a dictionary is deliberately not used here to allow multiple parameters with the + // same name to be specified. While initially not allowed by the core OAuth2 specification, + // this is required for derived drafts like the OAuth2 token exchange specification. + var parameters = new List>(); + + foreach (var parameter in request.GetParameters()) + { + // If the parameter is null or empty, send an empty value. + if (OpenIddictParameter.IsNullOrEmpty(parameter.Value)) + { + parameters.Add(new KeyValuePair(parameter.Key, string.Empty)); + + continue; + } + + var values = (string?[]?) parameter.Value; + if (values is not { Length: > 0 }) + { + continue; + } + + foreach (var value in values) + { + parameters.Add(new KeyValuePair(parameter.Key, value)); + } + } + + if (method == HttpMethod.Get && parameters.Count != 0) + { + var builder = new StringBuilder(); + + foreach (var parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + { + continue; + } + + if (builder.Length != 0) + { + builder.Append('&'); + } + + builder.Append(UrlEncoder.Default.Encode(parameter.Key)); + + if (!string.IsNullOrEmpty(parameter.Value)) + { + builder.Append('='); + builder.Append(UrlEncoder.Default.Encode(parameter.Value)); + } + } + + if (!uri.IsAbsoluteUri) + { + uri = new Uri(HttpClient.BaseAddress!, uri); + } + + uri = new UriBuilder(uri) { Query = builder.ToString() }.Uri; + } + + var message = new HttpRequestMessage(method, uri); + + if (method != HttpMethod.Get) + { + message.Content = new FormUrlEncodedContent(parameters); + } + + return message; + } + + private async Task GetResponseAsync(HttpResponseMessage message) + { + if (message.Headers.WwwAuthenticate.Count is not 0) + { + var parameters = new Dictionary(message.Headers.WwwAuthenticate.Count); + + foreach (var header in message.Headers.WwwAuthenticate) + { + if (string.IsNullOrEmpty(header.Parameter)) + { + continue; + } + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple + // parameters with the same name are used by derived drafts like the OAuth 2.0 + // token exchange specification. For consistency, multiple parameters with the + // same name are also supported when returned as part of WWW-Authentication headers. + + foreach (var parameter in header.Parameter.Split(Separators.Comma, StringSplitOptions.RemoveEmptyEntries)) + { + var values = parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries); + if (values.Length is not 2) + { + continue; + } + + var (name, value) = ( + values[0]?.Trim(Separators.Space[0]), + values[1]?.Trim(Separators.Space[0], Separators.DoubleQuote[0])); + + if (string.IsNullOrEmpty(name)) + { + continue; + } + + parameters[name] = parameters.ContainsKey(name) ? + StringValues.Concat(parameters[name], value?.Replace("\\\"", "\"")) : + new StringValues(value?.Replace("\\\"", "\"")); + } + } + + return new OpenIddictResponse(parameters); + } + + else if (message.Headers.Location is not null) + { + var payload = message.Headers.Location.Fragment; + if (string.IsNullOrEmpty(payload)) + { + payload = message.Headers.Location.Query; + } + + if (string.IsNullOrEmpty(payload)) + { + return new OpenIddictResponse(); + } + + static string? UnescapeDataString(string value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + return Uri.UnescapeDataString(value.Replace("+", "%20")); + } + + // Note: a dictionary is deliberately not used here to allow multiple parameters with the + // same name to be retrieved. While initially not allowed by the core OAuth2 specification, + // this is required for derived drafts like the OAuth2 token exchange specification. + var parameters = new List>(); + + foreach (var element in new StringTokenizer(payload, Separators.Ampersand)) + { + var segment = element; + if (segment.Length == 0) + { + continue; + } + + // Always skip the first char (# or ?). + if (segment.Offset == 0) + { + segment = segment.Subsegment(1, segment.Length - 1); + } + + var index = segment.IndexOf('='); + if (index == -1) + { + continue; + } + + var name = UnescapeDataString(segment.Substring(0, index)); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + var value = UnescapeDataString(segment.Substring(index + 1, segment.Length - (index + 1))); + + parameters.Add(new KeyValuePair(name, value)); + } + + return new OpenIddictResponse( + from parameter in parameters + group parameter by parameter.Key into grouping + let values = grouping.Select(parameter => parameter.Value) + select new KeyValuePair(grouping.Key, values.ToArray())); + } + + else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)) + { + return (await message.Content!.ReadFromJsonAsync())!; + } + + else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "text/html", StringComparison.OrdinalIgnoreCase)) + { + // Note: this test client is only used with OpenIddict's ASP.NET Core or OWIN hosts, + // that always return their HTTP responses encoded using UTF-8. As such, the stream + // returned by ReadAsStreamAsync() is always assumed to contain UTF-8 encoded payloads. + using var stream = await message.Content!.ReadAsStreamAsync(); + + using var document = await HtmlParser.ParseDocumentAsync(stream); + if (document.Body is null) + { + return new OpenIddictResponse(); + } + + // Note: a dictionary is deliberately not used here to allow multiple parameters with the + // same name to be retrieved. While initially not allowed by the core OAuth2 specification, + // this is required for derived drafts like the OAuth2 token exchange specification. + var parameters = new List>(); + + foreach (var element in document.Body.GetElementsByTagName("input")) + { + var name = element.GetAttribute("name"); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + var value = element.GetAttribute("value"); + + parameters.Add(new KeyValuePair(name, value)); + } + + return new OpenIddictResponse( + from parameter in parameters + group parameter by parameter.Key into grouping + let values = grouping.Select(parameter => parameter.Value) + select new KeyValuePair(grouping.Key, values.ToArray())); + } + + else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "text/plain", StringComparison.OrdinalIgnoreCase)) + { + // Note: this test client is only used with OpenIddict's ASP.NET Core or OWIN hosts, + // that always return their HTTP responses encoded using UTF-8. As such, the stream + // returned by ReadAsStreamAsync() is always assumed to contain UTF-8 encoded payloads. + using var stream = await message.Content!.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + + // Note: a dictionary is deliberately not used here to allow multiple parameters with the + // same name to be retrieved. While initially not allowed by the core OAuth2 specification, + // this is required for derived drafts like the OAuth2 token exchange specification. + var parameters = new List>(); + + for (var line = await reader.ReadLineAsync(); line is not null; line = await reader.ReadLineAsync()) + { + var index = line.IndexOf(':'); + if (index == -1) + { + continue; + } + + var name = line.Substring(0, index); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + var value = line.Substring(index + 1); + + parameters.Add(new KeyValuePair(name, value)); + } + + return new OpenIddictResponse( + from parameter in parameters + group parameter by parameter.Key into grouping + let values = grouping.Select(parameter => parameter.Value) + select new KeyValuePair(grouping.Key, values.ToArray())); + } + + return new OpenIddictResponse(); + } + + public ValueTask DisposeAsync() + { + HttpClient.Dispose(); + + return default; + } +} diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestServer.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestServer.cs new file mode 100644 index 00000000..3c8cbfbe --- /dev/null +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestServer.cs @@ -0,0 +1,17 @@ +/* + * 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. + */ + +namespace OpenIddict.Validation.IntegrationTests; + +/// +/// Represents a test host used by the validation integration tests. +/// +public abstract class OpenIddictValidationIntegrationTestServer : IAsyncDisposable +{ + public abstract ValueTask CreateClientAsync(); + + public virtual ValueTask DisposeAsync() => default; +} diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs new file mode 100644 index 00000000..c057251e --- /dev/null +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs @@ -0,0 +1,325 @@ + + +/* + * 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.Reflection; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Tokens; +using Moq; +using OpenIddict.Core; +using Xunit; +using Xunit.Abstractions; +using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlers; +using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; + +namespace OpenIddict.Validation.IntegrationTests; + +public abstract partial class OpenIddictValidationIntegrationTests +{ + protected OpenIddictValidationIntegrationTests(ITestOutputHelper outputHelper) + { + OutputHelper = outputHelper; + } + + protected ITestOutputHelper OutputHelper { get; } + + [Fact] + public async Task ProcessAuthentication_InvalidIssuerThrowsAnException() + { + // Arrange + await using var server = await CreateServerAsync(options => options.Configure(options => + { + options.ConfigurationManager = new StaticConfigurationManager(new() + { + Issuer = new Uri("https://fabrikam.com/") + }); + })); + + await using var client = await server.CreateClientAsync(); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.PostAsync("/authenticate", new OpenIddictRequest()); + }); + + Assert.Equal(SR.GetResourceString(SR.ID0307), exception.Message); + } + + [Fact] + public async Task ProcessAuthentication_EvalutesCorrectValidatedTokens() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + // Assert + Assert.True(context.ExtractAccessToken); + Assert.True(context.RequireAccessToken); + Assert.True(context.ValidateAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/authenticate", new OpenIddictRequest()); + + // Assert + Assert.Equal(0, response.Count); + } + + [Fact] + public async Task ProcessAuthentication_RejectsDemandWhenAccessTokenIsMissing() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + // Assert + Assert.True(context.IsRejected); + Assert.Equal(Errors.MissingToken, context.Error); + Assert.Equal(SR.GetResourceString(SR.ID2000), context.ErrorDescription); + + return default; + }); + + builder.SetOrder(ValidateRequiredTokens.Descriptor.Order + 1); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/authenticate", new OpenIddictRequest()); + + // Assert + Assert.Equal(0, response.Count); + } + + [Fact] + public async Task ProcessAuthentication_RejectsDemandWhenAccessTokenIsInvalid() + { + // Arrange + await using var server = await CreateServerAsync(); + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "SlAV32hkKG" + }); + + // Assert + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2004), response.ErrorDescription); + } + + [Fact] + public async Task ProcessAuthentication_ReturnsExpectedIdentityWhenAccessTokenIsValid() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]); + } + + [Fact] + public async Task ProcessChallenge_ReturnsDefaultErrorWhenNoneIsSpecified() + { + // Arrange + await using var server = await CreateServerAsync(); + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/challenge", new OpenIddictRequest()); + + // Assert + Assert.Equal(Errors.InsufficientAccess, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2095), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2095), response.ErrorUri); + } + + [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 ProcessChallenge_AllowsRejectingRequest(string error, string description, string uri) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + 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("/challenge", new OpenIddictRequest()); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task ProcessChallenge_AllowsHandlingResponse() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + 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("/challenge", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Bricoleur", (string?) response["name"]); + } + + protected virtual void ConfigureServices(IServiceCollection services) + { + services.AddOpenIddict() + .AddCore(options => + { + options.SetDefaultAuthorizationEntity() + .SetDefaultTokenEntity(); + + options.Services.AddSingleton(CreateAuthorizationManager()) + .AddSingleton(CreateTokenManager()); + }) + + .AddValidation(options => + { + options.SetIssuer(new Uri("https://contoso.com/")); + + options.SetConfiguration(new OpenIddictConfiguration + { + SigningKeys = + { + new X509SecurityKey(GetSigningCertificate( + assembly: typeof(OpenIddictValidationIntegrationTests).Assembly, + resource: "OpenIddict.Validation.IntegrationTests.Certificate.cer", + password: null)) + } + }); + }); + + static X509Certificate2 GetSigningCertificate(Assembly assembly, string resource, string? password) + { + using var stream = assembly.GetManifestResourceStream(resource) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0064)); + + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + + return new X509Certificate2(buffer.ToArray(), password, X509KeyStorageFlags.MachineKeySet); + } + } + + protected abstract ValueTask CreateServerAsync( + Action? configuration = null); + + protected OpenIddictAuthorizationManager CreateAuthorizationManager( + Action>>? configuration = null) + { + var manager = new Mock>( + Mock.Of>(), + OutputHelper.ToLogger>(), + Mock.Of>(), + Mock.Of()); + + configuration?.Invoke(manager); + + return manager.Object; + } + + protected OpenIddictTokenManager CreateTokenManager( + Action>>? configuration = null) + { + var manager = new Mock>( + Mock.Of>(), + OutputHelper.ToLogger>(), + Mock.Of>(), + Mock.Of()); + + configuration?.Invoke(manager); + + return manager.Object; + } + + public class OpenIddictAuthorization { } + public class OpenIddictToken { } +} diff --git a/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddict.Validation.Owin.IntegrationTests.csproj b/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddict.Validation.Owin.IntegrationTests.csproj new file mode 100644 index 00000000..e066e6ba --- /dev/null +++ b/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddict.Validation.Owin.IntegrationTests.csproj @@ -0,0 +1,26 @@ + + + + net461;net472;net48 + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTestServer.cs b/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTestServer.cs new file mode 100644 index 00000000..0e473410 --- /dev/null +++ b/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTestServer.cs @@ -0,0 +1,38 @@ +/* + * 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.Diagnostics.CodeAnalysis; +using Microsoft.Owin.Testing; +using OpenIddict.Validation.IntegrationTests; + +namespace OpenIddict.Validation.Owin.IntegrationTests; + +/// +/// Represents a test host used by the validation integration tests. +/// +public class OpenIddictValidationOwinIntegrationTestValidation : OpenIddictValidationIntegrationTestServer +{ + public OpenIddictValidationOwinIntegrationTestValidation(TestServer server) + => Server = server; + + /// + /// Gets the ASP.NET Core test server used by this instance. + /// + public TestServer Server { get; } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The caller is responsible for disposing the test client.")] + public override ValueTask CreateClientAsync() + => new(new OpenIddictValidationIntegrationTestClient(Server.HttpClient)); + + public override ValueTask DisposeAsync() + { + // Dispose of the underlying test server. + Server.Dispose(); + + return default; + } +} diff --git a/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTests.cs b/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTests.cs new file mode 100644 index 00000000..4e4c1754 --- /dev/null +++ b/test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTests.cs @@ -0,0 +1,217 @@ +/* + * 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.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Testing; +using OpenIddict.Validation.IntegrationTests; +using Owin; +using Xunit; +using Xunit.Abstractions; +using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; + +namespace OpenIddict.Validation.Owin.IntegrationTests; + +public partial class OpenIddictValidationOwinIntegrationTests : OpenIddictValidationIntegrationTests +{ + public OpenIddictValidationOwinIntegrationTests(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + [Fact] + public async Task ProcessAuthentication_CreationDateIsMappedToIssuedUtc() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetCreationDate(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + var properties = new AuthenticationProperties(response.GetParameters() + .ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); + + Assert.Equal(new DateTimeOffset(2020, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.IssuedUtc); + } + + [Fact] + public async Task ProcessAuthentication_ExpirationDateIsMappedToIssuedUtc() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetExpirationDate(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero)); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.GetAsync("/authenticate/properties", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + var properties = new AuthenticationProperties(response.GetParameters() + .ToDictionary(parameter => parameter.Key, parameter => (string?) parameter.Value)); + + Assert.Equal(new DateTimeOffset(2120, 01, 01, 00, 00, 00, TimeSpan.Zero), properties.ExpiresUtc); + } + + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", + Justification = "The caller is responsible for disposing the test Validation.")] + protected override ValueTask CreateServerAsync(Action? configuration = null) + { + var services = new ServiceCollection(); + ConfigureServices(services); + + services.AddLogging(options => options.AddXUnit(OutputHelper)); + + services.AddOpenIddict() + .AddValidation(options => + { + options.UseOwin(); + + configuration?.Invoke(options); + }); + + var provider = services.BuildServiceProvider(); + + var server = TestServer.Create(app => + { + app.Use(async (context, next) => + { + using var scope = provider.CreateScope(); + + context.Set(typeof(IServiceProvider).FullName, scope.ServiceProvider); + + try + { + await next(); + } + + finally + { + context.Environment.Remove(typeof(IServiceProvider).FullName); + } + }); + + app.Use(async (context, next) => + { + await next(); + + var transaction = context.Get(typeof(OpenIddictValidationTransaction).FullName); + var response = transaction?.GetProperty("custom_response"); + if (response is not null) + { + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(response)); + } + }); + + app.UseOpenIddictValidation(); + + app.Use(async (context, next) => + { + if (context.Request.Path == new PathString("/authenticate")) + { + var result = await context.Authentication.AuthenticateAsync(OpenIddictValidationOwinDefaults.AuthenticationType); + if (result?.Identity is null) + { + context.Authentication.Challenge(OpenIddictValidationOwinDefaults.AuthenticationType); + return; + } + + var claims = result.Identity.Claims.GroupBy(claim => claim.Type) + .Select(group => new KeyValuePair( + group.Key, group.Select(claim => claim.Value).ToArray())); + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(claims))); + return; + } + + else if (context.Request.Path == new PathString("/authenticate/properties")) + { + var result = await context.Authentication.AuthenticateAsync(OpenIddictValidationOwinDefaults.AuthenticationType); + if (result?.Properties is null) + { + return; + } + + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new OpenIddictResponse(result.Properties.Dictionary))); + return; + } + + else if (context.Request.Path == new PathString("/challenge")) + { + context.Authentication.Challenge(OpenIddictValidationOwinDefaults.AuthenticationType); + return; + } + + await next(); + }); + + app.Run(context => + { + context.Response.ContentType = "application/json"; + return context.Response.WriteAsync(JsonSerializer.Serialize(new + { + name = "Bob le Magnifique" + })); + }); + }); + + return new(new OpenIddictValidationOwinIntegrationTestValidation(server)); + } +}