Browse Source

Add integration tests projects for the client and validation stacks and use a high order for the AttachCustom*Parameters handlers

pull/1494/head
Kévin Chalet 4 years ago
parent
commit
a1f84d38d0
  1. 44
      OpenIddict.sln
  2. 6
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  3. 6
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  4. 98
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  5. 9
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs
  6. 7
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs
  7. 45
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  8. 26
      test/OpenIddict.Client.AspNetCore.IntegrationTests/OpenIddict.Client.AspNetCore.IntegrationTests.csproj
  9. 31
      test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj
  10. 26
      test/OpenIddict.Client.Owin.IntegrationTests/OpenIddict.Client.Owin.IntegrationTests.csproj
  11. 173
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  12. 26
      test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddict.Validation.AspNetCore.IntegrationTests.csproj
  13. 65
      test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTestServer.cs
  14. 234
      test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddictValidationAspNetCoreIntegrationTests.cs
  15. BIN
      test/OpenIddict.Validation.IntegrationTests/Certificate.cer
  16. 35
      test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj
  17. 509
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestClient.cs
  18. 17
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTestServer.cs
  19. 325
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs
  20. 26
      test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddict.Validation.Owin.IntegrationTests.csproj
  21. 38
      test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTestServer.cs
  22. 217
      test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddictValidationOwinIntegrationTests.cs

44
OpenIddict.sln

@ -130,7 +130,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.WebIntegr
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{4CF2AFFA-A31B-4925-ADF4-062E9BDD1381}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{4CF2AFFA-A31B-4925-ADF4-062E9BDD1381}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -363,6 +399,12 @@ Global
{E4D77737-4C73-4520-99E8-8A9E586C69A1} = {D544447C-D701-46BB-9A5B-C76C612A596B} {E4D77737-4C73-4520-99E8-8A9E586C69A1} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{867317E3-364E-4F4D-9D6D-A206E1F72B9F} = {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} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

6
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -330,7 +330,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ResolveHostChallengeParameters>() .UseSingletonHandler<ResolveHostChallengeParameters>()
.SetOrder(AttachChallengeParameters.Descriptor.Order - 500) .SetOrder(AttachCustomChallengeParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -385,7 +385,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ResolveHostSignInParameters>() .UseSingletonHandler<ResolveHostSignInParameters>()
.SetOrder(AttachSignInParameters.Descriptor.Order - 500) .SetOrder(AttachCustomSignInParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -448,7 +448,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ResolveHostSignOutParameters>() .UseSingletonHandler<ResolveHostSignOutParameters>()
.SetOrder(AttachSignOutParameters.Descriptor.Order - 500) .SetOrder(AttachCustomSignOutParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

6
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -331,7 +331,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostChallengeParameters>() .UseSingletonHandler<ResolveHostChallengeParameters>()
.SetOrder(AttachChallengeParameters.Descriptor.Order - 500) .SetOrder(AttachCustomChallengeParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -405,7 +405,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostSignInParameters>() .UseSingletonHandler<ResolveHostSignInParameters>()
.SetOrder(AttachSignInParameters.Descriptor.Order - 500) .SetOrder(AttachCustomSignInParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -487,7 +487,7 @@ public static partial class OpenIddictServerOwinHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ResolveHostSignOutParameters>() .UseSingletonHandler<ResolveHostSignOutParameters>()
.SetOrder(AttachSignOutParameters.Descriptor.Order - 500) .SetOrder(AttachCustomSignOutParameters.Descriptor.Order - 500)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

98
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -41,7 +41,7 @@ public static partial class OpenIddictServerHandlers
AttachDefaultChallengeError.Descriptor, AttachDefaultChallengeError.Descriptor,
RejectDeviceCodeEntry.Descriptor, RejectDeviceCodeEntry.Descriptor,
RejectUserCodeEntry.Descriptor, RejectUserCodeEntry.Descriptor,
AttachChallengeParameters.Descriptor, AttachCustomChallengeParameters.Descriptor,
/* /*
* Sign-in processing: * Sign-in processing:
@ -76,17 +76,19 @@ public static partial class OpenIddictServerHandlers
GenerateIdentityToken.Descriptor, GenerateIdentityToken.Descriptor,
AttachSignInParameters.Descriptor, AttachSignInParameters.Descriptor,
AttachCustomSignInParameters.Descriptor,
/* /*
* Sign-out processing: * Sign-out processing:
*/ */
ValidateSignOutDemand.Descriptor, ValidateSignOutDemand.Descriptor,
AttachSignOutParameters.Descriptor, AttachCustomSignOutParameters.Descriptor,
/* /*
* Error processing: * Error processing:
*/ */
AttachErrorParameters.Descriptor) AttachErrorParameters.Descriptor,
AttachCustomErrorParameters.Descriptor)
.AddRange(Authentication.DefaultHandlers) .AddRange(Authentication.DefaultHandlers)
.AddRange(Device.DefaultHandlers) .AddRange(Device.DefaultHandlers)
@ -1065,17 +1067,18 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public class AttachChallengeParameters : IOpenIddictServerHandler<ProcessChallengeContext> public class AttachCustomChallengeParameters : IOpenIddictServerHandler<ProcessChallengeContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachChallengeParameters>() .UseSingletonHandler<AttachCustomChallengeParameters>()
.SetOrder(RejectUserCodeEntry.Descriptor.Order + 1_000) .SetOrder(100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .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; return default;
static Uri? GetEndpointAbsoluteUri(Uri? issuer, Uri? endpoint) static Uri? GetEndpointAbsoluteUri(Uri? issuer, Uri? endpoint)
@ -2981,6 +2976,42 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for attaching the parameters
/// populated from user-defined handlers to the sign-in response.
/// </summary>
public class AttachCustomSignInParameters : IOpenIddictServerHandler<ProcessSignInContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.UseSingletonHandler<AttachCustomSignInParameters>()
.SetOrder(100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for ensuring that the sign-out demand /// Contains the logic responsible for ensuring that the sign-out demand
/// is compatible with the type of the endpoint that handled the request. /// is compatible with the type of the endpoint that handled the request.
@ -3015,17 +3046,18 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public class AttachSignOutParameters : IOpenIddictServerHandler<ProcessSignOutContext> public class AttachCustomSignOutParameters : IOpenIddictServerHandler<ProcessSignOutContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachSignOutParameters>() .UseSingletonHandler<AttachCustomSignOutParameters>()
.SetOrder(ValidateSignOutDemand.Descriptor.Order + 1_000) .SetOrder(100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -3076,6 +3108,34 @@ public static partial class OpenIddictServerHandlers
context.Response.ErrorDescription = context.ErrorDescription; context.Response.ErrorDescription = context.ErrorDescription;
context.Response.ErrorUri = context.ErrorUri; context.Response.ErrorUri = context.ErrorUri;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters
/// populated from user-defined handlers to the error response.
/// </summary>
public class AttachCustomErrorParameters : IOpenIddictServerHandler<ProcessErrorContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessErrorContext>()
.UseSingletonHandler<AttachCustomErrorParameters>()
.SetOrder(100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessErrorContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0) if (context.Parameters.Count > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)

9
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs

@ -56,12 +56,7 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
AttachHttpResponseCode<ProcessErrorContext>.Descriptor, AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor, AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor, AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor, ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor);
/*
* Error processing:
*/
AttachErrorParameters.Descriptor);
/// <summary> /// <summary>
/// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. /// 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<ProcessChallengeContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ResolveHostChallengeParameters>() .UseSingletonHandler<ResolveHostChallengeParameters>()
.SetOrder(AttachChallengeParameters.Descriptor.Order - 500) .SetOrder(AttachCustomChallengeParameters.Descriptor.Order - 500)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();

7
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs

@ -52,12 +52,7 @@ public static partial class OpenIddictValidationOwinHandlers
SuppressFormsAuthenticationRedirect<ProcessErrorContext>.Descriptor, SuppressFormsAuthenticationRedirect<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor, AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor, AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor, ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor);
/*
* Error processing:
*/
AttachErrorParameters.Descriptor);
/// <summary> /// <summary>
/// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. /// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it.

45
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -25,7 +25,13 @@ public static partial class OpenIddictValidationHandlers
* Challenge processing: * Challenge processing:
*/ */
AttachDefaultChallengeError.Descriptor, AttachDefaultChallengeError.Descriptor,
AttachChallengeParameters.Descriptor) AttachCustomChallengeParameters.Descriptor,
/*
* Error processing:
*/
AttachErrorParameters.Descriptor,
AttachCustomErrorParameters.Descriptor)
.AddRange(Discovery.DefaultHandlers) .AddRange(Discovery.DefaultHandlers)
.AddRange(Introspection.DefaultHandlers) .AddRange(Introspection.DefaultHandlers)
@ -258,17 +264,18 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public class AttachChallengeParameters : IOpenIddictValidationHandler<ProcessChallengeContext> public class AttachCustomChallengeParameters : IOpenIddictValidationHandler<ProcessChallengeContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachChallengeParameters>() .UseSingletonHandler<AttachCustomChallengeParameters>()
.SetOrder(AttachDefaultChallengeError.Descriptor.Order + 1_000) .SetOrder(100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -319,6 +326,34 @@ public static partial class OpenIddictValidationHandlers
context.Response.ErrorDescription = context.ErrorDescription; context.Response.ErrorDescription = context.ErrorDescription;
context.Response.ErrorUri = context.ErrorUri; context.Response.ErrorUri = context.ErrorUri;
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters
/// populated from user-defined handlers to the error response.
/// </summary>
public class AttachCustomErrorParameters : IOpenIddictValidationHandler<ProcessErrorContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessErrorContext>()
.UseSingletonHandler<AttachCustomErrorParameters>()
.SetOrder(100_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessErrorContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0) if (context.Parameters.Count > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)

26
test/OpenIddict.Client.AspNetCore.IntegrationTests/OpenIddict.Client.AspNetCore.IntegrationTests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.AspNetCore\OpenIddict.Client.AspNetCore.csproj" />
<ProjectReference Include="..\OpenIddict.Client.IntegrationTests\OpenIddict.Client.IntegrationTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
</Project>

31
test/OpenIddict.Client.IntegrationTests/OpenIddict.Client.IntegrationTests.csproj

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client\OpenIddict.Client.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" />
<PackageReference Include="MartinCostello.Logging.XUnit" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Net.Http.Json" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
</Project>

26
test/OpenIddict.Client.Owin.IntegrationTests/OpenIddict.Client.Owin.IntegrationTests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472;net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.Owin\OpenIddict.Client.Owin.csproj" />
<ProjectReference Include="..\OpenIddict.Client.IntegrationTests\OpenIddict.Client.IntegrationTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Owin.Testing" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
</Project>

173
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -7,6 +7,7 @@
*/ */
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
@ -17,6 +18,10 @@ using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.OpenIddictServerHandlers;
using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using static OpenIddict.Server.OpenIddictServerHandlers.Protection;
#if SUPPORTS_JSON_NODES
using System.Text.Json.Nodes;
#endif
namespace OpenIddict.Server.IntegrationTests; namespace OpenIddict.Server.IntegrationTests;
public abstract partial class OpenIddictServerIntegrationTests public abstract partial class OpenIddictServerIntegrationTests
@ -802,6 +807,69 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Bricoleur", (string?) response["name"]); 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<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessChallengeContext>(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<JsonElement>(@"[""Contoso"",""Fabrikam""]");
context.Parameters["object_parameter"] = JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonArray>((JsonNode?) response["node_array_parameter"]);
Assert.Equal("value", (string?) response["node_object_parameter"]?["parameter"]);
Assert.IsType<JsonObject>((JsonNode?) response["node_object_parameter"]);
#endif
}
[Fact] [Fact]
public async Task ProcessSignIn_UnknownEndpointCausesAnException() public async Task ProcessSignIn_UnknownEndpointCausesAnException()
{ {
@ -3094,6 +3162,69 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Bricoleur", (string?) response["name"]); 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<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
options.AddEventHandler<ProcessSignInContext>(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<JsonElement>(@"[""Contoso"",""Fabrikam""]");
context.Parameters["object_parameter"] = JsonSerializer.Deserialize<JsonElement>(@"{""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<JsonArray>((JsonNode?) response["node_array_parameter"]);
Assert.Equal("value", (string?) response["node_object_parameter"]?["parameter"]);
Assert.IsType<JsonObject>((JsonNode?) response["node_object_parameter"]);
#endif
}
[Fact] [Fact]
public async Task ProcessSignOut_UnknownEndpointCausesAnException() public async Task ProcessSignOut_UnknownEndpointCausesAnException()
{ {
@ -3221,6 +3352,48 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal("Bob le Bricoleur", (string?) response["name"]); 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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SignOut();
return default;
}));
options.AddEventHandler<ProcessSignOutContext>(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) protected virtual void ConfigureServices(IServiceCollection services)
{ {
services.AddOpenIddict() services.AddOpenIddict()

26
test/OpenIddict.Validation.AspNetCore.IntegrationTests/OpenIddict.Validation.AspNetCore.IntegrationTests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.IntegrationTests\OpenIddict.Validation.IntegrationTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
</Project>

65
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;
/// <summary>
/// Represents a test host used by the validation integration tests.
/// </summary>
public class OpenIddictValidationAspNetCoreIntegrationTestServer : OpenIddictValidationIntegrationTestServer
{
#if SUPPORTS_GENERIC_HOST
public OpenIddictValidationAspNetCoreIntegrationTestServer(IHost host)
{
Host = host;
Server = host.GetTestServer();
}
/// <summary>
/// Gets the generic host used by this instance.
/// </summary>
public IHost Host { get; }
#else
public OpenIddictValidationAspNetCoreIntegrationTestServer(TestServer server)
=> Server = server;
#endif
/// <summary>
/// Gets the ASP.NET Core test server used by this instance.
/// </summary>
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<OpenIddictValidationIntegrationTestClient> 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
}
}

234
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<ValidateTokenContext>(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<ValidateTokenContext>(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<OpenIddictValidationIntegrationTestServer> CreateServerAsync(Action<OpenIddictValidationBuilder>? 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<OpenIddictValidationAspNetCoreFeature>();
var response = feature?.Transaction?.GetProperty<object>("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<string, string?[]?>(
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"
}));
});
}
}
}

BIN
test/OpenIddict.Validation.IntegrationTests/Certificate.cer

Binary file not shown.

35
test/OpenIddict.Validation.IntegrationTests/OpenIddict.Validation.IntegrationTests.csproj

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472;net48;netcoreapp3.1;net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Certificate.cer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" />
<PackageReference Include="MartinCostello.Logging.XUnit" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Net.Http.Json" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
</Project>

509
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;
/// <summary>
/// Exposes methods that allow sending OpenID Connect
/// requests and extracting the corresponding responses.
/// </summary>
public class OpenIddictValidationIntegrationTestClient : IAsyncDisposable
{
/// <summary>
/// Initializes a new instance of the OpenID Connect client.
/// </summary>
public OpenIddictValidationIntegrationTestClient()
: this(new HttpClient())
{
}
/// <summary>
/// Initializes a new instance of the OpenID Connect client.
/// </summary>
/// <param name="client">The HTTP client used to communicate with the OpenID Connect server.</param>
public OpenIddictValidationIntegrationTestClient(HttpClient client)
: this(client, new HtmlParser())
{
}
/// <summary>
/// Initializes a new instance of the OpenID Connect client.
/// </summary>
/// <param name="client">The HTTP client used to communicate with the OpenID Connect server.</param>
/// <param name="parser">The HTML parser used to parse the responses returned by the OpenID Connect server.</param>
public OpenIddictValidationIntegrationTestClient(HttpClient client, HtmlParser parser)
{
HttpClient = client ?? throw new ArgumentNullException(nameof(client));
HtmlParser = parser ?? throw new ArgumentNullException(nameof(parser));
}
/// <summary>
/// Gets the underlying HTTP client used to
/// communicate with the OpenID Connect server.
/// </summary>
public HttpClient HttpClient { get; }
/// <summary>
/// Gets the underlying HTML parser used to parse the
/// responses returned by the OpenID Connect server.
/// </summary>
public HtmlParser HtmlParser { get; }
/// <summary>
/// Sends an empty OpenID Connect request to the given endpoint using GET
/// and converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> GetAsync(string uri)
=> GetAsync(uri, new OpenIddictRequest());
/// <summary>
/// Sends an empty OpenID Connect request to the given endpoint using GET
/// and converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> GetAsync(Uri uri)
=> GetAsync(uri, new OpenIddictRequest());
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint using GET
/// and converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> 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);
}
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint using GET
/// and converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> GetAsync(Uri uri, OpenIddictRequest request)
=> SendAsync(HttpMethod.Get, uri, request);
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint using POST
/// and converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> 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);
}
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint using POST
/// and converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> PostAsync(Uri uri, OpenIddictRequest request)
=> SendAsync(HttpMethod.Post, uri, request);
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint and
/// converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="method">The HTTP method used to send the OpenID Connect request.</param>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> 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);
}
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint and
/// converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="method">The HTTP method used to send the OpenID Connect request.</param>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public Task<OpenIddictResponse> 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);
}
/// <summary>
/// Sends a generic OpenID Connect request to the given endpoint and
/// converts the returned response to an OpenID Connect response.
/// </summary>
/// <param name="method">The HTTP method used to send the OpenID Connect request.</param>
/// <param name="uri">The endpoint to which the request is sent.</param>
/// <param name="request">The OpenID Connect request to send.</param>
/// <returns>The OpenID Connect response returned by the server.</returns>
public virtual async Task<OpenIddictResponse> 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<KeyValuePair<string?, string?>>();
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<string?, string?>(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<string?, string?>(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<OpenIddictResponse> GetResponseAsync(HttpResponseMessage message)
{
if (message.Headers.WwwAuthenticate.Count is not 0)
{
var parameters = new Dictionary<string, StringValues>(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<KeyValuePair<string, string?>>();
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<string, string?>(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<string, StringValues>(grouping.Key, values.ToArray()));
}
else if (string.Equals(message.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase))
{
return (await message.Content!.ReadFromJsonAsync<OpenIddictResponse>())!;
}
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<KeyValuePair<string, string?>>();
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<string, string?>(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<string, StringValues>(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<KeyValuePair<string, string>>();
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<string, string>(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<string, StringValues>(grouping.Key, values.ToArray()));
}
return new OpenIddictResponse();
}
public ValueTask DisposeAsync()
{
HttpClient.Dispose();
return default;
}
}

17
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;
/// <summary>
/// Represents a test host used by the validation integration tests.
/// </summary>
public abstract class OpenIddictValidationIntegrationTestServer : IAsyncDisposable
{
public abstract ValueTask<OpenIddictValidationIntegrationTestClient> CreateClientAsync();
public virtual ValueTask DisposeAsync() => default;
}

325
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<OpenIddictConfiguration>(new()
{
Issuer = new Uri("https://fabrikam.com/")
});
}));
await using var client = await server.CreateClientAsync();
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(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<ProcessAuthenticationContext>(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<ProcessAuthenticationContext>(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<ValidateTokenContext>(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<ProcessChallengeContext>(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<ProcessChallengeContext>(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<OpenIddictAuthorization>()
.SetDefaultTokenEntity<OpenIddictToken>();
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<OpenIddictValidationIntegrationTestServer> CreateServerAsync(
Action<OpenIddictValidationBuilder>? configuration = null);
protected OpenIddictAuthorizationManager<OpenIddictAuthorization> CreateAuthorizationManager(
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>? configuration = null)
{
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(
Mock.Of<IOpenIddictAuthorizationCache<OpenIddictAuthorization>>(),
OutputHelper.ToLogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(),
Mock.Of<IOpenIddictAuthorizationStoreResolver>());
configuration?.Invoke(manager);
return manager.Object;
}
protected OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>>? configuration = null)
{
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(
Mock.Of<IOpenIddictTokenCache<OpenIddictToken>>(),
OutputHelper.ToLogger<OpenIddictTokenManager<OpenIddictToken>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>(),
Mock.Of<IOpenIddictTokenStoreResolver>());
configuration?.Invoke(manager);
return manager.Object;
}
public class OpenIddictAuthorization { }
public class OpenIddictToken { }
}

26
test/OpenIddict.Validation.Owin.IntegrationTests/OpenIddict.Validation.Owin.IntegrationTests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;net472;net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Validation.Owin\OpenIddict.Validation.Owin.csproj" />
<ProjectReference Include="..\OpenIddict.Validation.IntegrationTests\OpenIddict.Validation.IntegrationTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Owin.Testing" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
</Project>

38
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;
/// <summary>
/// Represents a test host used by the validation integration tests.
/// </summary>
public class OpenIddictValidationOwinIntegrationTestValidation : OpenIddictValidationIntegrationTestServer
{
public OpenIddictValidationOwinIntegrationTestValidation(TestServer server)
=> Server = server;
/// <summary>
/// Gets the ASP.NET Core test server used by this instance.
/// </summary>
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<OpenIddictValidationIntegrationTestClient> CreateClientAsync()
=> new(new OpenIddictValidationIntegrationTestClient(Server.HttpClient));
public override ValueTask DisposeAsync()
{
// Dispose of the underlying test server.
Server.Dispose();
return default;
}
}

217
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<ValidateTokenContext>(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<ValidateTokenContext>(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<OpenIddictValidationIntegrationTestServer> CreateServerAsync(Action<OpenIddictValidationBuilder>? 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<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
var response = transaction?.GetProperty<object>("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<string, string?[]?>(
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));
}
}
Loading…
Cancel
Save